Skip to content

Commit

Permalink
Block Parser: Implement block schema numeric range validation
Browse files Browse the repository at this point in the history
  • Loading branch information
aduth committed Jan 6, 2020
1 parent a1475ee commit 4d0bc7a
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 2 deletions.
73 changes: 71 additions & 2 deletions packages/blocks/src/api/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,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.
Expand Down Expand Up @@ -147,6 +161,62 @@ 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.
Expand Down Expand Up @@ -234,7 +304,6 @@ export function parseWithAttributeSchema( innerHTML, attributeSchema ) {
* @return {*} Attribute value.
*/
export function getBlockAttribute( attributeKey, attributeSchema, innerHTML, commentAttributes ) {
const { type, enum: enumSet } = attributeSchema;
let value;

switch ( attributeSchema.source ) {
Expand All @@ -254,7 +323,7 @@ export function getBlockAttribute( attributeKey, attributeSchema, innerHTML, com
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;
Expand Down
64 changes: 64 additions & 0 deletions packages/blocks/src/api/test/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ import {
toBooleanAttributeMatcher,
isOfType,
isOfTypes,
isNumericType,
isValidByType,
isValidByEnum,
isInRange,
isValidSchemaValue,
serializeBlockNode,
} from '../parser';
import {
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -180,6 +195,55 @@ 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(
Expand Down

0 comments on commit 4d0bc7a

Please sign in to comment.