Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.13% covered (success)
99.13%
114 / 115
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
JsonSchemaTrait
99.13% covered (success)
99.13%
114 / 115
80.00% covered (warning)
80.00%
4 / 5
30
0.00% covered (danger)
0.00%
0 / 1
 jsonToPhpDoc
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
5
 phpDocToJson
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
7
 normalizeJsonSchema
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 doNormalizeJsonSchema
98.25% covered (success)
98.25%
56 / 57
0.00% covered (danger)
0.00%
0 / 1
15
 getDefaultFromJsonSchema
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace MediaWiki\Settings\Source;
4
5use InvalidArgumentException;
6
7/**
8 * Trait for dealing with JSON Schema structures and types.
9 *
10 * @since 1.39
11 */
12trait JsonSchemaTrait {
13
14    /**
15     * Converts a JSON Schema type to a PHPDoc type.
16     *
17     * @param string|string[] $jsonSchemaType A JSON Schema type
18     *
19     * @return string A PHPDoc type
20     */
21    private static function jsonToPhpDoc( $jsonSchemaType ) {
22        static $phpTypes = [
23            'array' => 'array',
24            'object' => 'array', // could be optional
25            'number' => 'float',
26            'double' => 'float', // for good measure
27            'boolean' => 'bool',
28            'integer' => 'int',
29        ];
30
31        if ( $jsonSchemaType === null ) {
32            throw new InvalidArgumentException( 'The type name cannot be null! Use "null" instead.' );
33        }
34
35        $nullable = false;
36        if ( is_array( $jsonSchemaType ) ) {
37            $nullIndex = array_search( 'null', $jsonSchemaType );
38            if ( $nullIndex !== false ) {
39                $nullable = true;
40                unset( $jsonSchemaType[$nullIndex] );
41            }
42
43            $jsonSchemaType = array_map( [ self::class, 'jsonToPhpDoc' ], $jsonSchemaType );
44            $type = implode( '|', $jsonSchemaType );
45        } else {
46            $type = $phpTypes[ strtolower( $jsonSchemaType ) ] ?? $jsonSchemaType;
47        }
48
49        if ( $nullable ) {
50            $type = "?$type";
51        }
52
53        return $type;
54    }
55
56    /**
57     * @param string|string[] $phpDocType The PHPDoc type
58     *
59     * @return string|string[] A JSON Schema type
60     */
61    private static function phpDocToJson( $phpDocType ) {
62        static $jsonTypes = [
63            'list' => 'array',
64            'dict' => 'object',
65            'map' => 'object',
66            'stdclass' => 'object',
67            'int' => 'integer',
68            'float' => 'number',
69            'bool' => 'boolean',
70            'false' => 'boolean',
71        ];
72
73        if ( $phpDocType === null ) {
74            throw new InvalidArgumentException( 'The type name cannot be null! Use "null" instead.' );
75        }
76
77        if ( is_array( $phpDocType ) ) {
78            $types = $phpDocType;
79        } else {
80            $types = explode( '|', trim( $phpDocType ) );
81        }
82
83        $nullable = false;
84        foreach ( $types as $i => $t ) {
85            if ( str_starts_with( $t, '?' ) ) {
86                $nullable = true;
87                $t = substr( $t, 1 );
88            }
89
90            $types[$i] = $jsonTypes[ strtolower( $t ) ] ?? $t;
91        }
92
93        if ( $nullable ) {
94            $types[] = 'null';
95        }
96
97        $types = array_unique( $types );
98
99        if ( count( $types ) === 1 ) {
100            return reset( $types );
101        }
102
103        return $types;
104    }
105
106    /**
107     * Applies phpDocToJson() to type declarations in a JSON schema.
108     *
109     * @param array $schema JSON Schema structure with PHPDoc types
110     * @param array &$defs List of definitions (JSON schemas) referenced in the schema
111     * @param string $source An identifier for the source schema being reflected, used
112     * for error descriptions.
113     * @param string $propertyName The name of the property the schema belongs to, used for error descriptions.
114     * @return array JSON Schema structure using only proper JSON types
115     */
116    private static function normalizeJsonSchema(
117        array $schema,
118        array &$defs,
119        string $source,
120        string $propertyName,
121        bool $inlineReferences = false
122    ): array {
123        $traversedReferences = [];
124        return self::doNormalizeJsonSchema(
125            $schema, $defs, $source, $propertyName, $inlineReferences, $traversedReferences
126        );
127    }
128
129    /**
130     * Recursively applies phpDocToJson() to type declarations in a JSON schema.
131     *
132     * @param array $schema JSON Schema structure with PHPDoc types
133     * @param array &$defs List of definitions (JSON schemas) referenced in the schema
134     * @param string $source An identifier for the source schema being reflected, used
135     * for error descriptions.
136     * @param string $propertyName The name of the property the schema belongs to, used for error descriptions.
137     * @param bool $inlineReferences Whether references in the schema should be inlined or not.
138     * @param array $traversedReferences An accumulator for the resolved references within a schema normalization,
139     * used for cycle detection.
140     * @return array JSON Schema structure using only proper JSON types
141     */
142    private static function doNormalizeJsonSchema(
143        array $schema,
144        array &$defs,
145        string $source,
146        string $propertyName,
147        bool $inlineReferences,
148        array $traversedReferences
149    ): array {
150        if ( isset( $schema['type'] ) ) {
151            // Support PHP Doc style types, for convenience.
152            $schema['type'] = self::phpDocToJson( $schema['type'] );
153        }
154
155        if ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) {
156            $schema['additionalProperties'] =
157                self::doNormalizeJsonSchema(
158                    $schema['additionalProperties'],
159                    $defs,
160                    $source,
161                    $propertyName,
162                    $inlineReferences,
163                    $traversedReferences
164                );
165        }
166
167        if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
168            $schema['items'] = self::doNormalizeJsonSchema(
169                $schema['items'],
170                $defs,
171                $source,
172                $propertyName,
173                $inlineReferences,
174                $traversedReferences
175            );
176        }
177
178        if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
179            foreach ( $schema['properties'] as $name => $propSchema ) {
180                $schema['properties'][$name] = self::doNormalizeJsonSchema(
181                    $propSchema,
182                    $defs,
183                    $source,
184                    $propertyName,
185                    $inlineReferences,
186                    $traversedReferences
187                );
188            }
189        }
190
191        if ( isset( $schema['$ref'] ) ) {
192            $definitionName = JsonSchemaReferenceResolver::getDefinitionName( $schema[ '$ref' ] );
193            if ( array_key_exists( $definitionName, $traversedReferences ) ) {
194                throw new RefLoopException(
195                    "Found a loop while resolving reference $definitionName in $propertyName." .
196                    " Root schema location: $source"
197                );
198            }
199            $def = JsonSchemaReferenceResolver::resolveRef( $schema['$ref'], $source );
200            if ( $def ) {
201                if ( !isset( $defs[$definitionName] ) ) {
202                    $traversedReferences[$definitionName] = true;
203                    $normalizedDefinition = self::doNormalizeJsonSchema(
204                        $def,
205                        $defs,
206                        $source,
207                        $propertyName,
208                        $inlineReferences,
209                        $traversedReferences
210                    );
211                    if ( !$inlineReferences ) {
212                        $defs[$definitionName] = $normalizedDefinition;
213                    }
214                } else {
215                    $normalizedDefinition = $defs[$definitionName];
216                }
217                // Normalize reference after resolving it since JsonSchemaReferenceResolver expects
218                // the $ref to be an array with: [ "class" => "Some\\Class", "field" => "someField" ]
219                if ( $inlineReferences ) {
220                    $schema = $normalizedDefinition;
221                } else {
222                    $schema['$ref'] = JsonSchemaReferenceResolver::normalizeRef( $schema['$ref'] );
223                }
224            }
225        }
226
227        return $schema;
228    }
229
230    /**
231     * Returns the default value from the given schema structure.
232     * If the schema defines properties, the default value of each
233     * property is determined recursively, and the collected into a
234     * the top level default, which in that case will be a map
235     * (that is, a JSON object).
236     *
237     * @param array $schema
238     * @return mixed The default specified by $schema, or null if no default
239     *         is defined.
240     */
241    private static function getDefaultFromJsonSchema( array $schema ) {
242        $default = $schema['default'] ?? null;
243
244        foreach ( $schema['properties'] ?? [] as $name => $sch ) {
245            $def = self::getDefaultFromJsonSchema( $sch );
246
247            $default[$name] = $def;
248        }
249
250        return $default;
251    }
252
253}