From a0f977af78fd8d567f4718574b9e333814ff7599 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 6 Jan 2020 09:37:21 -0500 Subject: [PATCH] Block Parser: Implement block schema numeric range validation --- packages/blocks/src/api/parser.js | 67 ++++++++++++++++++++- packages/blocks/src/api/test/parser.js | 82 ++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 2 deletions(-) diff --git a/packages/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js index 72b45bb12d9de7..29d5d5bec2cb82 100644 --- a/packages/blocks/src/api/parser.js +++ b/packages/blocks/src/api/parser.js @@ -113,6 +113,20 @@ export function isOfTypes( value, types ) { return types.some( ( type ) => isOfType( value, type ) ); } +/** + * Returns true if the given JSON schema type is numeric, either "integer" or + * "number". + * + * @see https://json-schema.org/understanding-json-schema/reference/numeric.html + * + * @param {string} type Type to test. + * + * @return {boolean} Whether type is numeric. + */ +export function isNumericType( type ) { + return type === 'integer' || type === 'number'; +} + /** * Returns true if value is valid per the given block attribute schema type * definition, or false otherwise. @@ -143,6 +157,56 @@ export function isValidByEnum( value, enumSet ) { return ! Array.isArray( enumSet ) || enumSet.includes( value ); } +/** + * Returns true if the value is valid according to the given JSON schema, or + * false otherwise. Conforms to JSON Schema Draft 4 treatment of exclusivity as + * a boolean indicator. + * + * @see https://json-schema.org/understanding-json-schema/reference/numeric.html#range + * + * @param {number} value Number to validate. + * @param {Object} schema Schema. + * @param {number} [schema.minimum] Minimum valid value. + * @param {boolean} [schema.exclusiveMinimum] Whether minimum is exclusive. + * @param {number} [schema.maximum] Maximum valid value. + * @param {boolean} [schema.exclusiveMaximum] Whether maximum is exclusive. + * + * @return {boolean} Whether value is valid. + */ +export function isInRange( value, schema ) { + const { minimum, exclusiveMinimum, maximum, exclusiveMaximum } = schema; + + return ( + ( minimum === undefined || + ( exclusiveMinimum ? value > minimum : value >= minimum ) ) && + ( maximum === undefined || + ( exclusiveMaximum ? value < maximum : value <= maximum ) ) + ); +} + +/** + * Returns true if the given value is valid based on the given JSON schema, or + * false otherwise. This is not a complete JSON schema validator, and instead is + * intended to recreate an equivalent behavior to the WordPress REST JSON schema + * validation. + * + * @see https://developer.wordpress.org/reference/functions/rest_validate_value_from_schema/ + * + * @param {*} value Value to validate. + * @param {Object} schema JSON schema object. + * + * @return {boolean} Whether value is valid. + */ +export function isValidSchemaValue( value, schema ) { + const { type, enum: enumSet } = schema; + + return ( + isValidByType( value, type ) && + isValidByEnum( value, enumSet ) && + ( ! isNumericType( type ) || isInRange( value, schema ) ) + ); +} + /** * Returns true if the given attribute schema describes a value which may be * an ambiguous string. @@ -239,7 +303,6 @@ export function getBlockAttribute( innerHTML, commentAttributes ) { - const { type, enum: enumSet } = attributeSchema; let value; switch ( attributeSchema.source ) { @@ -261,7 +324,7 @@ export function getBlockAttribute( break; } - if ( ! isValidByType( value, type ) || ! isValidByEnum( value, enumSet ) ) { + if ( ! isValidSchemaValue( value, attributeSchema ) ) { // Reject the value if it is not valid. Reverting to the undefined // value ensures the default is respected, if applicable. value = undefined; diff --git a/packages/blocks/src/api/test/parser.js b/packages/blocks/src/api/test/parser.js index be377dd956da0a..2d38ef365a8818 100644 --- a/packages/blocks/src/api/test/parser.js +++ b/packages/blocks/src/api/test/parser.js @@ -17,8 +17,11 @@ import { toBooleanAttributeMatcher, isOfType, isOfTypes, + isNumericType, isValidByType, isValidByEnum, + isInRange, + isValidSchemaValue, serializeBlockNode, } from '../parser'; import { @@ -144,6 +147,18 @@ describe( 'block parser', () => { } ); } ); + describe( 'isNumericType', () => { + it( 'returns false for non-numeric type', () => { + expect( isNumericType( undefined ) ).toBe( false ); + expect( isNumericType( 'string' ) ).toBe( false ); + } ); + + it( 'returns true for numeric type', () => { + expect( isNumericType( 'integer' ) ).toBe( true ); + expect( isNumericType( 'number' ) ).toBe( true ); + } ); + } ); + describe( 'isValidByType', () => { it( 'returns true if type undefined', () => { expect( isValidByType( null ) ).toBe( true ); @@ -180,6 +195,73 @@ describe( 'block parser', () => { } ); } ); + describe( 'isInRange', () => { + describe( 'minimum', () => { + it( 'is valid if undefined', () => { + expect( isInRange( 10, {} ) ).toBe( true ); + } ); + + it( 'is valid by non-exclusive', () => { + expect( isInRange( 9, { minimum: 10 } ) ).toBe( false ); + expect( isInRange( 10, { minimum: 10 } ) ).toBe( true ); + expect( isInRange( 11, { minimum: 10 } ) ).toBe( true ); + } ); + + it( 'is valid by exclusive', () => { + expect( + isInRange( 9, { minimum: 10, exclusiveMinimum: true } ) + ).toBe( false ); + expect( + isInRange( 10, { minimum: 10, exclusiveMinimum: true } ) + ).toBe( false ); + expect( + isInRange( 11, { minimum: 10, exclusiveMinimum: true } ) + ).toBe( true ); + } ); + } ); + + describe( 'maximum', () => { + it( 'is valid if undefined', () => { + expect( isInRange( 10, {} ) ).toBe( true ); + } ); + + it( 'is valid by non-exclusive', () => { + expect( isInRange( 9, { maximum: 10 } ) ).toBe( true ); + expect( isInRange( 10, { maximum: 10 } ) ).toBe( true ); + expect( isInRange( 11, { maximum: 10 } ) ).toBe( false ); + } ); + + it( 'is valid by exclusive', () => { + expect( + isInRange( 9, { maximum: 10, exclusiveMaximum: true } ) + ).toBe( true ); + expect( + isInRange( 10, { maximum: 10, exclusiveMaximum: true } ) + ).toBe( false ); + expect( + isInRange( 11, { maximum: 10, exclusiveMaximum: true } ) + ).toBe( false ); + } ); + } ); + } ); + + describe( 'isValidSchemaValue', () => { + // These tests are intentionally sparse, since the bulk of the logic is + // deferred to separate schema-specific validation functions. + + it( 'validates value by schema', () => { + expect( isValidSchemaValue( 2, { enum: [ 1, 2, 3 ] } ) ).toBe( + true + ); + expect( + isValidSchemaValue( 2, { type: 'number', maximum: 2 } ) + ).toBe( true ); + expect( isValidSchemaValue( 'foo', { type: 'string' } ) ).toBe( + true + ); + } ); + } ); + describe( 'parseWithAttributeSchema', () => { it( 'should return the matcher’s attribute value', () => { const value = parseWithAttributeSchema( '
chicken
', {