Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block Parser: Implement block schema numeric range validation #19433

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/block-library/src/column/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
},
"width": {
"type": "number",
"min": 0,
"max": 100
"minimum": 0,
"maximum": 100
}
}
}
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