REST API: Add support for the oneOf and anyOf keywords.

This allows for REST API routes to define more complex validation requirements as JSON Schema instead of procedural validation.

The error code returned from `rest_validate_value_from_schema` for invalid parameter types has been changed from the generic `rest_invalid_param` to the more specific `rest_invalid_type`.

Props yakimun, johnbillion, TimothyBlynJacobs.
Fixes #51025.


git-svn-id: https://develop.svn.wordpress.org/trunk@49246 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Timothy Jacobs 2020-10-20 18:22:39 +00:00
parent 194d32b970
commit 54aa0bc7d1
10 changed files with 1546 additions and 38 deletions

View File

@ -1664,6 +1664,216 @@ function rest_find_matching_pattern_property_schema( $property, $args ) {
return null;
}
/**
* Formats a combining operation error into a WP_Error object.
*
* @since 5.6.0
*
* @param string $param The parameter name.
* @param array $error The error details.
* @return WP_Error
*/
function rest_format_combining_operation_error( $param, $error ) {
$position = $error['index'];
$reason = $error['error_object']->get_error_message();
if ( isset( $error['schema']['title'] ) ) {
$title = $error['schema']['title'];
return new WP_Error(
'rest_invalid_param',
/* translators: 1: Parameter, 2: Schema title, 3: Reason. */
sprintf( __( '%1$s is not a valid %2$s. Reason: %3$s' ), $param, $title, $reason ),
array( 'position' => $position )
);
}
return new WP_Error(
'rest_invalid_param',
/* translators: 1: Parameter, 2: Reason. */
sprintf( __( '%1$s does not match the expected format. Reason: %2$s' ), $param, $reason ),
array( 'position' => $position )
);
}
/**
* Gets the error of combining operation.
*
* @since 5.6.0
*
* @param array $value The value to validate.
* @param string $param The parameter name, used in error messages.
* @param array $errors The errors array, to search for possible error.
* @return WP_Error The combining operation error.
*/
function rest_get_combining_operation_error( $value, $param, $errors ) {
// If there is only one error, simply return it.
if ( 1 === count( $errors ) ) {
return rest_format_combining_operation_error( $param, $errors[0] );
}
// Filter out all errors related to type validation.
$filtered_errors = array();
foreach ( $errors as $error ) {
$error_code = $error['error_object']->get_error_code();
$error_data = $error['error_object']->get_error_data();
if ( 'rest_invalid_type' !== $error_code || ( isset( $error_data['param'] ) && $param !== $error_data['param'] ) ) {
$filtered_errors[] = $error;
}
}
// If there is only one error left, simply return it.
if ( 1 === count( $filtered_errors ) ) {
return rest_format_combining_operation_error( $param, $filtered_errors[0] );
}
// If there are only errors related to object validation, try choosing the most appropriate one.
if ( count( $filtered_errors ) > 1 && 'object' === $filtered_errors[0]['schema']['type'] ) {
$result = null;
$number = 0;
foreach ( $filtered_errors as $error ) {
if ( isset( $error['schema']['properties'] ) ) {
$n = count( array_intersect_key( $error['schema']['properties'], $value ) );
if ( $n > $number ) {
$result = $error;
$number = $n;
}
}
}
if ( null !== $result ) {
return rest_format_combining_operation_error( $param, $result );
}
}
// If each schema has a title, include those titles in the error message.
$schema_titles = array();
foreach ( $errors as $error ) {
if ( isset( $error['schema']['title'] ) ) {
$schema_titles[] = $error['schema']['title'];
}
}
if ( count( $schema_titles ) === count( $errors ) ) {
/* translators: 1: Parameter, 2: Schema titles. */
return new WP_Error( 'rest_invalid_param', wp_sprintf( __( '%1$s is not a valid %2$l.' ), $param, $schema_titles ) );
}
/* translators: 1: Parameter. */
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s does not match any of the expected formats.' ), $param ) );
}
/**
* Finds the matching schema among the "anyOf" schemas.
*
* @since 5.6.0
*
* @param mixed $value The value to validate.
* @param array $args The schema array to use.
* @param string $param The parameter name, used in error messages.
* @return array|WP_Error The matching schema or WP_Error instance if all schemas do not match.
*/
function rest_find_any_matching_schema( $value, $args, $param ) {
$errors = array();
foreach ( $args['anyOf'] as $index => $schema ) {
if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) {
$schema['type'] = $args['type'];
}
$is_valid = rest_validate_value_from_schema( $value, $schema, $param );
if ( ! is_wp_error( $is_valid ) ) {
return $schema;
}
$errors[] = array(
'error_object' => $is_valid,
'schema' => $schema,
'index' => $index,
);
}
return rest_get_combining_operation_error( $value, $param, $errors );
}
/**
* Finds the matching schema among the "oneOf" schemas.
*
* @since 5.6.0
*
* @param mixed $value The value to validate.
* @param array $args The schema array to use.
* @param string $param The parameter name, used in error messages.
* @param bool $stop_after_first_match Optional. Whether the process should stop after the first successful match.
* @return array|WP_Error The matching schema or WP_Error instance if the number of matching schemas is not equal to one.
*/
function rest_find_one_matching_schema( $value, $args, $param, $stop_after_first_match = false ) {
$matching_schemas = array();
$errors = array();
foreach ( $args['oneOf'] as $index => $schema ) {
if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) {
$schema['type'] = $args['type'];
}
$is_valid = rest_validate_value_from_schema( $value, $schema, $param );
if ( ! is_wp_error( $is_valid ) ) {
if ( $stop_after_first_match ) {
return $schema;
}
$matching_schemas[] = array(
'schema_object' => $schema,
'index' => $index,
);
} else {
$errors[] = array(
'error_object' => $is_valid,
'schema' => $schema,
'index' => $index,
);
}
}
if ( ! $matching_schemas ) {
return rest_get_combining_operation_error( $value, $param, $errors );
}
if ( count( $matching_schemas ) > 1 ) {
$schema_positions = array();
$schema_titles = array();
foreach ( $matching_schemas as $schema ) {
$schema_positions[] = $schema['index'];
if ( isset( $schema['schema_object']['title'] ) ) {
$schema_titles[] = $schema['schema_object']['title'];
}
}
// If each schema has a title, include those titles in the error message.
if ( count( $schema_titles ) === count( $matching_schemas ) ) {
return new WP_Error(
'rest_invalid_param',
/* translators: 1: Parameter, 2: Schema titles. */
wp_sprintf( __( '%1$s matches %2$l, but should match only one.' ), $param, $schema_titles ),
array( 'positions' => $schema_positions )
);
}
return new WP_Error(
'rest_invalid_param',
/* translators: 1: Parameter. */
sprintf( __( '%1$s matches more than one of the expected formats.' ), $param ),
array( 'positions' => $schema_positions )
);
}
return $matching_schemas[0]['schema_object'];
}
/**
* Validate a value based on a schema.
*
@ -1679,6 +1889,7 @@ function rest_find_matching_pattern_property_schema( $property, $args ) {
* @since 5.6.0 Support the "minProperties" and "maxProperties" keywords for objects.
* Support the "multipleOf" keyword for numbers and integers.
* Support the "patternProperties" keyword for objects.
* Support the "anyOf" and "oneOf" keywords.
*
* @param mixed $value The value to validate.
* @param array $args Schema array to use for validation.
@ -1686,6 +1897,28 @@ function rest_find_matching_pattern_property_schema( $property, $args ) {
* @return true|WP_Error
*/
function rest_validate_value_from_schema( $value, $args, $param = '' ) {
if ( isset( $args['anyOf'] ) ) {
$matching_schema = rest_find_any_matching_schema( $value, $args, $param );
if ( is_wp_error( $matching_schema ) ) {
return $matching_schema;
}
if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) {
$args['type'] = $matching_schema['type'];
}
}
if ( isset( $args['oneOf'] ) ) {
$matching_schema = rest_find_one_matching_schema( $value, $args, $param );
if ( is_wp_error( $matching_schema ) ) {
return $matching_schema;
}
if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) {
$args['type'] = $matching_schema['type'];
}
}
$allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
if ( ! isset( $args['type'] ) ) {
@ -1697,8 +1930,12 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) {
$best_type = rest_handle_multi_type_schema( $value, $args, $param );
if ( ! $best_type ) {
return new WP_Error(
'rest_invalid_type',
/* translators: 1: Parameter, 2: List of types. */
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ) );
sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ),
array( 'param' => $param )
);
}
$args['type'] = $best_type;
@ -1715,8 +1952,12 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) {
if ( 'array' === $args['type'] ) {
if ( ! rest_is_array( $value ) ) {
return new WP_Error(
'rest_invalid_type',
/* translators: 1: Parameter, 2: Type name. */
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ) );
sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ),
array( 'param' => $param )
);
}
$value = rest_sanitize_array( $value );
@ -1748,8 +1989,12 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) {
if ( 'object' === $args['type'] ) {
if ( ! rest_is_object( $value ) ) {
return new WP_Error(
'rest_invalid_type',
/* translators: 1: Parameter, 2: Type name. */
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ) );
sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ),
array( 'param' => $param )
);
}
$value = rest_sanitize_object( $value );
@ -1816,8 +2061,12 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) {
if ( 'null' === $args['type'] ) {
if ( null !== $value ) {
return new WP_Error(
'rest_invalid_type',
/* translators: 1: Parameter, 2: Type name. */
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ) );
sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ),
array( 'param' => $param )
);
}
return true;
@ -1832,8 +2081,12 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) {
if ( in_array( $args['type'], array( 'integer', 'number' ), true ) ) {
if ( ! is_numeric( $value ) ) {
return new WP_Error(
'rest_invalid_type',
/* translators: 1: Parameter, 2: Type name. */
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) );
sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ),
array( 'param' => $param )
);
}
if ( isset( $args['multipleOf'] ) && fmod( $value, $args['multipleOf'] ) !== 0.0 ) {
@ -1843,19 +2096,31 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) {
}
if ( 'integer' === $args['type'] && ! rest_is_integer( $value ) ) {
return new WP_Error(
'rest_invalid_type',
/* translators: 1: Parameter, 2: Type name. */
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ),
array( 'param' => $param )
);
}
if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) {
return new WP_Error(
'rest_invalid_type',
/* translators: 1: Parameter, 2: Type name. */
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ) );
sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ),
array( 'param' => $param )
);
}
if ( 'string' === $args['type'] ) {
if ( ! is_string( $value ) ) {
return new WP_Error(
'rest_invalid_type',
/* translators: 1: Parameter, 2: Type name. */
return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ) );
sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ),
array( 'param' => $param )
);
}
if ( isset( $args['minLength'] ) && mb_strlen( $value ) < $args['minLength'] ) {
@ -1976,6 +2241,7 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) {
*
* @since 4.7.0
* @since 5.5.0 Added the `$param` parameter.
* @since 5.6.0 Support the "anyOf" and "oneOf" keywords.
*
* @param mixed $value The value to sanitize.
* @param array $args Schema array to use for sanitization.
@ -1983,6 +2249,32 @@ function rest_validate_value_from_schema( $value, $args, $param = '' ) {
* @return mixed|WP_Error The sanitized value or a WP_Error instance if the value cannot be safely sanitized.
*/
function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
if ( isset( $args['anyOf'] ) ) {
$matching_schema = rest_find_any_matching_schema( $value, $args, $param );
if ( is_wp_error( $matching_schema ) ) {
return $matching_schema;
}
if ( ! isset( $args['type'] ) ) {
$args['type'] = $matching_schema['type'];
}
$value = rest_sanitize_value_from_schema( $value, $matching_schema, $param );
}
if ( isset( $args['oneOf'] ) ) {
$matching_schema = rest_find_one_matching_schema( $value, $args, $param );
if ( is_wp_error( $matching_schema ) ) {
return $matching_schema;
}
if ( ! isset( $args['type'] ) ) {
$args['type'] = $matching_schema['type'];
}
$value = rest_sanitize_value_from_schema( $value, $matching_schema, $param );
}
$allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
if ( ! isset( $args['type'] ) ) {
@ -2198,6 +2490,7 @@ function rest_parse_embed_param( $embed ) {
*
* @since 5.5.0
* @since 5.6.0 Support the "patternProperties" keyword for objects.
* Support the "anyOf" and "oneOf" keywords.
*
* @param array|object $data The response data to modify.
* @param array $schema The schema for the endpoint used to filter the response.
@ -2205,6 +2498,28 @@ function rest_parse_embed_param( $embed ) {
* @return array|object The filtered response data.
*/
function rest_filter_response_by_context( $data, $schema, $context ) {
if ( isset( $schema['anyOf'] ) ) {
$matching_schema = rest_find_any_matching_schema( $data, $schema, '' );
if ( ! is_wp_error( $matching_schema ) ) {
if ( ! isset( $schema['type'] ) ) {
$schema['type'] = $matching_schema['type'];
}
$data = rest_filter_response_by_context( $data, $matching_schema, $context );
}
}
if ( isset( $schema['oneOf'] ) ) {
$matching_schema = rest_find_one_matching_schema( $data, $schema, '', true );
if ( ! is_wp_error( $matching_schema ) ) {
if ( ! isset( $schema['type'] ) ) {
$schema['type'] = $matching_schema['type'];
}
$data = rest_filter_response_by_context( $data, $matching_schema, $context );
}
}
if ( ! is_array( $data ) && ! is_object( $data ) ) {
return $data;
}
@ -2471,6 +2786,8 @@ function rest_get_endpoint_args_for_schema( $schema, $method = WP_REST_Server::C
'minItems',
'maxItems',
'uniqueItems',
'anyOf',
'oneOf',
);
foreach ( $schema_properties as $field_id => $params ) {

View File

@ -1547,6 +1547,210 @@ class Tests_REST_API extends WP_UnitTestCase {
),
array(),
),
'anyOf applies the correct schema' => array(
array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'type' => 'object',
'anyOf' => array(
array(
'properties' => array(
'a' => array(
'type' => 'string',
'context' => array( 'view' ),
),
'b' => array(
'type' => 'string',
'context' => array( 'edit' ),
),
),
),
array(
'properties' => array(
'a' => array(
'type' => 'integer',
'context' => array( 'edit' ),
),
'b' => array(
'type' => 'integer',
'context' => array( 'view' ),
),
),
),
),
),
array(
'a' => 1,
'b' => 2,
),
array(
'b' => 2,
),
),
'anyOf is ignored if no valid schema is found' => array(
array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'type' => 'object',
'anyOf' => array(
array(
'properties' => array(
'a' => array(
'type' => 'string',
'context' => array( 'view' ),
),
'b' => array(
'type' => 'string',
'context' => array( 'edit' ),
),
),
),
array(
'properties' => array(
'a' => array(
'type' => 'integer',
'context' => array( 'edit' ),
),
'b' => array(
'type' => 'integer',
'context' => array( 'view' ),
),
),
),
),
),
array(
'a' => true,
'b' => false,
),
array(
'a' => true,
'b' => false,
),
),
'oneOf applies the correct schema' => array(
array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'type' => 'object',
'oneOf' => array(
array(
'properties' => array(
'a' => array(
'type' => 'string',
'context' => array( 'view' ),
),
'b' => array(
'type' => 'string',
'context' => array( 'edit' ),
),
),
),
array(
'properties' => array(
'a' => array(
'type' => 'integer',
'context' => array( 'edit' ),
),
'b' => array(
'type' => 'integer',
'context' => array( 'view' ),
),
),
),
),
),
array(
'a' => 1,
'b' => 2,
),
array(
'b' => 2,
),
),
'oneOf ignored if no valid schema was found' => array(
array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'type' => 'object',
'anyOf' => array(
array(
'properties' => array(
'a' => array(
'type' => 'string',
'context' => array( 'view' ),
),
'b' => array(
'type' => 'string',
'context' => array( 'edit' ),
),
),
),
array(
'properties' => array(
'a' => array(
'type' => 'integer',
'context' => array( 'edit' ),
),
'b' => array(
'type' => 'integer',
'context' => array( 'view' ),
),
),
),
),
),
array(
'a' => true,
'b' => false,
),
array(
'a' => true,
'b' => false,
),
),
'oneOf combined with base' => array(
array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'type' => 'object',
'properties' => array(
'c' => array(
'type' => 'integer',
'context' => array( 'edit' ),
),
),
'oneOf' => array(
array(
'properties' => array(
'a' => array(
'type' => 'string',
'context' => array( 'view' ),
),
'b' => array(
'type' => 'string',
'context' => array( 'edit' ),
),
),
),
array(
'properties' => array(
'a' => array(
'type' => 'integer',
'context' => array( 'edit' ),
),
'b' => array(
'type' => 'integer',
'context' => array( 'view' ),
),
),
),
),
),
array(
'a' => 1,
'b' => 2,
'c' => 3,
),
array(
'b' => 2,
),
),
);
}

View File

@ -0,0 +1,229 @@
[
{
"description": "anyOf",
"schema": {
"anyOf": [
{
"type": "integer"
},
{
"type": [
"integer",
"number"
],
"minimum": 2
}
]
},
"tests": [
{
"description": "first anyOf valid",
"data": 1,
"valid": true
},
{
"description": "second anyOf valid",
"data": 2.5,
"valid": true
},
{
"description": "both anyOf valid",
"data": 3,
"valid": true
},
{
"description": "neither anyOf valid",
"data": 1.5,
"valid": false
}
]
},
{
"description": "anyOf with base schema",
"schema": {
"type": "string",
"anyOf": [
{
"maxLength": 2
},
{
"minLength": 4
}
]
},
"tests": [
{
"description": "mismatch base schema",
"data": 3,
"valid": false
},
{
"description": "one anyOf valid",
"data": "foobar",
"valid": true
},
{
"description": "both anyOf invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "anyOf with boolean schemas, all true",
"schema": {
"anyOf": [
true,
true
]
},
"tests": [
{
"description": "any value is valid",
"data": "foo",
"valid": true
}
]
},
{
"description": "anyOf with boolean schemas, some true",
"schema": {
"anyOf": [
true,
false
]
},
"tests": [
{
"description": "any value is valid",
"data": "foo",
"valid": true
}
]
},
{
"description": "anyOf with boolean schemas, all false",
"schema": {
"anyOf": [
false,
false
]
},
"tests": [
{
"description": "any value is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "anyOf complex types",
"schema": {
"type": "object",
"anyOf": [
{
"properties": {
"bar": {
"type": "integer"
}
},
"required": [
"bar"
]
},
{
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
}
]
},
"tests": [
{
"description": "first anyOf valid (complex)",
"data": {
"bar": 2
},
"valid": true
},
{
"description": "second anyOf valid (complex)",
"data": {
"foo": "baz"
},
"valid": true
},
{
"description": "both anyOf valid (complex)",
"data": {
"foo": "baz",
"bar": 2
},
"valid": true
},
{
"description": "neither anyOf valid (complex)",
"data": {
"foo": 2,
"bar": "quux"
},
"valid": false
}
]
},
{
"description": "anyOf with one empty schema",
"schema": {
"anyOf": [
{
"type": "number"
},
{}
]
},
"tests": [
{
"description": "string is valid",
"data": "foo",
"valid": true
},
{
"description": "number is valid",
"data": 123,
"valid": true
}
]
},
{
"description": "nested anyOf, to check validation semantics",
"schema": {
"anyOf": [
{
"anyOf": [
{
"type": "null"
}
]
}
]
},
"tests": [
{
"description": "null is valid",
"data": null,
"valid": true
},
{
"description": "anything non-null is invalid",
"data": 123,
"valid": false
}
]
}
]

View File

@ -0,0 +1,365 @@
[
{
"description": "oneOf",
"schema": {
"oneOf": [
{
"type": "integer"
},
{
"type": [
"number",
"integer"
],
"minimum": 2
}
]
},
"tests": [
{
"description": "first oneOf valid",
"data": 1,
"valid": true
},
{
"description": "second oneOf valid",
"data": 2.5,
"valid": true
},
{
"description": "both oneOf valid",
"data": 3,
"valid": false
},
{
"description": "neither oneOf valid",
"data": 1.5,
"valid": false
}
]
},
{
"description": "oneOf with base schema",
"schema": {
"type": "string",
"oneOf": [
{
"minLength": 2
},
{
"maxLength": 4
}
]
},
"tests": [
{
"description": "mismatch base schema",
"data": 3,
"valid": false
},
{
"description": "one oneOf valid",
"data": "foobar",
"valid": true
},
{
"description": "both oneOf valid",
"data": "foo",
"valid": false
}
]
},
{
"description": "oneOf with boolean schemas, all true",
"schema": {
"oneOf": [
true,
true,
true
]
},
"tests": [
{
"description": "any value is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "oneOf with boolean schemas, one true",
"schema": {
"oneOf": [
true,
false,
false
]
},
"tests": [
{
"description": "any value is valid",
"data": "foo",
"valid": true
}
]
},
{
"description": "oneOf with boolean schemas, more than one true",
"schema": {
"oneOf": [
true,
true,
false
]
},
"tests": [
{
"description": "any value is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "oneOf with boolean schemas, all false",
"schema": {
"oneOf": [
false,
false,
false
]
},
"tests": [
{
"description": "any value is invalid",
"data": "foo",
"valid": false
}
]
},
{
"description": "oneOf complex types",
"schema": {
"type": "object",
"oneOf": [
{
"properties": {
"bar": {
"type": "integer"
}
},
"required": [
"bar"
]
},
{
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
}
]
},
"tests": [
{
"description": "first oneOf valid (complex)",
"data": {
"bar": 2
},
"valid": true
},
{
"description": "second oneOf valid (complex)",
"data": {
"foo": "baz"
},
"valid": true
},
{
"description": "both oneOf valid (complex)",
"data": {
"foo": "baz",
"bar": 2
},
"valid": false
},
{
"description": "neither oneOf valid (complex)",
"data": {
"foo": 2,
"bar": "quux"
},
"valid": false
}
]
},
{
"description": "oneOf with empty schema",
"schema": {
"oneOf": [
{
"type": "number"
},
{}
]
},
"tests": [
{
"description": "one valid - valid",
"data": "foo",
"valid": true
},
{
"description": "both valid - invalid",
"data": 123,
"valid": false
}
]
},
{
"description": "oneOf with required",
"schema": {
"type": "object",
"oneOf": [
{
"required": [
"foo",
"bar"
]
},
{
"required": [
"foo",
"baz"
]
}
]
},
"tests": [
{
"description": "both invalid - invalid",
"data": {
"bar": 2
},
"valid": false
},
{
"description": "first valid - valid",
"data": {
"foo": 1,
"bar": 2
},
"valid": true
},
{
"description": "second valid - valid",
"data": {
"foo": 1,
"baz": 3
},
"valid": true
},
{
"description": "both valid - invalid",
"data": {
"foo": 1,
"bar": 2,
"baz": 3
},
"valid": false
}
]
},
{
"description": "oneOf with missing optional property",
"schema": {
"type": "object",
"oneOf": [
{
"properties": {
"bar": {
"type": "integer"
},
"baz": {
"type": "string"
}
},
"required": [
"bar"
]
},
{
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
}
]
},
"tests": [
{
"description": "first oneOf valid",
"data": {
"bar": 8
},
"valid": true
},
{
"description": "second oneOf valid",
"data": {
"foo": "foo"
},
"valid": true
},
{
"description": "both oneOf valid",
"data": {
"foo": "foo",
"bar": 8
},
"valid": false
},
{
"description": "neither oneOf valid",
"data": {
"baz": "quux"
},
"valid": false
}
]
},
{
"description": "nested oneOf, to check validation semantics",
"schema": {
"oneOf": [
{
"oneOf": [
{
"type": "null"
}
]
}
]
},
"tests": [
{
"description": "null is valid",
"data": null,
"valid": true
},
{
"description": "anything non-null is invalid",
"data": 123,
"valid": false
}
]
}
]

View File

@ -66,7 +66,7 @@ class WP_Test_REST_Controller extends WP_Test_REST_TestCase {
);
$this->assertErrorResponse(
'rest_invalid_param',
'rest_invalid_type',
rest_validate_request_arg( 'abc', $this->request, 'someinteger' )
);
}
@ -140,7 +140,7 @@ class WP_Test_REST_Controller extends WP_Test_REST_TestCase {
);
$this->assertErrorResponse(
'rest_invalid_param',
'rest_invalid_type',
rest_validate_request_arg( '123', $this->request, 'someboolean' )
);
}
@ -152,7 +152,7 @@ class WP_Test_REST_Controller extends WP_Test_REST_TestCase {
);
$this->assertErrorResponse(
'rest_invalid_param',
'rest_invalid_type',
rest_validate_request_arg( array( 'foo' => 'bar' ), $this->request, 'somestring' )
);
}
@ -297,6 +297,8 @@ class WP_Test_REST_Controller extends WP_Test_REST_TestCase {
'additionalProperties',
'minProperties',
'maxProperties',
'anyOf',
'oneOf',
);
foreach ( $object_properties as $property ) {
$this->assertArrayHasKey( $property, $args['someobject'] );

View File

@ -692,7 +692,7 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase {
$request->set_body_params( $data );
$response = rest_get_server()->dispatch( $request );
$this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
$this->assertErrorResponse( 'rest_invalid_type', $response, 400 );
}
public function test_set_value_invalid_value_multiple() {
@ -717,7 +717,7 @@ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase {
$request->set_body_params( $data );
$response = rest_get_server()->dispatch( $request );
$this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
$this->assertErrorResponse( 'rest_invalid_type', $response, 400 );
}
public function test_set_value_sanitized() {

View File

@ -587,4 +587,51 @@ class WP_Test_REST_Schema_Sanitization extends WP_UnitTestCase {
$this->assertTrue( rest_validate_value_from_schema( $data, $schema ) );
$this->assertWPError( rest_sanitize_value_from_schema( $data, $schema ) );
}
/**
* @ticket 51025
*/
public function test_any_of() {
$schema = array(
'anyOf' => array(
array(
'type' => 'integer',
'multipleOf' => 2,
),
array(
'type' => 'string',
'maxLength' => 1,
),
),
);
$this->assertSame( 4, rest_sanitize_value_from_schema( '4', $schema ) );
$this->assertSame( '5', rest_sanitize_value_from_schema( '5', $schema ) );
$this->assertWPError( rest_sanitize_value_from_schema( true, $schema ) );
$this->assertWPError( rest_sanitize_value_from_schema( '11', $schema ) );
}
/**
* @ticket 51025
*/
public function test_one_of() {
$schema = array(
'oneOf' => array(
array(
'type' => 'integer',
'multipleOf' => 2,
),
array(
'type' => 'string',
'maxLength' => 1,
),
),
);
$this->assertSame( 10, rest_sanitize_value_from_schema( '10', $schema ) );
$this->assertSame( '5', rest_sanitize_value_from_schema( '5', $schema ) );
$this->assertWPError( rest_sanitize_value_from_schema( true, $schema ) );
$this->assertWPError( rest_sanitize_value_from_schema( '11', $schema ) );
$this->assertWPError( rest_sanitize_value_from_schema( '4', $schema ) );
}
}

View File

@ -1253,4 +1253,312 @@ class WP_Test_REST_Schema_Validation extends WP_UnitTestCase {
$this->assertWPError( rest_validate_value_from_schema( 15.5, $schema ) );
}
/**
* @ticket 51025
*
* @dataProvider data_any_of
*
* @param array $data
* @param array $schema
* @param bool $valid
*/
public function test_any_of( $data, $schema, $valid ) {
$is_valid = rest_validate_value_from_schema( $data, $schema );
if ( $valid ) {
$this->assertTrue( $is_valid );
} else {
$this->assertWPError( $is_valid );
}
}
/**
* @return array
*/
public function data_any_of() {
$suites = json_decode( file_get_contents( __DIR__ . '/json_schema_test_suite/anyof.json' ), true );
$skip = array(
'anyOf with boolean schemas, all true',
'anyOf with boolean schemas, some true',
'anyOf with boolean schemas, all false',
'anyOf with one empty schema',
'nested anyOf, to check validation semantics',
);
$tests = array();
foreach ( $suites as $suite ) {
if ( in_array( $suite['description'], $skip, true ) ) {
continue;
}
foreach ( $suite['tests'] as $test ) {
$tests[ $suite['description'] . ': ' . $test['description'] ] = array(
$test['data'],
$suite['schema'],
$test['valid'],
);
}
}
return $tests;
}
/**
* @ticket 51025
*
* @dataProvider data_one_of
*
* @param array $data
* @param array $schema
* @param bool $valid
*/
public function test_one_of( $data, $schema, $valid ) {
$is_valid = rest_validate_value_from_schema( $data, $schema );
if ( $valid ) {
$this->assertTrue( $is_valid );
} else {
$this->assertWPError( $is_valid );
}
}
/**
* @return array
*/
public function data_one_of() {
$suites = json_decode( file_get_contents( __DIR__ . '/json_schema_test_suite/oneof.json' ), true );
$skip = array(
'oneOf with boolean schemas, all true',
'oneOf with boolean schemas, one true',
'oneOf with boolean schemas, more than one true',
'oneOf with boolean schemas, all false',
'oneOf with empty schema',
'nested oneOf, to check validation semantics',
);
$tests = array();
foreach ( $suites as $suite ) {
if ( in_array( $suite['description'], $skip, true ) ) {
continue;
}
foreach ( $suite['tests'] as $test ) {
$tests[ $suite['description'] . ': ' . $test['description'] ] = array(
$test['data'],
$suite['schema'],
$test['valid'],
);
}
}
return $tests;
}
/**
* @ticket 51025
*
* @dataProvider data_combining_operation_error_message
*
* @param $data
* @param $schema
* @param $expected
*/
public function test_combining_operation_error_message( $data, $schema, $expected ) {
$is_valid = rest_validate_value_from_schema( $data, $schema, 'foo' );
$this->assertWPError( $is_valid );
$this->assertSame( $expected, $is_valid->get_error_message() );
}
/**
* @return array
*/
public function data_combining_operation_error_message() {
return array(
array(
10,
array(
'anyOf' => array(
array(
'title' => 'circle',
'type' => 'integer',
'maximum' => 5,
),
),
),
'foo is not a valid circle. Reason: foo must be less than or equal to 5',
),
array(
10,
array(
'anyOf' => array(
array(
'type' => 'integer',
'maximum' => 5,
),
),
),
'foo does not match the expected format. Reason: foo must be less than or equal to 5',
),
array(
array( 'a' => 1 ),
array(
'anyOf' => array(
array( 'type' => 'boolean' ),
array(
'title' => 'circle',
'type' => 'object',
'properties' => array(
'a' => array( 'type' => 'string' ),
),
),
),
),
'foo is not a valid circle. Reason: foo[a] is not of type string.',
),
array(
array( 'a' => 1 ),
array(
'anyOf' => array(
array( 'type' => 'boolean' ),
array(
'type' => 'object',
'properties' => array(
'a' => array( 'type' => 'string' ),
),
),
),
),
'foo does not match the expected format. Reason: foo[a] is not of type string.',
),
array(
array(
'a' => 1,
'b' => 2,
'c' => 3,
),
array(
'anyOf' => array(
array( 'type' => 'boolean' ),
array(
'type' => 'object',
'properties' => array(
'a' => array( 'type' => 'string' ),
),
),
array(
'title' => 'square',
'type' => 'object',
'properties' => array(
'b' => array( 'type' => 'string' ),
'c' => array( 'type' => 'string' ),
),
),
array(
'type' => 'object',
'properties' => array(
'b' => array( 'type' => 'boolean' ),
'x' => array( 'type' => 'boolean' ),
),
),
),
),
'foo is not a valid square. Reason: foo[b] is not of type string.',
),
array(
array(
'a' => 1,
'b' => 2,
'c' => 3,
),
array(
'anyOf' => array(
array( 'type' => 'boolean' ),
array(
'type' => 'object',
'properties' => array(
'a' => array( 'type' => 'string' ),
),
),
array(
'type' => 'object',
'properties' => array(
'b' => array( 'type' => 'string' ),
'c' => array( 'type' => 'string' ),
),
),
array(
'type' => 'object',
'properties' => array(
'b' => array( 'type' => 'boolean' ),
'x' => array( 'type' => 'boolean' ),
),
),
),
),
'foo does not match the expected format. Reason: foo[b] is not of type string.',
),
array(
'test',
array(
'anyOf' => array(
array(
'title' => 'circle',
'type' => 'boolean',
),
array(
'title' => 'square',
'type' => 'integer',
),
array(
'title' => 'triangle',
'type' => 'null',
),
),
),
'foo is not a valid circle, square, and triangle.',
),
array(
'test',
array(
'anyOf' => array(
array( 'type' => 'boolean' ),
array( 'type' => 'integer' ),
array( 'type' => 'null' ),
),
),
'foo does not match any of the expected formats.',
),
array(
'test',
array(
'oneOf' => array(
array(
'title' => 'circle',
'type' => 'string',
),
array( 'type' => 'integer' ),
array(
'title' => 'triangle',
'type' => 'string',
),
),
),
'foo matches circle and triangle, but should match only one.',
),
array(
'test',
array(
'oneOf' => array(
array( 'type' => 'string' ),
array( 'type' => 'integer' ),
array( 'type' => 'string' ),
),
),
'foo matches more than one of the expected formats.',
),
);
}
}

View File

@ -639,7 +639,7 @@ class WP_Test_REST_Term_Meta_Fields extends WP_Test_REST_TestCase {
$request->set_body_params( $data );
$response = rest_get_server()->dispatch( $request );
$this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
$this->assertErrorResponse( 'rest_invalid_type', $response, 400 );
}
public function test_set_value_invalid_value_multiple() {
@ -664,7 +664,7 @@ class WP_Test_REST_Term_Meta_Fields extends WP_Test_REST_TestCase {
$request->set_body_params( $data );
$response = rest_get_server()->dispatch( $request );
$this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
$this->assertErrorResponse( 'rest_invalid_type', $response, 400 );
}
public function test_set_value_sanitized() {

View File

@ -128,6 +128,42 @@ class WP_REST_Test_Controller extends WP_REST_Controller {
),
'minProperties' => 1,
'maxProperties' => 10,
'anyOf' => array(
array(
'properties' => array(
'object_id' => array(
'type' => 'integer',
'minimum' => 100,
),
),
),
array(
'properties' => array(
'object_id' => array(
'type' => 'integer',
'maximum' => 100,
),
),
),
),
'oneOf' => array(
array(
'properties' => array(
'object_id' => array(
'type' => 'integer',
'minimum' => 100,
),
),
),
array(
'properties' => array(
'object_id' => array(
'type' => 'integer',
'maximum' => 100,
),
),
),
),
'ignored_prop' => 'ignored_prop',
'context' => array( 'view' ),
),