In I130326e5762e706be4889e308eeec45a6c7683c5 we introduced support for json schema references. In some use cases it may be more suitable to compute the references and inline them in the resulting schema. Introduce an $inlineReferences flag to do so. Possible follow-up: Memorization of already resolved and normalized references could speed up the full schema processing, T363678. Bug: T362098 Co-Authored-by: Sergio Gimeno <sgimeno@wikimedia.org> Change-Id: I570faddca7216a65db2e8e70b12997bf7b5a2933
253 lines
6.9 KiB
PHP
253 lines
6.9 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Settings\Source;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
/**
|
|
* Trait for dealing with JSON Schema structures and types.
|
|
*
|
|
* @since 1.39
|
|
*/
|
|
trait JsonSchemaTrait {
|
|
|
|
/**
|
|
* Converts a JSON Schema type to a PHPDoc type.
|
|
*
|
|
* @param string|string[] $jsonSchemaType A JSON Schema type
|
|
*
|
|
* @return string A PHPDoc type
|
|
*/
|
|
private static function jsonToPhpDoc( $jsonSchemaType ) {
|
|
static $phpTypes = [
|
|
'array' => 'array',
|
|
'object' => 'array', // could be optional
|
|
'number' => 'float',
|
|
'double' => 'float', // for good measure
|
|
'boolean' => 'bool',
|
|
'integer' => 'int',
|
|
];
|
|
|
|
if ( $jsonSchemaType === null ) {
|
|
throw new InvalidArgumentException( 'The type name cannot be null! Use "null" instead.' );
|
|
}
|
|
|
|
$nullable = false;
|
|
if ( is_array( $jsonSchemaType ) ) {
|
|
$nullIndex = array_search( 'null', $jsonSchemaType );
|
|
if ( $nullIndex !== false ) {
|
|
$nullable = true;
|
|
unset( $jsonSchemaType[$nullIndex] );
|
|
}
|
|
|
|
$jsonSchemaType = array_map( [ self::class, 'jsonToPhpDoc' ], $jsonSchemaType );
|
|
$type = implode( '|', $jsonSchemaType );
|
|
} else {
|
|
$type = $phpTypes[ strtolower( $jsonSchemaType ) ] ?? $jsonSchemaType;
|
|
}
|
|
|
|
if ( $nullable ) {
|
|
$type = "?$type";
|
|
}
|
|
|
|
return $type;
|
|
}
|
|
|
|
/**
|
|
* @param string|string[] $phpDocType The PHPDoc type
|
|
*
|
|
* @return string|string[] A JSON Schema type
|
|
*/
|
|
private static function phpDocToJson( $phpDocType ) {
|
|
static $jsonTypes = [
|
|
'list' => 'array',
|
|
'dict' => 'object',
|
|
'map' => 'object',
|
|
'stdclass' => 'object',
|
|
'int' => 'integer',
|
|
'float' => 'number',
|
|
'bool' => 'boolean',
|
|
'false' => 'boolean',
|
|
];
|
|
|
|
if ( $phpDocType === null ) {
|
|
throw new InvalidArgumentException( 'The type name cannot be null! Use "null" instead.' );
|
|
}
|
|
|
|
if ( is_array( $phpDocType ) ) {
|
|
$types = $phpDocType;
|
|
} else {
|
|
$types = explode( '|', trim( $phpDocType ) );
|
|
}
|
|
|
|
$nullable = false;
|
|
foreach ( $types as $i => $t ) {
|
|
if ( str_starts_with( $t, '?' ) ) {
|
|
$nullable = true;
|
|
$t = substr( $t, 1 );
|
|
}
|
|
|
|
$types[$i] = $jsonTypes[ strtolower( $t ) ] ?? $t;
|
|
}
|
|
|
|
if ( $nullable ) {
|
|
$types[] = 'null';
|
|
}
|
|
|
|
$types = array_unique( $types );
|
|
|
|
if ( count( $types ) === 1 ) {
|
|
return reset( $types );
|
|
}
|
|
|
|
return $types;
|
|
}
|
|
|
|
/**
|
|
* Applies phpDocToJson() to type declarations in a JSON schema.
|
|
*
|
|
* @param array $schema JSON Schema structure with PHPDoc types
|
|
* @param array &$defs List of definitions (JSON schemas) referenced in the schema
|
|
* @param string $source An identifier for the source schema being reflected, used
|
|
* for error descriptions.
|
|
* @param string $propertyName The name of the property the schema belongs to, used for error descriptions.
|
|
* @return array JSON Schema structure using only proper JSON types
|
|
*/
|
|
private static function normalizeJsonSchema(
|
|
array $schema,
|
|
array &$defs,
|
|
string $source,
|
|
string $propertyName,
|
|
bool $inlineReferences = false
|
|
): array {
|
|
$traversedReferences = [];
|
|
return self::doNormalizeJsonSchema(
|
|
$schema, $defs, $source, $propertyName, $inlineReferences, $traversedReferences
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Recursively applies phpDocToJson() to type declarations in a JSON schema.
|
|
*
|
|
* @param array $schema JSON Schema structure with PHPDoc types
|
|
* @param array &$defs List of definitions (JSON schemas) referenced in the schema
|
|
* @param string $source An identifier for the source schema being reflected, used
|
|
* for error descriptions.
|
|
* @param string $propertyName The name of the property the schema belongs to, used for error descriptions.
|
|
* @param bool $inlineReferences Whether references in the schema should be inlined or not.
|
|
* @param array $traversedReferences An accumulator for the resolved references within a schema normalization,
|
|
* used for cycle detection.
|
|
* @return array JSON Schema structure using only proper JSON types
|
|
*/
|
|
private static function doNormalizeJsonSchema(
|
|
array $schema,
|
|
array &$defs,
|
|
string $source,
|
|
string $propertyName,
|
|
bool $inlineReferences,
|
|
array $traversedReferences
|
|
): array {
|
|
if ( isset( $schema['type'] ) ) {
|
|
// Support PHP Doc style types, for convenience.
|
|
$schema['type'] = self::phpDocToJson( $schema['type'] );
|
|
}
|
|
|
|
if ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) {
|
|
$schema['additionalProperties'] =
|
|
self::doNormalizeJsonSchema(
|
|
$schema['additionalProperties'],
|
|
$defs,
|
|
$source,
|
|
$propertyName,
|
|
$inlineReferences,
|
|
$traversedReferences
|
|
);
|
|
}
|
|
|
|
if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
|
|
$schema['items'] = self::doNormalizeJsonSchema(
|
|
$schema['items'],
|
|
$defs,
|
|
$source,
|
|
$propertyName,
|
|
$inlineReferences,
|
|
$traversedReferences
|
|
);
|
|
}
|
|
|
|
if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
|
|
foreach ( $schema['properties'] as $name => $propSchema ) {
|
|
$schema['properties'][$name] = self::doNormalizeJsonSchema(
|
|
$propSchema,
|
|
$defs,
|
|
$source,
|
|
$propertyName,
|
|
$inlineReferences,
|
|
$traversedReferences
|
|
);
|
|
}
|
|
}
|
|
|
|
if ( isset( $schema['$ref'] ) ) {
|
|
$definitionName = JsonSchemaReferenceResolver::getDefinitionName( $schema[ '$ref' ] );
|
|
if ( array_key_exists( $definitionName, $traversedReferences ) ) {
|
|
throw new RefLoopException(
|
|
"Found a loop while resolving reference $definitionName in $propertyName." .
|
|
" Root schema location: $source"
|
|
);
|
|
}
|
|
$def = JsonSchemaReferenceResolver::resolveRef( $schema['$ref'], $source );
|
|
if ( $def ) {
|
|
if ( !isset( $defs[$definitionName] ) ) {
|
|
$traversedReferences[$definitionName] = true;
|
|
$normalizedDefinition = self::doNormalizeJsonSchema(
|
|
$def,
|
|
$defs,
|
|
$source,
|
|
$propertyName,
|
|
$inlineReferences,
|
|
$traversedReferences
|
|
);
|
|
if ( !$inlineReferences ) {
|
|
$defs[$definitionName] = $normalizedDefinition;
|
|
}
|
|
} else {
|
|
$normalizedDefinition = $defs[$definitionName];
|
|
}
|
|
// Normalize reference after resolving it since JsonSchemaReferenceResolver expects
|
|
// the $ref to be an array with: [ "class" => "Some\\Class", "field" => "someField" ]
|
|
if ( $inlineReferences ) {
|
|
$schema = $normalizedDefinition;
|
|
} else {
|
|
$schema['$ref'] = JsonSchemaReferenceResolver::normalizeRef( $schema['$ref'] );
|
|
}
|
|
}
|
|
}
|
|
|
|
return $schema;
|
|
}
|
|
|
|
/**
|
|
* Returns the default value from the given schema structure.
|
|
* If the schema defines properties, the default value of each
|
|
* property is determined recursively, and the collected into a
|
|
* the top level default, which in that case will be a map
|
|
* (that is, a JSON object).
|
|
*
|
|
* @param array $schema
|
|
* @return mixed The default specified by $schema, or null if no default
|
|
* is defined.
|
|
*/
|
|
private static function getDefaultFromJsonSchema( array $schema ) {
|
|
$default = $schema['default'] ?? null;
|
|
|
|
foreach ( $schema['properties'] ?? [] as $name => $sch ) {
|
|
$def = self::getDefaultFromJsonSchema( $sch );
|
|
|
|
$default[$name] = $def;
|
|
}
|
|
|
|
return $default;
|
|
}
|
|
|
|
}
|