Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.13% |
114 / 115 |
|
80.00% |
4 / 5 |
CRAP | |
0.00% |
0 / 1 |
JsonSchemaTrait | |
99.13% |
114 / 115 |
|
80.00% |
4 / 5 |
30 | |
0.00% |
0 / 1 |
jsonToPhpDoc | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
5 | |||
phpDocToJson | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
7 | |||
normalizeJsonSchema | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
doNormalizeJsonSchema | |
98.25% |
56 / 57 |
|
0.00% |
0 / 1 |
15 | |||
getDefaultFromJsonSchema | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Settings\Source; |
4 | |
5 | use InvalidArgumentException; |
6 | |
7 | /** |
8 | * Trait for dealing with JSON Schema structures and types. |
9 | * |
10 | * @since 1.39 |
11 | */ |
12 | trait 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 | } |