@kirigami/php-prepros 1.9.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +502 -21
- package/README.md +1988 -1817
- package/index.d.ts +62 -26
- package/index.js +1 -1
- package/package.json +59 -59
- package/src/imagebatch.php +68 -67
- package/src/libraries/aliases.inc.php +879 -879
- package/src/libraries/curl.class.php +7 -7
- package/src/libraries/ld.class.php +803 -804
- package/src/libraries/md-legacy.class.php +1257 -0
- package/src/libraries/md.class.php +51 -1249
- package/src/libraries/meta.class.php +496 -506
- package/src/libraries/{normalizer.class.php → normalizer-legacy.class.php} +11 -1
- package/src/libraries/prepros.class.php +85 -8
- package/src/libraries/schema-legacy.class.php +549 -0
- package/src/libraries/schema.class.php +99 -472
- package/src/libraries/yaml-legacy.class.php +598 -0
- package/src/libraries/yaml.class.php +41 -406
- package/src/prepros.js +152 -48
- package/src/prepros.php +4 -5
- package/src/runenv.php +2 -2
- package/src/utils.inc.php +12 -2
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SCHEMA_LEGACY — hand-written, pure-PHP JSON Schema validator (Draft 7 style,
|
|
7
|
+
* Ajv-like API), with no external dependencies.
|
|
8
|
+
*
|
|
9
|
+
* Retired in favor of SCHEMA:: (native `jsonk` extension — see docs/STATUS.md
|
|
10
|
+
* in the repo root). Kept here as a reference implementation and an easy
|
|
11
|
+
* rollback path; autoloadable, but not used anywhere by default.
|
|
12
|
+
*
|
|
13
|
+
* Usage example:
|
|
14
|
+
*
|
|
15
|
+
* $validator = new SCHEMA_LEGACY([
|
|
16
|
+
* 'type' => 'object',
|
|
17
|
+
* 'required' => ['name', 'age'],
|
|
18
|
+
* 'properties' => [
|
|
19
|
+
* 'name' => ['type' => 'string', 'minLength' => 1],
|
|
20
|
+
* 'age' => ['type' => 'integer', 'minimum' => 0],
|
|
21
|
+
* ],
|
|
22
|
+
* 'additionalProperties' => false,
|
|
23
|
+
* ]);
|
|
24
|
+
*
|
|
25
|
+
* if (!$validator->isValid(['name' => 'Alice', 'age' => 30])) {
|
|
26
|
+
* foreach ($validator->getErrors() as $error) {
|
|
27
|
+
* echo $error, PHP_EOL;
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
final class SCHEMA_LEGACY
|
|
32
|
+
{
|
|
33
|
+
/** @var array<string,mixed> Schema used as the entry point of isValid(). */
|
|
34
|
+
private array $rootSchema;
|
|
35
|
+
|
|
36
|
+
/** @var array<string,mixed> Root document used to resolve local $ref pointers. */
|
|
37
|
+
private array $refRoot;
|
|
38
|
+
|
|
39
|
+
/** @var string[] */
|
|
40
|
+
private array $errors = [];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param array<string,mixed> $schema The root schema (JSON Schema format).
|
|
44
|
+
*/
|
|
45
|
+
public function __construct(array $schema)
|
|
46
|
+
{
|
|
47
|
+
$this->rootSchema = $schema;
|
|
48
|
+
$this->refRoot = $schema;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validates data. Returns true/false, like Ajv's `validate()`.
|
|
53
|
+
* Errors are available via getErrors().
|
|
54
|
+
*
|
|
55
|
+
* @param mixed $data
|
|
56
|
+
*/
|
|
57
|
+
public function isValid(mixed $data): bool
|
|
58
|
+
{
|
|
59
|
+
$this->errors = [];
|
|
60
|
+
$this->validateAgainst($data, $this->rootSchema, '');
|
|
61
|
+
|
|
62
|
+
return count($this->errors) === 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Alias closer to Ajv's semantics (validate($data)).
|
|
67
|
+
*/
|
|
68
|
+
public function validate(mixed $data): bool
|
|
69
|
+
{
|
|
70
|
+
return $this->isValid($data);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @return string[] List of error messages (path: message).
|
|
75
|
+
*/
|
|
76
|
+
public function getErrors(): array
|
|
77
|
+
{
|
|
78
|
+
return $this->errors;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private function addError(string $path, string $message): void
|
|
82
|
+
{
|
|
83
|
+
$p = $path === '' ? '(root)' : $path;
|
|
84
|
+
$this->errors[] = "{$p}: {$message}";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param mixed $data
|
|
89
|
+
* @param array<string,mixed> $schema
|
|
90
|
+
*/
|
|
91
|
+
private function validateAgainst(mixed $data, array $schema, string $path): void
|
|
92
|
+
{
|
|
93
|
+
// Local $ref (#/definitions/xxx or #/$defs/xxx)
|
|
94
|
+
if (isset($schema['$ref'])) {
|
|
95
|
+
$resolved = $this->resolveRef((string) $schema['$ref']);
|
|
96
|
+
if ($resolved === null) {
|
|
97
|
+
$this->addError($path, "reference not found: {$schema['$ref']}");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
$this->validateAgainst($data, $resolved, $path);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isset($schema['const'])) {
|
|
105
|
+
if (!$this->looseEquals($data, $schema['const'])) {
|
|
106
|
+
$this->addError($path, 'does not match the expected const value');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (isset($schema['enum']) && is_array($schema['enum'])) {
|
|
111
|
+
$found = false;
|
|
112
|
+
foreach ($schema['enum'] as $candidate) {
|
|
113
|
+
if ($this->looseEquals($data, $candidate)) {
|
|
114
|
+
$found = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!$found) {
|
|
119
|
+
$this->addError($path, 'is not one of the allowed enum values');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (isset($schema['type'])) {
|
|
124
|
+
$this->validateType($data, $schema['type'], $path);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
$this->validateCombinators($data, $schema, $path);
|
|
128
|
+
|
|
129
|
+
$type = $this->detectType($data);
|
|
130
|
+
|
|
131
|
+
switch ($type) {
|
|
132
|
+
case 'string':
|
|
133
|
+
$this->validateString($data, $schema, $path);
|
|
134
|
+
break;
|
|
135
|
+
case 'integer':
|
|
136
|
+
case 'number':
|
|
137
|
+
$this->validateNumber($data, $schema, $path);
|
|
138
|
+
break;
|
|
139
|
+
case 'array':
|
|
140
|
+
$this->validateArray($data, $schema, $path);
|
|
141
|
+
break;
|
|
142
|
+
case 'object':
|
|
143
|
+
$this->validateObject($data, $schema, $path);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param array<string,mixed> $schema
|
|
150
|
+
*/
|
|
151
|
+
private function validateCombinators(mixed $data, array $schema, string $path): void
|
|
152
|
+
{
|
|
153
|
+
if (isset($schema['allOf']) && is_array($schema['allOf'])) {
|
|
154
|
+
foreach ($schema['allOf'] as $sub) {
|
|
155
|
+
$this->validateAgainst($data, $sub, $path);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isset($schema['anyOf']) && is_array($schema['anyOf'])) {
|
|
160
|
+
$ok = false;
|
|
161
|
+
foreach ($schema['anyOf'] as $sub) {
|
|
162
|
+
if ($this->isSubSchemaValid($data, $sub)) {
|
|
163
|
+
$ok = true;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!$ok) {
|
|
168
|
+
$this->addError($path, 'does not satisfy any of the anyOf schemas');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (isset($schema['oneOf']) && is_array($schema['oneOf'])) {
|
|
173
|
+
$matches = 0;
|
|
174
|
+
foreach ($schema['oneOf'] as $sub) {
|
|
175
|
+
if ($this->isSubSchemaValid($data, $sub)) {
|
|
176
|
+
$matches++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if ($matches !== 1) {
|
|
180
|
+
$this->addError($path, "must satisfy exactly one oneOf schema ({$matches} match(es))");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (isset($schema['not'])) {
|
|
185
|
+
if ($this->isSubSchemaValid($data, $schema['not'])) {
|
|
186
|
+
$this->addError($path, 'must not satisfy the "not" schema');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param array<string,mixed> $schema
|
|
193
|
+
*/
|
|
194
|
+
private function isSubSchemaValid(mixed $data, array $schema): bool
|
|
195
|
+
{
|
|
196
|
+
$sub = new self($schema);
|
|
197
|
+
// $ref pointers inside the sub-schema must keep resolving against
|
|
198
|
+
// the original root document, not against the sub-schema itself.
|
|
199
|
+
$sub->refRoot = $this->refRoot;
|
|
200
|
+
return $sub->isValid($data);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private function validateType(mixed $data, mixed $expected, string $path): void
|
|
204
|
+
{
|
|
205
|
+
$expectedTypes = is_array($expected) ? $expected : [$expected];
|
|
206
|
+
$actual = $this->detectType($data);
|
|
207
|
+
|
|
208
|
+
foreach ($expectedTypes as $t) {
|
|
209
|
+
if ($t === 'integer' && $actual === 'number' && is_float($data) && floor($data) === $data) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if ($t === $actual) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if ($t === 'number' && $actual === 'integer') {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
$expectedStr = implode('|', $expectedTypes);
|
|
221
|
+
$this->addError($path, "invalid type: expected {$expectedStr}, got {$actual}");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private function detectType(mixed $data): string
|
|
225
|
+
{
|
|
226
|
+
return match (true) {
|
|
227
|
+
$data === null => 'null',
|
|
228
|
+
is_bool($data) => 'boolean',
|
|
229
|
+
is_int($data) => 'integer',
|
|
230
|
+
is_float($data) => (floor($data) === $data ? 'integer' : 'number'),
|
|
231
|
+
is_string($data) => 'string',
|
|
232
|
+
// A JSON array always decodes to a PHP list array, whether or not
|
|
233
|
+
// json_decode() was called with associative=true.
|
|
234
|
+
is_array($data) && $this->isList($data) => 'array',
|
|
235
|
+
// A JSON object decodes to either an associative array
|
|
236
|
+
// (json_decode(..., true)) or a stdClass (json_decode(..., false)).
|
|
237
|
+
is_array($data) || is_object($data) => 'object',
|
|
238
|
+
default => 'unknown',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* True for plain PHP list arrays (sequential integer keys from 0).
|
|
244
|
+
* Objects (stdClass or associative arrays) are never lists.
|
|
245
|
+
*/
|
|
246
|
+
private function isList(mixed $arr): bool
|
|
247
|
+
{
|
|
248
|
+
if (!is_array($arr)) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
if ($arr === []) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
return array_keys($arr) === range(0, count($arr) - 1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* True when $data behaves like a JSON object: either a stdClass
|
|
259
|
+
* (json_decode(..., false)) or a non-list associative array
|
|
260
|
+
* (json_decode(..., true)).
|
|
261
|
+
*/
|
|
262
|
+
private function isObjectLike(mixed $data): bool
|
|
263
|
+
{
|
|
264
|
+
return is_object($data) || (is_array($data) && !$this->isList($data));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @return string[] Property names of an object-like value, in order.
|
|
269
|
+
*/
|
|
270
|
+
private function objectKeys(mixed $data): array
|
|
271
|
+
{
|
|
272
|
+
if (is_object($data)) {
|
|
273
|
+
return array_keys(get_object_vars($data));
|
|
274
|
+
}
|
|
275
|
+
if (is_array($data)) {
|
|
276
|
+
return array_map('strval', array_keys($data));
|
|
277
|
+
}
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private function objectHas(mixed $data, string $key): bool
|
|
282
|
+
{
|
|
283
|
+
if (is_object($data)) {
|
|
284
|
+
return property_exists($data, $key);
|
|
285
|
+
}
|
|
286
|
+
if (is_array($data)) {
|
|
287
|
+
return array_key_exists($key, $data);
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private function objectGet(mixed $data, string $key): mixed
|
|
293
|
+
{
|
|
294
|
+
if (is_object($data)) {
|
|
295
|
+
return $data->{$key};
|
|
296
|
+
}
|
|
297
|
+
return $data[$key];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* @param array<string,mixed> $schema
|
|
302
|
+
*/
|
|
303
|
+
private function validateString(mixed $data, array $schema, string $path): void
|
|
304
|
+
{
|
|
305
|
+
if (!is_string($data)) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
$len = function_exists('mb_strlen') ? mb_strlen($data) : strlen($data);
|
|
310
|
+
|
|
311
|
+
if (isset($schema['minLength']) && $len < (int) $schema['minLength']) {
|
|
312
|
+
$this->addError($path, "minimum length {$schema['minLength']} not met ({$len})");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (isset($schema['maxLength']) && $len > (int) $schema['maxLength']) {
|
|
316
|
+
$this->addError($path, "maximum length {$schema['maxLength']} exceeded ({$len})");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (isset($schema['pattern'])) {
|
|
320
|
+
$pattern = '/' . str_replace('/', '\/', (string) $schema['pattern']) . '/u';
|
|
321
|
+
if (@preg_match($pattern, $data) !== 1) {
|
|
322
|
+
$this->addError($path, "does not match pattern {$schema['pattern']}");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (isset($schema['format'])) {
|
|
327
|
+
$this->validateFormat($data, (string) $schema['format'], $path);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private function validateFormat(string $data, string $format, string $path): void
|
|
332
|
+
{
|
|
333
|
+
$ok = match ($format) {
|
|
334
|
+
'email' => (bool) filter_var($data, FILTER_VALIDATE_EMAIL),
|
|
335
|
+
'uri', 'url' => (bool) filter_var($data, FILTER_VALIDATE_URL),
|
|
336
|
+
'date' => (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $data)
|
|
337
|
+
&& checkdate((int) substr($data, 5, 2), (int) substr($data, 8, 2), (int) substr($data, 0, 4)),
|
|
338
|
+
'date-time' => (bool) preg_match(
|
|
339
|
+
'/^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/',
|
|
340
|
+
$data
|
|
341
|
+
),
|
|
342
|
+
'ipv4' => (bool) filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4),
|
|
343
|
+
'ipv6' => (bool) filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6),
|
|
344
|
+
'uuid' => (bool) preg_match(
|
|
345
|
+
'/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i',
|
|
346
|
+
$data
|
|
347
|
+
),
|
|
348
|
+
default => true, // Unknown format: does not block, same as Ajv in non-strict mode.
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
if (!$ok) {
|
|
352
|
+
$this->addError($path, "invalid format, expected '{$format}'");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* @param array<string,mixed> $schema
|
|
358
|
+
*/
|
|
359
|
+
private function validateNumber(mixed $data, array $schema, string $path): void
|
|
360
|
+
{
|
|
361
|
+
if (!is_int($data) && !is_float($data)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (isset($schema['minimum']) && $data < $schema['minimum']) {
|
|
366
|
+
$this->addError($path, "must be >= {$schema['minimum']}");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (isset($schema['maximum']) && $data > $schema['maximum']) {
|
|
370
|
+
$this->addError($path, "must be <= {$schema['maximum']}");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (isset($schema['exclusiveMinimum']) && $data <= $schema['exclusiveMinimum']) {
|
|
374
|
+
$this->addError($path, "must be > {$schema['exclusiveMinimum']}");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (isset($schema['exclusiveMaximum']) && $data >= $schema['exclusiveMaximum']) {
|
|
378
|
+
$this->addError($path, "must be < {$schema['exclusiveMaximum']}");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (isset($schema['multipleOf'])) {
|
|
382
|
+
$factor = (float) $schema['multipleOf'];
|
|
383
|
+
if ($factor > 0) {
|
|
384
|
+
$div = $data / $factor;
|
|
385
|
+
if (abs($div - round($div)) > 1e-9) {
|
|
386
|
+
$this->addError($path, "must be a multiple of {$schema['multipleOf']}");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* @param array<string,mixed> $schema
|
|
394
|
+
*/
|
|
395
|
+
private function validateArray(mixed $data, array $schema, string $path): void
|
|
396
|
+
{
|
|
397
|
+
if (!is_array($data) || !$this->isList($data)) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
$count = count($data);
|
|
402
|
+
|
|
403
|
+
if (isset($schema['minItems']) && $count < (int) $schema['minItems']) {
|
|
404
|
+
$this->addError($path, "must contain at least {$schema['minItems']} item(s)");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (isset($schema['maxItems']) && $count > (int) $schema['maxItems']) {
|
|
408
|
+
$this->addError($path, "must contain at most {$schema['maxItems']} item(s)");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (($schema['uniqueItems'] ?? false) === true) {
|
|
412
|
+
$seen = [];
|
|
413
|
+
foreach ($data as $item) {
|
|
414
|
+
$key = json_encode($item, JSON_THROW_ON_ERROR);
|
|
415
|
+
if (isset($seen[$key])) {
|
|
416
|
+
$this->addError($path, 'items must be unique');
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
$seen[$key] = true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (isset($schema['items'])) {
|
|
424
|
+
$items = $schema['items'];
|
|
425
|
+
|
|
426
|
+
if ($this->isList($items) && array_is_list($items)) {
|
|
427
|
+
// Tuple mode: items is a list of positional schemas.
|
|
428
|
+
foreach ($data as $i => $item) {
|
|
429
|
+
if (isset($items[$i])) {
|
|
430
|
+
$this->validateAgainst($item, $items[$i], "{$path}[{$i}]");
|
|
431
|
+
} elseif (isset($schema['additionalItems']) && $schema['additionalItems'] === false) {
|
|
432
|
+
$this->addError("{$path}[{$i}]", 'additional item not allowed');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
// Standard mode: a single schema applied to every item.
|
|
437
|
+
foreach ($data as $i => $item) {
|
|
438
|
+
$this->validateAgainst($item, $items, "{$path}[{$i}]");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* @param array<string,mixed> $schema
|
|
446
|
+
*/
|
|
447
|
+
private function validateObject(mixed $data, array $schema, string $path): void
|
|
448
|
+
{
|
|
449
|
+
// Accepts a JSON object regardless of how it was decoded:
|
|
450
|
+
// associative array (json_decode(..., true)) or stdClass (json_decode(..., false)).
|
|
451
|
+
if (!$this->isObjectLike($data)) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
$keys = $this->objectKeys($data);
|
|
456
|
+
|
|
457
|
+
// required
|
|
458
|
+
if (isset($schema['required']) && is_array($schema['required'])) {
|
|
459
|
+
foreach ($schema['required'] as $requiredKey) {
|
|
460
|
+
if (!$this->objectHas($data, (string) $requiredKey)) {
|
|
461
|
+
$this->addError($path, "missing required property: '{$requiredKey}'");
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
$properties = $schema['properties'] ?? [];
|
|
467
|
+
$patternProperties = $schema['patternProperties'] ?? [];
|
|
468
|
+
$additional = $schema['additionalProperties'] ?? true;
|
|
469
|
+
|
|
470
|
+
foreach ($keys as $key) {
|
|
471
|
+
$value = $this->objectGet($data, $key);
|
|
472
|
+
$childPath = $path === '' ? $key : "{$path}.{$key}";
|
|
473
|
+
$matched = false;
|
|
474
|
+
|
|
475
|
+
if (isset($properties[$key])) {
|
|
476
|
+
$matched = true;
|
|
477
|
+
$this->validateAgainst($value, $properties[$key], $childPath);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
foreach ($patternProperties as $pattern => $subSchema) {
|
|
481
|
+
$regex = '/' . str_replace('/', '\/', (string) $pattern) . '/u';
|
|
482
|
+
if (@preg_match($regex, $key) === 1) {
|
|
483
|
+
$matched = true;
|
|
484
|
+
$this->validateAgainst($value, $subSchema, $childPath);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!$matched) {
|
|
489
|
+
if ($additional === false) {
|
|
490
|
+
$this->addError($childPath, 'additional property not allowed');
|
|
491
|
+
} elseif (is_array($additional)) {
|
|
492
|
+
$this->validateAgainst($value, $additional, $childPath);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
$count = count($keys);
|
|
498
|
+
|
|
499
|
+
if (isset($schema['minProperties']) && $count < (int) $schema['minProperties']) {
|
|
500
|
+
$this->addError($path, "must have at least {$schema['minProperties']} propert(y/ies)");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (isset($schema['maxProperties']) && $count > (int) $schema['maxProperties']) {
|
|
504
|
+
$this->addError($path, "must have at most {$schema['maxProperties']} propert(y/ies)");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Resolves a local reference such as "#/definitions/foo" or "#/$defs/foo".
|
|
510
|
+
*
|
|
511
|
+
* @return array<string,mixed>|null
|
|
512
|
+
*/
|
|
513
|
+
private function resolveRef(string $ref): ?array
|
|
514
|
+
{
|
|
515
|
+
if (!str_starts_with($ref, '#/')) {
|
|
516
|
+
return null; // External references are not supported.
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
$segments = explode('/', substr($ref, 2));
|
|
520
|
+
$current = $this->refRoot;
|
|
521
|
+
|
|
522
|
+
foreach ($segments as $segment) {
|
|
523
|
+
$segment = str_replace(['~1', '~0'], ['/', '~'], $segment);
|
|
524
|
+
if (!is_array($current) || !array_key_exists($segment, $current)) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
$current = $current[$segment];
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return is_array($current) ? $current : null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private function looseEquals(mixed $a, mixed $b): bool
|
|
534
|
+
{
|
|
535
|
+
$aStructured = is_array($a) || is_object($a);
|
|
536
|
+
$bStructured = is_array($b) || is_object($b);
|
|
537
|
+
|
|
538
|
+
if ($aStructured && $bStructured) {
|
|
539
|
+
try {
|
|
540
|
+
// Compares structural (JSON) equality, so an associative array
|
|
541
|
+
// and a stdClass with the same content are considered equal.
|
|
542
|
+
return json_encode($a, JSON_THROW_ON_ERROR) === json_encode($b, JSON_THROW_ON_ERROR);
|
|
543
|
+
} catch (\JsonException) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return $a === $b;
|
|
548
|
+
}
|
|
549
|
+
}
|