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 Mar 12, 2020
1 parent 79df76b commit a0f977a
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 2 deletions.
67 changes: 65 additions & 2 deletions packages/blocks/src/api/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -239,7 +303,6 @@ export function getBlockAttribute(
innerHTML,
commentAttributes
) {
const { type, enum: enumSet } = attributeSchema;
let value;

switch ( attributeSchema.source ) {
Expand All @@ -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;
Expand Down
82 changes: 82 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,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( '<div>chicken</div>', {
Expand Down

0 comments on commit a0f977a

Please sign in to comment.