json-schema-compatibility-checker 1.0.7 → 1.0.8
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/README.md +39 -1836
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,41 +11,9 @@
|
|
|
11
11
|
- [Installation](#installation)
|
|
12
12
|
- [Démarrage rapide](#démarrage-rapide)
|
|
13
13
|
- [API Reference](#api-reference)
|
|
14
|
-
|
|
15
|
-
- [`check(sub, sup)`](#checksub-sup)
|
|
16
|
-
- [`isEqual(a, b)`](#isequala-b)
|
|
17
|
-
- [`intersect(a, b)`](#intersecta-b)
|
|
18
|
-
- [`resolveConditions(schema, data)`](#resolveconditionsschema-data)
|
|
19
|
-
- [`check(sub, sup, options)`](#checksub-sup-options)
|
|
20
|
-
- [`normalize(schema)`](#normalizeschema)
|
|
21
|
-
- [`formatResult(label, result)`](#formatresultlabel-result)
|
|
22
|
-
- [Guide des fonctionnalités](#guide-des-fonctionnalités)
|
|
23
|
-
- [1. Compatibilité de types](#1-compatibilité-de-types)
|
|
24
|
-
- [2. Champs requis (`required`)](#2-champs-requis-required)
|
|
25
|
-
- [3. Contraintes numériques](#3-contraintes-numériques)
|
|
26
|
-
- [4. Contraintes de chaînes](#4-contraintes-de-chaînes)
|
|
27
|
-
- [5. `enum` et `const`](#5-enum-et-const)
|
|
28
|
-
- [6. Contraintes de tableaux](#6-contraintes-de-tableaux)
|
|
29
|
-
- [7. `additionalProperties`](#7-additionalproperties)
|
|
30
|
-
- [8. Objets imbriqués](#8-objets-imbriqués)
|
|
31
|
-
- [9. `anyOf` / `oneOf`](#9-anyof--oneof)
|
|
32
|
-
- [10. Négation (`not`)](#10-négation-not)
|
|
33
|
-
- [11. Formats (`format`)](#11-formats-format)
|
|
34
|
-
- [12. Patterns regex (`pattern`)](#12-patterns-regex-pattern)
|
|
35
|
-
- [13. Conditions `if` / `then` / `else`](#13-conditions-if--then--else)
|
|
36
|
-
- [14. `allOf` avec conditions](#14-allof-avec-conditions)
|
|
37
|
-
- [Fonctions utilitaires](#fonctions-utilitaires)
|
|
38
|
-
- [`isPatternSubset(sub, sup)`](#ispatternsubsetsub-sup)
|
|
39
|
-
- [`arePatternsEquivalent(a, b)`](#arepatternsEquivalenta-b)
|
|
40
|
-
- [`isTrivialPattern(pattern)`](#istrivialpatternpattern)
|
|
41
|
-
- [Cas d'usage concrets](#cas-dusage-concrets)
|
|
42
|
-
- [Connexion de nœuds dans un orchestrateur](#connexion-de-nœuds-dans-un-orchestrateur)
|
|
43
|
-
- [Validation de réponse API](#validation-de-réponse-api)
|
|
44
|
-
- [Union discriminée](#union-discriminée)
|
|
45
|
-
- [Formulaire conditionnel](#formulaire-conditionnel)
|
|
46
|
-
- [Types exportés](#types-exportés)
|
|
14
|
+
- [Documentation complète](#-documentation-complète)
|
|
47
15
|
- [Limitations connues](#limitations-connues)
|
|
48
|
-
- [
|
|
16
|
+
- [Licence](#licence)
|
|
49
17
|
|
|
50
18
|
---
|
|
51
19
|
|
|
@@ -151,1839 +119,74 @@ console.log(checker.isSubset(loose, strict)); // false ❌
|
|
|
151
119
|
Toutes les méthodes sont exposées par la classe `JsonSchemaCompatibilityChecker`.
|
|
152
120
|
|
|
153
121
|
```ts
|
|
154
|
-
import { JsonSchemaCompatibilityChecker } from "json-schema-compatibility-checker";
|
|
155
|
-
|
|
156
122
|
const checker = new JsonSchemaCompatibilityChecker();
|
|
157
123
|
```
|
|
158
124
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
Retourne un simple `boolean`.
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
// integer est un sous-ensemble de number
|
|
173
|
-
checker.isSubset({ type: "integer" }, { type: "number" });
|
|
174
|
-
// → true
|
|
175
|
-
|
|
176
|
-
// number n'est PAS un sous-ensemble de integer
|
|
177
|
-
checker.isSubset({ type: "number" }, { type: "integer" });
|
|
178
|
-
// → false
|
|
179
|
-
|
|
180
|
-
// string n'est PAS un sous-ensemble de number
|
|
181
|
-
checker.isSubset({ type: "string" }, { type: "number" });
|
|
182
|
-
// → false
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
#### Schemas booléens
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
// false (aucune valeur) est sous-ensemble de tout
|
|
189
|
-
checker.isSubset(false, true); // → true
|
|
190
|
-
checker.isSubset(false, { type: "string" }); // → true
|
|
191
|
-
|
|
192
|
-
// true (toutes les valeurs) n'est sous-ensemble de rien de spécifique
|
|
193
|
-
checker.isSubset(true, { type: "string" }); // → false
|
|
194
|
-
|
|
195
|
-
// Tout schema est sous-ensemble de true
|
|
196
|
-
checker.isSubset({ type: "string" }, true); // → true
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
### `check(sub, sup)`
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
check(sub: JSONSchema7Definition, sup: JSONSchema7Definition): SubsetResult
|
|
205
|
-
check(sub: JSONSchema7Definition, sup: JSONSchema7Definition, options: CheckConditionsOptions): ResolvedSubsetResult
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Comme `isSubset`, mais retourne un **résultat détaillé** avec les erreurs sémantiques.
|
|
209
|
-
|
|
210
|
-
Quand `options` est fourni, les conditions `if/then/else` sont résolues avant le check (voir [`check(sub, sup, options)`](#checksub-sup-options) plus bas).
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
interface SchemaError {
|
|
214
|
-
key: string; // Chemin normalisé (ex: "user.name", "users[].email")
|
|
215
|
-
expected: string; // Type/valeur attendu(e) par le schema cible (sup)
|
|
216
|
-
received: string; // Type/valeur reçu(e) depuis le schema source (sub)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
interface SubsetResult {
|
|
220
|
-
isSubset: boolean;
|
|
221
|
-
merged: JSONSchema7Definition | null; // Résultat de l'intersection
|
|
222
|
-
errors: SchemaError[]; // Erreurs sémantiques
|
|
223
|
-
}
|
|
224
|
-
```
|
|
125
|
+
| Méthode | Description | Retour |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `isSubset(sub, sup)` | Vérifie si `sub ⊆ sup` | `boolean` |
|
|
128
|
+
| `check(sub, sup)` | Vérifie avec diagnostic détaillé | `SubsetResult` |
|
|
129
|
+
| `check(sub, sup, options)` | Vérifie avec résolution des conditions `if/then/else` | `ResolvedSubsetResult` |
|
|
130
|
+
| `isEqual(a, b)` | Égalité structurelle après normalisation | `boolean` |
|
|
131
|
+
| `intersect(a, b)` | Intersection de deux schemas | `JSONSchema7Definition \| null` |
|
|
132
|
+
| `resolveConditions(schema, data)` | Résout les `if/then/else` avec des données | `ResolvedConditionResult` |
|
|
133
|
+
| `normalize(schema)` | Normalise un schema (infère types, résout double négation) | `JSONSchema7Definition` |
|
|
134
|
+
| `formatResult(label, result)` | Formate un résultat pour le debug | `string` |
|
|
225
135
|
|
|
226
|
-
|
|
136
|
+
**Exemple rapide — `check` avec diagnostic :**
|
|
227
137
|
|
|
228
138
|
```ts
|
|
229
139
|
const result = checker.check(
|
|
230
|
-
{ type: "string",
|
|
231
|
-
{ type: "string" }
|
|
140
|
+
{ type: "object", properties: { name: { type: "string" } }, required: ["name"] },
|
|
141
|
+
{ type: "object", properties: { name: { type: "string" }, age: { type: "number" } }, required: ["name", "age"] }
|
|
232
142
|
);
|
|
233
143
|
|
|
234
|
-
console.log(result.isSubset); // true
|
|
235
|
-
console.log(result.errors); // [] (aucune erreur)
|
|
236
|
-
console.log(result.merged); // { type: "string", minLength: 5 }
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
#### Exemple — Check incompatible avec diagnostic
|
|
240
|
-
|
|
241
|
-
```ts
|
|
242
|
-
const sub = {
|
|
243
|
-
type: "object",
|
|
244
|
-
properties: { name: { type: "string" } },
|
|
245
|
-
required: ["name"],
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const sup = {
|
|
249
|
-
type: "object",
|
|
250
|
-
properties: {
|
|
251
|
-
name: { type: "string" },
|
|
252
|
-
age: { type: "number" },
|
|
253
|
-
},
|
|
254
|
-
required: ["name", "age"],
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const result = checker.check(sub, sup);
|
|
258
|
-
console.log(result.errors);
|
|
259
|
-
// [{ key: "age", expected: "number", received: "undefined" }]
|
|
260
|
-
|
|
261
144
|
console.log(result.isSubset); // false
|
|
262
145
|
console.log(result.errors);
|
|
263
|
-
// [
|
|
264
|
-
// { key: "age", expected: "number", received: "undefined" }
|
|
265
|
-
// ]
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
#### Exemple — Types incompatibles
|
|
269
|
-
|
|
270
|
-
```ts
|
|
271
|
-
const result = checker.check({ type: "string" }, { type: "number" });
|
|
272
|
-
|
|
273
|
-
console.log(result.isSubset); // false
|
|
274
|
-
console.log(result.merged); // null (intersection impossible)
|
|
275
|
-
console.log(result.errors); // [{ key: "$root", expected: "number", received: "string" }]
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
---
|
|
279
|
-
|
|
280
|
-
### `isEqual(a, b)`
|
|
281
|
-
|
|
282
|
-
```ts
|
|
283
|
-
isEqual(a: JSONSchema7Definition, b: JSONSchema7Definition): boolean
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
Vérifie l'**égalité structurelle** entre deux schemas après normalisation.
|
|
287
|
-
|
|
288
|
-
```ts
|
|
289
|
-
checker.isEqual(
|
|
290
|
-
{ type: "string", minLength: 1 },
|
|
291
|
-
{ type: "string", minLength: 1 }
|
|
292
|
-
);
|
|
293
|
-
// → true
|
|
294
|
-
|
|
295
|
-
checker.isEqual(
|
|
296
|
-
{ type: "string" },
|
|
297
|
-
{ type: "number" }
|
|
298
|
-
);
|
|
299
|
-
// → false
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
### `intersect(a, b)`
|
|
305
|
-
|
|
306
|
-
```ts
|
|
307
|
-
intersect(
|
|
308
|
-
a: JSONSchema7Definition,
|
|
309
|
-
b: JSONSchema7Definition
|
|
310
|
-
): JSONSchema7Definition | null
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
Calcule l'**intersection** de deux schemas (merge `allOf`). Retourne `null` si les schemas sont incompatibles.
|
|
314
|
-
|
|
315
|
-
#### Exemple — Intersection de contraintes numériques
|
|
316
|
-
|
|
317
|
-
```ts
|
|
318
|
-
const result = checker.intersect(
|
|
319
|
-
{ type: "number", minimum: 5, maximum: 10 },
|
|
320
|
-
{ type: "number", minimum: 0, maximum: 100 }
|
|
321
|
-
);
|
|
322
|
-
// → { type: "number", minimum: 5, maximum: 10 }
|
|
323
|
-
// L'intersection conserve les contraintes les plus restrictives
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
#### Exemple — Intersection de propriétés d'objets
|
|
327
|
-
|
|
328
|
-
```ts
|
|
329
|
-
const result = checker.intersect(
|
|
330
|
-
{
|
|
331
|
-
type: "object",
|
|
332
|
-
properties: { a: { type: "string" } },
|
|
333
|
-
required: ["a"],
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
type: "object",
|
|
337
|
-
properties: { b: { type: "number" } },
|
|
338
|
-
required: ["b"],
|
|
339
|
-
}
|
|
340
|
-
);
|
|
341
|
-
// → {
|
|
342
|
-
// type: "object",
|
|
343
|
-
// properties: { a: { type: "string" }, b: { type: "number" } },
|
|
344
|
-
// required: ["a", "b"]
|
|
345
|
-
// }
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
#### Exemple — Intersection d'enums
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
const result = checker.intersect(
|
|
352
|
-
{ type: "string", enum: ["a", "b", "c"] },
|
|
353
|
-
{ type: "string", enum: ["b", "c", "d"] }
|
|
354
|
-
);
|
|
355
|
-
// → { type: "string", enum: ["b", "c"] }
|
|
356
|
-
// Seules les valeurs communes sont conservées
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
#### Exemple — Types incompatibles
|
|
360
|
-
|
|
361
|
-
```ts
|
|
362
|
-
checker.intersect({ type: "string" }, { type: "number" });
|
|
363
|
-
// → null (aucune valeur ne peut être à la fois string ET number)
|
|
364
|
-
```
|
|
365
|
-
|
|
366
|
-
---
|
|
367
|
-
|
|
368
|
-
### `resolveConditions(schema, data)`
|
|
369
|
-
|
|
370
|
-
```ts
|
|
371
|
-
resolveConditions(
|
|
372
|
-
schema: JSONSchema7,
|
|
373
|
-
data: Record<string, unknown>
|
|
374
|
-
): ResolvedConditionResult
|
|
146
|
+
// [{ key: "age", expected: "number", received: "undefined" }]
|
|
375
147
|
```
|
|
376
148
|
|
|
377
|
-
|
|
149
|
+
**Exemple rapide — résolution de conditions :**
|
|
378
150
|
|
|
379
151
|
```ts
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
branch: "then" | "else" | null; // Branche appliquée
|
|
383
|
-
discriminant: Record<string, unknown>; // Discriminant utilisé
|
|
384
|
-
}
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
```ts
|
|
388
|
-
const formSchema = {
|
|
389
|
-
type: "object",
|
|
390
|
-
properties: {
|
|
391
|
-
accountType: { type: "string", enum: ["personal", "business"] },
|
|
392
|
-
email: { type: "string", format: "email" },
|
|
393
|
-
companyName: { type: "string" },
|
|
394
|
-
firstName: { type: "string" },
|
|
395
|
-
},
|
|
396
|
-
required: ["accountType", "email"],
|
|
397
|
-
if: {
|
|
398
|
-
properties: { accountType: { const: "business" } },
|
|
399
|
-
required: ["accountType"],
|
|
400
|
-
},
|
|
401
|
-
then: {
|
|
402
|
-
required: ["companyName"],
|
|
403
|
-
},
|
|
404
|
-
else: {
|
|
405
|
-
required: ["firstName"],
|
|
406
|
-
},
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
// Résoudre pour un compte business
|
|
410
|
-
const business = checker.resolveConditions(formSchema, {
|
|
411
|
-
accountType: "business",
|
|
412
|
-
});
|
|
413
|
-
console.log(business.branch); // "then"
|
|
414
|
-
console.log(business.resolved.required);
|
|
415
|
-
// → ["accountType", "email", "companyName"]
|
|
416
|
-
|
|
417
|
-
// Résoudre pour un compte personnel
|
|
418
|
-
const personal = checker.resolveConditions(formSchema, {
|
|
419
|
-
accountType: "personal",
|
|
152
|
+
const result = checker.check(sub, conditionalSup, {
|
|
153
|
+
subData: { kind: "text" },
|
|
420
154
|
});
|
|
421
|
-
console.log(personal.branch); // "else"
|
|
422
|
-
console.log(personal.resolved.required);
|
|
423
|
-
// → ["accountType", "email", "firstName"]
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
---
|
|
427
|
-
|
|
428
|
-
### `check(sub, sup, options)`
|
|
429
|
-
|
|
430
|
-
```ts
|
|
431
|
-
check(
|
|
432
|
-
sub: JSONSchema7Definition,
|
|
433
|
-
sup: JSONSchema7Definition,
|
|
434
|
-
options: CheckConditionsOptions
|
|
435
|
-
): ResolvedSubsetResult
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
Résout les conditions `if/then/else` des deux schemas **puis** vérifie `sub ⊆ sup`. Utile quand le superset contient des `if/then/else` et que vous connaissez les valeurs discriminantes. Effectue aussi un **narrowing** du sub par rapport aux contraintes `enum`/`const` du sup en utilisant les données runtime.
|
|
439
|
-
|
|
440
|
-
```ts
|
|
441
|
-
interface CheckConditionsOptions {
|
|
442
|
-
/** Runtime data for the sub schema — used for condition resolution and enum narrowing */
|
|
443
|
-
subData: unknown;
|
|
444
|
-
/** Runtime data for the sup schema (defaults to subData) — used for condition resolution and enum narrowing */
|
|
445
|
-
supData?: unknown;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
interface ResolvedSubsetResult extends SubsetResult {
|
|
449
|
-
resolvedSub: ResolvedConditionResult;
|
|
450
|
-
resolvedSup: ResolvedConditionResult;
|
|
451
|
-
}
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
```ts
|
|
455
|
-
const conditionalSup = {
|
|
456
|
-
type: "object",
|
|
457
|
-
properties: {
|
|
458
|
-
kind: { type: "string" },
|
|
459
|
-
value: {},
|
|
460
|
-
},
|
|
461
|
-
required: ["kind", "value"],
|
|
462
|
-
if: {
|
|
463
|
-
properties: { kind: { const: "text" } },
|
|
464
|
-
required: ["kind"],
|
|
465
|
-
},
|
|
466
|
-
then: {
|
|
467
|
-
properties: { value: { type: "string" } },
|
|
468
|
-
},
|
|
469
|
-
else: {
|
|
470
|
-
properties: { value: { type: "number" } },
|
|
471
|
-
},
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
const sub = {
|
|
475
|
-
type: "object",
|
|
476
|
-
properties: {
|
|
477
|
-
kind: { const: "text" },
|
|
478
|
-
value: { type: "string", minLength: 1 },
|
|
479
|
-
},
|
|
480
|
-
required: ["kind", "value"],
|
|
481
|
-
};
|
|
482
|
-
|
|
483
|
-
// Sans résolution : false (le if/then/else brut ne matche pas)
|
|
484
|
-
console.log(checker.isSubset(sub, conditionalSup)); // false
|
|
485
|
-
|
|
486
|
-
// Avec résolution via options : true !
|
|
487
|
-
const result = checker.check(sub, conditionalSup, { subData: { kind: "text" } });
|
|
488
155
|
console.log(result.isSubset); // true ✅
|
|
489
156
|
console.log(result.resolvedSup.branch); // "then"
|
|
490
|
-
|
|
491
|
-
// Avec des données différentes pour sub et sup
|
|
492
|
-
const result2 = checker.check(sub, conditionalSup, {
|
|
493
|
-
subData: { kind: "text" },
|
|
494
|
-
supData: { kind: "text" },
|
|
495
|
-
});
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
---
|
|
499
|
-
|
|
500
|
-
### `normalize(schema)`
|
|
501
|
-
|
|
502
|
-
```ts
|
|
503
|
-
normalize(def: JSONSchema7Definition): JSONSchema7Definition
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
Normalise un schema : infère `type` depuis `const`/`enum`, résout la double négation `not(not(X)) → X`, et normalise récursivement tous les sous-schemas.
|
|
507
|
-
|
|
508
|
-
```ts
|
|
509
|
-
// Infère le type depuis const
|
|
510
|
-
checker.normalize({ const: "hello" });
|
|
511
|
-
// → { const: "hello", type: "string" }
|
|
512
|
-
|
|
513
|
-
// Infère le type depuis enum
|
|
514
|
-
checker.normalize({ enum: [1, 2, 3] });
|
|
515
|
-
// → { enum: [1, 2, 3], type: "integer" }
|
|
516
|
-
|
|
517
|
-
// Convertit enum à un seul élément en const
|
|
518
|
-
checker.normalize({ enum: ["only"] });
|
|
519
|
-
// → { const: "only", type: "string" }
|
|
520
|
-
|
|
521
|
-
// Résout la double négation
|
|
522
|
-
checker.normalize({ not: { not: { type: "string" } } });
|
|
523
|
-
// → { type: "string" }
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
---
|
|
527
|
-
|
|
528
|
-
### `formatResult(label, result)`
|
|
529
|
-
|
|
530
|
-
```ts
|
|
531
|
-
formatResult(label: string, result: SubsetResult): string
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
Formate un `SubsetResult` en chaîne lisible pour les logs / le debug.
|
|
535
|
-
|
|
536
|
-
```ts
|
|
537
|
-
const result = checker.check(
|
|
538
|
-
{ type: "number", minimum: 0, maximum: 100 },
|
|
539
|
-
{ type: "number", minimum: 5, maximum: 10 }
|
|
540
|
-
);
|
|
541
|
-
|
|
542
|
-
console.log(checker.formatResult("range check", result));
|
|
543
|
-
// ❌ range check: false
|
|
544
|
-
// Errors:
|
|
545
|
-
// ✗ $root: expected minimum 5, received minimum 0
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
```ts
|
|
549
|
-
const result2 = checker.check(
|
|
550
|
-
{ type: "string", minLength: 5 },
|
|
551
|
-
{ type: "string" }
|
|
552
|
-
);
|
|
553
|
-
|
|
554
|
-
console.log(checker.formatResult("strict ⊆ loose", result2));
|
|
555
|
-
// ✅ strict ⊆ loose: true
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
Format de sortie :
|
|
559
|
-
- `✅` — le check a réussi (`isSubset: true`)
|
|
560
|
-
- `❌` — le check a échoué (`isSubset: false`), suivi de la liste des erreurs
|
|
561
|
-
- `✗ key: expected X, received Y` — détail de chaque erreur sémantique
|
|
562
|
-
|
|
563
|
-
---
|
|
564
|
-
|
|
565
|
-
## Guide des fonctionnalités
|
|
566
|
-
|
|
567
|
-
Cette section présente les fonctionnalités supportées, du plus simple au plus complexe, avec des exemples illustratifs.
|
|
568
|
-
|
|
569
|
-
---
|
|
570
|
-
|
|
571
|
-
### 1. Compatibilité de types
|
|
572
|
-
|
|
573
|
-
La librairie comprend le système de types JSON Schema et ses relations d'inclusion.
|
|
574
|
-
|
|
575
|
-
```ts
|
|
576
|
-
// integer ⊆ number (tout entier est un nombre)
|
|
577
|
-
checker.isSubset({ type: "integer" }, { type: "number" }); // true
|
|
578
|
-
|
|
579
|
-
// number ⊄ integer (1.5 est un nombre mais pas un entier)
|
|
580
|
-
checker.isSubset({ type: "number" }, { type: "integer" }); // false
|
|
581
|
-
|
|
582
|
-
// Types incompatibles
|
|
583
|
-
checker.isSubset({ type: "string" }, { type: "number" }); // false
|
|
584
|
-
checker.isSubset({ type: "boolean" }, { type: "string" }); // false
|
|
585
|
-
|
|
586
|
-
// Identité
|
|
587
|
-
checker.isSubset({ type: "string" }, { type: "string" }); // true
|
|
588
|
-
|
|
589
|
-
// L'intersection integer ∩ number = integer
|
|
590
|
-
checker.intersect({ type: "integer" }, { type: "number" });
|
|
591
|
-
// → { type: "integer" }
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
---
|
|
595
|
-
|
|
596
|
-
### 2. Champs requis (`required`)
|
|
597
|
-
|
|
598
|
-
Un schema qui exige **plus** de champs est un sous-ensemble d'un schema qui en exige **moins**.
|
|
599
|
-
|
|
600
|
-
```ts
|
|
601
|
-
const strict = {
|
|
602
|
-
type: "object",
|
|
603
|
-
properties: {
|
|
604
|
-
name: { type: "string" },
|
|
605
|
-
age: { type: "number" },
|
|
606
|
-
},
|
|
607
|
-
required: ["name", "age"],
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
const loose = {
|
|
611
|
-
type: "object",
|
|
612
|
-
properties: {
|
|
613
|
-
name: { type: "string" },
|
|
614
|
-
},
|
|
615
|
-
required: ["name"],
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
// Plus de champs requis → plus restrictif → sous-ensemble
|
|
619
|
-
checker.isSubset(strict, loose); // true
|
|
620
|
-
|
|
621
|
-
// Moins de champs requis → plus permissif → PAS sous-ensemble
|
|
622
|
-
checker.isSubset(loose, strict); // false
|
|
623
|
-
|
|
624
|
-
// Le diagnostic montre exactement ce qui manque
|
|
625
|
-
const result = checker.check(loose, strict);
|
|
626
|
-
console.log(result.errors);
|
|
627
|
-
// [{ key: "age", expected: "number", received: "undefined" }]
|
|
628
157
|
```
|
|
629
158
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
### 3. Contraintes numériques
|
|
633
|
-
|
|
634
|
-
La librairie gère `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum` et `multipleOf`.
|
|
635
|
-
|
|
636
|
-
```ts
|
|
637
|
-
// Plage stricte ⊆ plage large
|
|
638
|
-
checker.isSubset(
|
|
639
|
-
{ type: "number", minimum: 5, maximum: 10 },
|
|
640
|
-
{ type: "number", minimum: 0, maximum: 100 }
|
|
641
|
-
); // true
|
|
642
|
-
|
|
643
|
-
// Plage large ⊄ plage stricte
|
|
644
|
-
checker.isSubset(
|
|
645
|
-
{ type: "number", minimum: 0, maximum: 100 },
|
|
646
|
-
{ type: "number", minimum: 5, maximum: 10 }
|
|
647
|
-
); // false
|
|
648
|
-
|
|
649
|
-
// exclusiveMinimum
|
|
650
|
-
checker.isSubset(
|
|
651
|
-
{ type: "number", exclusiveMinimum: 5 },
|
|
652
|
-
{ type: "number", exclusiveMinimum: 0 }
|
|
653
|
-
); // true (x > 5 implique x > 0)
|
|
654
|
-
|
|
655
|
-
// multipleOf : 6 est multiple de 3
|
|
656
|
-
checker.isSubset(
|
|
657
|
-
{ type: "number", multipleOf: 6 },
|
|
658
|
-
{ type: "number", multipleOf: 3 }
|
|
659
|
-
); // true
|
|
660
|
-
|
|
661
|
-
// multipleOf : 3 n'est PAS multiple de 6
|
|
662
|
-
checker.isSubset(
|
|
663
|
-
{ type: "number", multipleOf: 3 },
|
|
664
|
-
{ type: "number", multipleOf: 6 }
|
|
665
|
-
); // false
|
|
666
|
-
|
|
667
|
-
// L'intersection conserve les contraintes les plus restrictives
|
|
668
|
-
checker.intersect(
|
|
669
|
-
{ type: "number", minimum: 5, maximum: 10 },
|
|
670
|
-
{ type: "number", minimum: 0, maximum: 100 }
|
|
671
|
-
);
|
|
672
|
-
// → { type: "number", minimum: 5, maximum: 10 }
|
|
673
|
-
```
|
|
159
|
+
👉 Pour la documentation complète de chaque méthode avec tous les exemples, consultez la **[Référence API](./docs/api-reference.md)**.
|
|
674
160
|
|
|
675
161
|
---
|
|
676
162
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
Gestion de `minLength`, `maxLength` et `pattern`.
|
|
680
|
-
|
|
681
|
-
```ts
|
|
682
|
-
const strict = {
|
|
683
|
-
type: "string",
|
|
684
|
-
minLength: 3,
|
|
685
|
-
maxLength: 10,
|
|
686
|
-
pattern: "^[a-z]+$",
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
const loose = {
|
|
690
|
-
type: "string",
|
|
691
|
-
minLength: 1,
|
|
692
|
-
maxLength: 100,
|
|
693
|
-
};
|
|
694
|
-
|
|
695
|
-
// Plus de contraintes → sous-ensemble
|
|
696
|
-
checker.isSubset(strict, loose); // true
|
|
697
|
-
|
|
698
|
-
// Moins de contraintes → PAS sous-ensemble
|
|
699
|
-
checker.isSubset(loose, strict); // false
|
|
700
|
-
```
|
|
163
|
+
## 📖 Documentation complète
|
|
701
164
|
|
|
702
|
-
|
|
165
|
+
| Page | Description |
|
|
166
|
+
|---|---|
|
|
167
|
+
| **[Référence API](./docs/api-reference.md)** | Documentation détaillée de chaque méthode avec exemples |
|
|
168
|
+
| **[Guide des fonctionnalités](./docs/features-guide.md)** | Tour complet des fonctionnalités : types, `required`, contraintes numériques, `enum`/`const`, `anyOf`/`oneOf`, `not`, `format`, `pattern`, conditions `if/then/else`, `allOf`... |
|
|
169
|
+
| **[Fonctions utilitaires](./docs/utilities.md)** | `isPatternSubset`, `arePatternsEquivalent`, `isTrivialPattern` |
|
|
170
|
+
| **[Cas d'usage concrets](./docs/use-cases.md)** | Connexion de nœuds dans un orchestrateur, validation de réponse API, unions discriminées, formulaires conditionnels |
|
|
171
|
+
| **[Types exportés](./docs/types.md)** | `SubsetResult`, `SchemaError`, `ResolvedConditionResult`, `ResolvedSubsetResult`, `CheckConditionsOptions` |
|
|
172
|
+
| **[Limitations connues](./docs/limitations.md)** | Cross-keyword constraints, `oneOf` exclusivité, patterns probabilistes, `$ref` non supporté |
|
|
173
|
+
| **[Architecture interne](./docs/architecture.md)** | Diagramme des modules, flux de vérification, dépendances |
|
|
703
174
|
|
|
704
175
|
---
|
|
705
176
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
#### Enum
|
|
709
|
-
|
|
710
|
-
```ts
|
|
711
|
-
// Petit enum ⊆ grand enum (toutes les valeurs du petit sont dans le grand)
|
|
712
|
-
checker.isSubset(
|
|
713
|
-
{ type: "string", enum: ["a", "b"] },
|
|
714
|
-
{ type: "string", enum: ["a", "b", "c", "d"] }
|
|
715
|
-
); // true
|
|
716
|
-
|
|
717
|
-
// Grand enum ⊄ petit enum
|
|
718
|
-
checker.isSubset(
|
|
719
|
-
{ type: "string", enum: ["a", "b", "c", "d"] },
|
|
720
|
-
{ type: "string", enum: ["a", "b"] }
|
|
721
|
-
); // false
|
|
722
|
-
|
|
723
|
-
// Enum d'une seule valeur ⊆ type
|
|
724
|
-
checker.isSubset(
|
|
725
|
-
{ type: "string", enum: ["hello"] },
|
|
726
|
-
{ type: "string" }
|
|
727
|
-
); // true
|
|
177
|
+
## Limitations connues
|
|
728
178
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
```
|
|
179
|
+
- **Cross-keyword constraints** : `exclusiveMinimum` vs `minimum` peut produire des faux négatifs (limitation structurelle)
|
|
180
|
+
- **`oneOf` exclusivité** : traité comme `anyOf` — l'exclusivité sémantique n'est pas vérifiée
|
|
181
|
+
- **Patterns regex** : approche probabiliste par échantillonnage (200 samples), pas une preuve formelle
|
|
182
|
+
- **`if/then/else`** : nécessite des données discriminantes via `check(sub, sup, { subData })`
|
|
183
|
+
- **`$ref`** : non supporté — les schemas doivent être pré-déréférencés
|
|
184
|
+
- **`patternProperties`** : support partiel
|
|
736
185
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
```ts
|
|
740
|
-
// const string ⊆ type string
|
|
741
|
-
checker.isSubset({ const: "hello" }, { type: "string" }); // true
|
|
742
|
-
|
|
743
|
-
// const number ⊆ type number
|
|
744
|
-
checker.isSubset({ const: 42 }, { type: "number" }); // true
|
|
745
|
-
|
|
746
|
-
// const string ⊄ type number (types incompatibles)
|
|
747
|
-
checker.isSubset({ const: "hello" }, { type: "number" }); // false
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
> **Normalisation** : un `enum` à un seul élément est automatiquement converti en `const` lors de la normalisation. `{ enum: ["x"] }` ≡ `{ const: "x" }`.
|
|
751
|
-
|
|
752
|
-
---
|
|
753
|
-
|
|
754
|
-
### 6. Contraintes de tableaux
|
|
755
|
-
|
|
756
|
-
Gestion de `items`, `minItems`, `maxItems`, `uniqueItems`.
|
|
757
|
-
|
|
758
|
-
```ts
|
|
759
|
-
const strict = {
|
|
760
|
-
type: "array",
|
|
761
|
-
items: { type: "string", minLength: 1 },
|
|
762
|
-
minItems: 1,
|
|
763
|
-
maxItems: 5,
|
|
764
|
-
};
|
|
765
|
-
|
|
766
|
-
const loose = {
|
|
767
|
-
type: "array",
|
|
768
|
-
items: { type: "string" },
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
// Tableau plus contraint ⊆ tableau moins contraint
|
|
772
|
-
checker.isSubset(strict, loose); // true
|
|
773
|
-
|
|
774
|
-
// L'inverse est faux
|
|
775
|
-
checker.isSubset(loose, strict); // false
|
|
776
|
-
|
|
777
|
-
// uniqueItems: true est plus restrictif que sans uniqueItems
|
|
778
|
-
checker.isSubset(
|
|
779
|
-
{ type: "array", items: { type: "number" }, uniqueItems: true },
|
|
780
|
-
{ type: "array", items: { type: "number" } }
|
|
781
|
-
); // true
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
---
|
|
785
|
-
|
|
786
|
-
### 7. `additionalProperties`
|
|
787
|
-
|
|
788
|
-
`additionalProperties: false` ferme un objet : seules les propriétés listées dans `properties` sont autorisées.
|
|
789
|
-
|
|
790
|
-
```ts
|
|
791
|
-
const closed = {
|
|
792
|
-
type: "object",
|
|
793
|
-
properties: { name: { type: "string" } },
|
|
794
|
-
required: ["name"],
|
|
795
|
-
additionalProperties: false,
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
const open = {
|
|
799
|
-
type: "object",
|
|
800
|
-
properties: {
|
|
801
|
-
name: { type: "string" },
|
|
802
|
-
age: { type: "number" },
|
|
803
|
-
},
|
|
804
|
-
required: ["name"],
|
|
805
|
-
};
|
|
806
|
-
|
|
807
|
-
// Fermé ⊆ ouvert (un objet sans propriétés supplémentaires est valide partout)
|
|
808
|
-
checker.isSubset(closed, open); // true
|
|
809
|
-
|
|
810
|
-
// Ouvert ⊄ fermé (un objet avec age serait rejeté par closed)
|
|
811
|
-
checker.isSubset(open, closed); // false
|
|
812
|
-
|
|
813
|
-
// Le diagnostic montre la contrainte
|
|
814
|
-
const result = checker.check(open, closed);
|
|
815
|
-
console.log(result.errors);
|
|
816
|
-
// [{ key: "age", expected: "not allowed (additionalProperties: false)", received: "number" }]
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
---
|
|
820
|
-
|
|
821
|
-
### 8. Objets imbriqués
|
|
822
|
-
|
|
823
|
-
La vérification de sous-ensemble est **récursive** : elle descend dans toutes les propriétés imbriquées.
|
|
824
|
-
|
|
825
|
-
```ts
|
|
826
|
-
const deep = {
|
|
827
|
-
type: "object",
|
|
828
|
-
properties: {
|
|
829
|
-
user: {
|
|
830
|
-
type: "object",
|
|
831
|
-
properties: {
|
|
832
|
-
profile: {
|
|
833
|
-
type: "object",
|
|
834
|
-
properties: {
|
|
835
|
-
name: { type: "string" },
|
|
836
|
-
bio: { type: "string" },
|
|
837
|
-
},
|
|
838
|
-
required: ["name", "bio"],
|
|
839
|
-
},
|
|
840
|
-
},
|
|
841
|
-
required: ["profile"],
|
|
842
|
-
},
|
|
843
|
-
},
|
|
844
|
-
required: ["user"],
|
|
845
|
-
};
|
|
846
|
-
|
|
847
|
-
const shallow = {
|
|
848
|
-
type: "object",
|
|
849
|
-
properties: {
|
|
850
|
-
user: {
|
|
851
|
-
type: "object",
|
|
852
|
-
properties: {
|
|
853
|
-
profile: {
|
|
854
|
-
type: "object",
|
|
855
|
-
properties: { name: { type: "string" } },
|
|
856
|
-
required: ["name"],
|
|
857
|
-
},
|
|
858
|
-
},
|
|
859
|
-
required: ["profile"],
|
|
860
|
-
},
|
|
861
|
-
},
|
|
862
|
-
required: ["user"],
|
|
863
|
-
};
|
|
864
|
-
|
|
865
|
-
// Plus profond et plus exigeant → sous-ensemble du moins exigeant
|
|
866
|
-
checker.isSubset(deep, shallow); // true
|
|
867
|
-
checker.isSubset(shallow, deep); // false
|
|
868
|
-
|
|
869
|
-
// Les erreurs montrent les propriétés manquantes avec un chemin complet
|
|
870
|
-
const result = checker.check(shallow, deep);
|
|
871
|
-
console.log(result.errors);
|
|
872
|
-
// [{ key: "user.profile.bio", expected: "string", received: "undefined" }]
|
|
873
|
-
```
|
|
874
|
-
|
|
875
|
-
---
|
|
876
|
-
|
|
877
|
-
### 9. `anyOf` / `oneOf`
|
|
878
|
-
|
|
879
|
-
La librairie supporte `anyOf` et `oneOf` pour la vérification de sous-ensemble.
|
|
880
|
-
|
|
881
|
-
#### anyOf
|
|
882
|
-
|
|
883
|
-
```ts
|
|
884
|
-
const sub = {
|
|
885
|
-
anyOf: [{ type: "string" }, { type: "number" }],
|
|
886
|
-
};
|
|
887
|
-
const sup = {
|
|
888
|
-
anyOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }],
|
|
889
|
-
};
|
|
890
|
-
|
|
891
|
-
// Chaque branche de sub doit matcher une branche de sup
|
|
892
|
-
checker.isSubset(sub, sup); // true
|
|
893
|
-
checker.isSubset(sup, sub); // false
|
|
894
|
-
|
|
895
|
-
// Atomic ⊆ anyOf (si au moins une branche accepte)
|
|
896
|
-
checker.isSubset(
|
|
897
|
-
{ type: "string", minLength: 1 },
|
|
898
|
-
{ anyOf: [{ type: "string" }, { type: "number" }] }
|
|
899
|
-
); // true
|
|
900
|
-
|
|
901
|
-
// Atomic ⊄ anyOf (si aucune branche n'accepte)
|
|
902
|
-
checker.isSubset(
|
|
903
|
-
{ type: "boolean" },
|
|
904
|
-
{ anyOf: [{ type: "string" }, { type: "number" }] }
|
|
905
|
-
); // false
|
|
906
|
-
```
|
|
907
|
-
|
|
908
|
-
#### oneOf
|
|
909
|
-
|
|
910
|
-
Le `oneOf` est traité comme `anyOf` pour la vérification de sous-ensemble (chaque branche doit être acceptée).
|
|
911
|
-
|
|
912
|
-
```ts
|
|
913
|
-
const result = checker.check(
|
|
914
|
-
{ oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }] },
|
|
915
|
-
{ oneOf: [{ type: "string" }, { type: "number" }] }
|
|
916
|
-
);
|
|
917
|
-
|
|
918
|
-
console.log(result.isSubset); // false
|
|
919
|
-
console.log(result.errors); // erreurs pour la branche non couverte
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
#### Unions discriminées
|
|
923
|
-
|
|
924
|
-
```ts
|
|
925
|
-
const sub = {
|
|
926
|
-
oneOf: [
|
|
927
|
-
{
|
|
928
|
-
type: "object",
|
|
929
|
-
properties: { kind: { const: "a" }, value: { type: "string" } },
|
|
930
|
-
required: ["kind", "value"],
|
|
931
|
-
},
|
|
932
|
-
{
|
|
933
|
-
type: "object",
|
|
934
|
-
properties: { kind: { const: "b" }, value: { type: "number" } },
|
|
935
|
-
required: ["kind", "value"],
|
|
936
|
-
},
|
|
937
|
-
],
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
const sup = {
|
|
941
|
-
type: "object",
|
|
942
|
-
properties: { kind: { type: "string" } },
|
|
943
|
-
required: ["kind"],
|
|
944
|
-
};
|
|
945
|
-
|
|
946
|
-
// Chaque branche de l'union discriminée est sous-ensemble du sup
|
|
947
|
-
checker.isSubset(sub, sup); // true
|
|
948
|
-
```
|
|
949
|
-
|
|
950
|
-
> **Note** : La librairie ne vérifie **pas** l'exclusivité sémantique de `oneOf` (le fait qu'exactement une branche doit matcher). Elle traite `oneOf` comme `anyOf` pour la vérification de sous-ensemble.
|
|
951
|
-
|
|
952
|
-
---
|
|
953
|
-
|
|
954
|
-
### 10. Négation (`not`)
|
|
955
|
-
|
|
956
|
-
La librairie gère le mot-clé `not` avec un raisonnement étendu.
|
|
957
|
-
|
|
958
|
-
#### Cas de base
|
|
959
|
-
|
|
960
|
-
```ts
|
|
961
|
-
// number ⊆ not(string) → true (un nombre n'est jamais une string)
|
|
962
|
-
checker.isSubset(
|
|
963
|
-
{ type: "number" },
|
|
964
|
-
{ not: { type: "string" } }
|
|
965
|
-
); // true
|
|
966
|
-
|
|
967
|
-
// string ⊄ not(string) → false
|
|
968
|
-
checker.isSubset(
|
|
969
|
-
{ type: "string" },
|
|
970
|
-
{ not: { type: "string" } }
|
|
971
|
-
); // false
|
|
972
|
-
```
|
|
973
|
-
|
|
974
|
-
#### not avec `const` et `enum`
|
|
975
|
-
|
|
976
|
-
```ts
|
|
977
|
-
// status: "active" est compatible avec not(status: "deleted")
|
|
978
|
-
checker.isSubset(
|
|
979
|
-
{
|
|
980
|
-
type: "object",
|
|
981
|
-
properties: { status: { const: "active" } },
|
|
982
|
-
required: ["status"],
|
|
983
|
-
},
|
|
984
|
-
{
|
|
985
|
-
not: {
|
|
986
|
-
type: "object",
|
|
987
|
-
properties: { status: { const: "deleted" } },
|
|
988
|
-
required: ["status"],
|
|
989
|
-
},
|
|
990
|
-
}
|
|
991
|
-
); // true
|
|
992
|
-
|
|
993
|
-
// enum disjoint du not.enum → compatible
|
|
994
|
-
checker.isSubset(
|
|
995
|
-
{ enum: [1, 2] },
|
|
996
|
-
{ not: { enum: [3, 4] } }
|
|
997
|
-
); // true
|
|
998
|
-
|
|
999
|
-
// enum qui chevauche not.enum → incompatible
|
|
1000
|
-
checker.isSubset(
|
|
1001
|
-
{ enum: [1, 2, 3] },
|
|
1002
|
-
{ not: { enum: [3, 4] } }
|
|
1003
|
-
); // false
|
|
1004
|
-
```
|
|
1005
|
-
|
|
1006
|
-
#### not avec `anyOf` / `oneOf`
|
|
1007
|
-
|
|
1008
|
-
```ts
|
|
1009
|
-
// number est compatible avec not(anyOf([string, null]))
|
|
1010
|
-
checker.isSubset(
|
|
1011
|
-
{ type: "number" },
|
|
1012
|
-
{ not: { anyOf: [{ type: "string" }, { type: "null" }] } }
|
|
1013
|
-
); // true
|
|
1014
|
-
|
|
1015
|
-
// string est INcompatible avec not(anyOf([string, null]))
|
|
1016
|
-
checker.isSubset(
|
|
1017
|
-
{ type: "string" },
|
|
1018
|
-
{ not: { anyOf: [{ type: "string" }, { type: "null" }] } }
|
|
1019
|
-
); // false
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
#### Double négation
|
|
1023
|
-
|
|
1024
|
-
La normalisation résout automatiquement `not(not(X))` en `X` :
|
|
1025
|
-
|
|
1026
|
-
```ts
|
|
1027
|
-
// not(not(string)) normalise en string
|
|
1028
|
-
checker.normalize({ not: { not: { type: "string" } } });
|
|
1029
|
-
// → { type: "string" }
|
|
1030
|
-
|
|
1031
|
-
// Donc not(not(string)) ⊆ string
|
|
1032
|
-
checker.isSubset(
|
|
1033
|
-
{ not: { not: { type: "string", minLength: 3 } } },
|
|
1034
|
-
{ type: "string" }
|
|
1035
|
-
); // true
|
|
1036
|
-
```
|
|
1037
|
-
|
|
1038
|
-
#### `not` dans sub comme restriction
|
|
1039
|
-
|
|
1040
|
-
Quand `not` apparaît dans le sub, c'est une **restriction** (exclut des valeurs), donc le sub reste un sous-ensemble du sup :
|
|
1041
|
-
|
|
1042
|
-
```ts
|
|
1043
|
-
// { type: "string", not: { const: "foo" } } ⊆ { type: "string" }
|
|
1044
|
-
// "Toutes les strings sauf foo" est sous-ensemble de "toutes les strings"
|
|
1045
|
-
checker.isSubset(
|
|
1046
|
-
{ type: "string", not: { const: "foo" } },
|
|
1047
|
-
{ type: "string" }
|
|
1048
|
-
); // true
|
|
1049
|
-
```
|
|
1050
|
-
|
|
1051
|
-
---
|
|
1052
|
-
|
|
1053
|
-
### 11. Formats (`format`)
|
|
1054
|
-
|
|
1055
|
-
La librairie connaît les formats JSON Schema Draft-07 et leur hiérarchie d'inclusion.
|
|
1056
|
-
|
|
1057
|
-
#### Formats supportés
|
|
1058
|
-
|
|
1059
|
-
`date-time`, `date`, `time`, `email`, `idn-email`, `hostname`, `idn-hostname`, `ipv4`, `ipv6`, `uri`, `uri-reference`, `iri`, `iri-reference`, `uri-template`, `uuid`, `json-pointer`, `relative-json-pointer`, `regex`.
|
|
1060
|
-
|
|
1061
|
-
#### Hiérarchie des formats
|
|
1062
|
-
|
|
1063
|
-
```
|
|
1064
|
-
email ⊆ idn-email
|
|
1065
|
-
hostname ⊆ idn-hostname
|
|
1066
|
-
uri ⊆ iri
|
|
1067
|
-
uri-reference ⊆ iri-reference
|
|
1068
|
-
```
|
|
1069
|
-
|
|
1070
|
-
#### Exemples
|
|
1071
|
-
|
|
1072
|
-
```ts
|
|
1073
|
-
// format ⊆ type (email ⊆ string) → géré nativement par le merge
|
|
1074
|
-
checker.isSubset(
|
|
1075
|
-
{ type: "string", format: "email" },
|
|
1076
|
-
{ type: "string" }
|
|
1077
|
-
); // true
|
|
1078
|
-
|
|
1079
|
-
// type ⊄ format (string ⊄ email) → le format ajoute une contrainte
|
|
1080
|
-
checker.isSubset(
|
|
1081
|
-
{ type: "string" },
|
|
1082
|
-
{ type: "string", format: "email" }
|
|
1083
|
-
); // false
|
|
1084
|
-
|
|
1085
|
-
// Hiérarchie : email ⊆ idn-email
|
|
1086
|
-
checker.isSubset(
|
|
1087
|
-
{ type: "string", format: "email" },
|
|
1088
|
-
{ type: "string", format: "idn-email" }
|
|
1089
|
-
); // true
|
|
1090
|
-
|
|
1091
|
-
// Hiérarchie inverse : idn-email ⊄ email
|
|
1092
|
-
checker.isSubset(
|
|
1093
|
-
{ type: "string", format: "idn-email" },
|
|
1094
|
-
{ type: "string", format: "email" }
|
|
1095
|
-
); // false
|
|
1096
|
-
|
|
1097
|
-
// Formats incompatibles : email ∩ ipv4 = ∅
|
|
1098
|
-
checker.intersect(
|
|
1099
|
-
{ type: "string", format: "email" },
|
|
1100
|
-
{ type: "string", format: "ipv4" }
|
|
1101
|
-
); // null
|
|
1102
|
-
|
|
1103
|
-
// Même format : email ∩ email = email
|
|
1104
|
-
checker.intersect(
|
|
1105
|
-
{ type: "string", format: "email" },
|
|
1106
|
-
{ type: "string", format: "email" }
|
|
1107
|
-
);
|
|
1108
|
-
// → { type: "string", format: "email" }
|
|
1109
|
-
```
|
|
1110
|
-
|
|
1111
|
-
#### Formats dans les conditions
|
|
1112
|
-
|
|
1113
|
-
Les formats sont aussi évalués dans les conditions `if/then/else` via `class-validator` :
|
|
1114
|
-
|
|
1115
|
-
```ts
|
|
1116
|
-
const schema = {
|
|
1117
|
-
type: "object",
|
|
1118
|
-
properties: {
|
|
1119
|
-
contactMethod: { type: "string" },
|
|
1120
|
-
contactValue: { type: "string" },
|
|
1121
|
-
},
|
|
1122
|
-
if: {
|
|
1123
|
-
properties: { contactValue: { format: "email" } },
|
|
1124
|
-
},
|
|
1125
|
-
then: { required: ["contactValue"] },
|
|
1126
|
-
};
|
|
1127
|
-
|
|
1128
|
-
const result = checker.resolveConditions(schema, {
|
|
1129
|
-
contactValue: "test@example.com", // valide pour format: email
|
|
1130
|
-
});
|
|
1131
|
-
console.log(result.branch); // "then"
|
|
1132
|
-
```
|
|
1133
|
-
|
|
1134
|
-
---
|
|
1135
|
-
|
|
1136
|
-
### 12. Patterns regex (`pattern`)
|
|
1137
|
-
|
|
1138
|
-
Les patterns regex sont comparés via une approche par **échantillonnage** (sampling) pour détecter les inclusions.
|
|
1139
|
-
|
|
1140
|
-
#### Mêmes patterns
|
|
1141
|
-
|
|
1142
|
-
```ts
|
|
1143
|
-
// Même pattern → toujours sous-ensemble
|
|
1144
|
-
checker.isSubset(
|
|
1145
|
-
{ type: "string", pattern: "^[a-z]+$" },
|
|
1146
|
-
{ type: "string", pattern: "^[a-z]+$" }
|
|
1147
|
-
); // true
|
|
1148
|
-
```
|
|
1149
|
-
|
|
1150
|
-
#### Pattern plus restrictif ⊆ pattern plus permissif
|
|
1151
|
-
|
|
1152
|
-
```ts
|
|
1153
|
-
// ^[a-z]{3}$ ⊆ ^[a-z]+$ (3 lettres ⊆ 1+ lettres)
|
|
1154
|
-
checker.isSubset(
|
|
1155
|
-
{ type: "string", pattern: "^[a-z]{3}$" },
|
|
1156
|
-
{ type: "string", pattern: "^[a-z]+$" }
|
|
1157
|
-
); // true
|
|
1158
|
-
|
|
1159
|
-
// L'inverse est faux
|
|
1160
|
-
checker.isSubset(
|
|
1161
|
-
{ type: "string", pattern: "^[a-z]+$" },
|
|
1162
|
-
{ type: "string", pattern: "^[a-z]{3}$" }
|
|
1163
|
-
); // false
|
|
1164
|
-
```
|
|
1165
|
-
|
|
1166
|
-
#### Patterns incompatibles
|
|
1167
|
-
|
|
1168
|
-
```ts
|
|
1169
|
-
// Lettres ⊄ chiffres
|
|
1170
|
-
checker.isSubset(
|
|
1171
|
-
{ type: "string", pattern: "^[a-z]+$" },
|
|
1172
|
-
{ type: "string", pattern: "^[0-9]+$" }
|
|
1173
|
-
); // false
|
|
1174
|
-
```
|
|
1175
|
-
|
|
1176
|
-
#### Pattern vs pas de pattern
|
|
1177
|
-
|
|
1178
|
-
```ts
|
|
1179
|
-
// Sub avec pattern, sup sans pattern → sous-ensemble (sub plus restrictif)
|
|
1180
|
-
checker.isSubset(
|
|
1181
|
-
{ type: "string", pattern: "^[a-z]+$" },
|
|
1182
|
-
{ type: "string" }
|
|
1183
|
-
); // true
|
|
1184
|
-
|
|
1185
|
-
// Sub sans pattern, sup avec pattern → PAS sous-ensemble
|
|
1186
|
-
checker.isSubset(
|
|
1187
|
-
{ type: "string" },
|
|
1188
|
-
{ type: "string", pattern: "^[a-z]+$" }
|
|
1189
|
-
); // false
|
|
1190
|
-
```
|
|
1191
|
-
|
|
1192
|
-
#### Patterns dans les propriétés imbriquées
|
|
1193
|
-
|
|
1194
|
-
```ts
|
|
1195
|
-
// Pattern sur une propriété imbriquée
|
|
1196
|
-
checker.isSubset(
|
|
1197
|
-
{
|
|
1198
|
-
type: "object",
|
|
1199
|
-
properties: { code: { type: "string", pattern: "^FR[0-9]{5}$" } },
|
|
1200
|
-
required: ["code"],
|
|
1201
|
-
},
|
|
1202
|
-
{
|
|
1203
|
-
type: "object",
|
|
1204
|
-
properties: { code: { type: "string", pattern: "^[A-Z]{2}[0-9]+$" } },
|
|
1205
|
-
required: ["code"],
|
|
1206
|
-
}
|
|
1207
|
-
); // true (FR + 5 chiffres ⊆ 2 majuscules + chiffres)
|
|
1208
|
-
```
|
|
1209
|
-
|
|
1210
|
-
> **Note** : la comparaison de patterns utilise un échantillonnage avec 200 samples par défaut. C'est une heuristique, pas une preuve formelle. Les faux positifs sont possibles mais très improbables. Les faux négatifs (counter-examples concrets) sont certains.
|
|
1211
|
-
|
|
1212
|
-
---
|
|
1213
|
-
|
|
1214
|
-
### 13. Conditions `if` / `then` / `else`
|
|
1215
|
-
|
|
1216
|
-
La librairie peut résoudre les conditions JSON Schema en évaluant le `if` contre des données partielles.
|
|
1217
|
-
|
|
1218
|
-
#### Résolution simple
|
|
1219
|
-
|
|
1220
|
-
```ts
|
|
1221
|
-
const schema = {
|
|
1222
|
-
type: "object",
|
|
1223
|
-
properties: {
|
|
1224
|
-
status: { type: "string" },
|
|
1225
|
-
activatedAt: { type: "string", format: "date-time" },
|
|
1226
|
-
},
|
|
1227
|
-
required: ["status"],
|
|
1228
|
-
if: {
|
|
1229
|
-
properties: { status: { const: "active" } },
|
|
1230
|
-
required: ["status"],
|
|
1231
|
-
},
|
|
1232
|
-
then: {
|
|
1233
|
-
required: ["activatedAt"],
|
|
1234
|
-
},
|
|
1235
|
-
};
|
|
1236
|
-
|
|
1237
|
-
// Si status = "active" → branche then appliquée
|
|
1238
|
-
const active = checker.resolveConditions(schema, { status: "active" });
|
|
1239
|
-
console.log(active.branch); // "then"
|
|
1240
|
-
console.log(active.resolved.required); // ["status", "activatedAt"]
|
|
1241
|
-
|
|
1242
|
-
// Si status ≠ "active" → branche else (ou pas de branche supplémentaire)
|
|
1243
|
-
const inactive = checker.resolveConditions(schema, { status: "inactive" });
|
|
1244
|
-
console.log(inactive.branch); // "else"
|
|
1245
|
-
console.log(inactive.resolved.required); // ["status"]
|
|
1246
|
-
```
|
|
1247
|
-
|
|
1248
|
-
#### Résolution avec des conditions sur `enum`
|
|
1249
|
-
|
|
1250
|
-
```ts
|
|
1251
|
-
const schema = {
|
|
1252
|
-
type: "object",
|
|
1253
|
-
properties: {
|
|
1254
|
-
tier: { type: "string" },
|
|
1255
|
-
limit: { type: "number" },
|
|
1256
|
-
},
|
|
1257
|
-
required: ["tier"],
|
|
1258
|
-
if: {
|
|
1259
|
-
properties: { tier: { enum: ["premium", "enterprise"] } },
|
|
1260
|
-
required: ["tier"],
|
|
1261
|
-
},
|
|
1262
|
-
then: {
|
|
1263
|
-
properties: { limit: { type: "number", minimum: 1000 } },
|
|
1264
|
-
required: ["limit"],
|
|
1265
|
-
},
|
|
1266
|
-
else: {
|
|
1267
|
-
properties: { limit: { type: "number", maximum: 100 } },
|
|
1268
|
-
},
|
|
1269
|
-
};
|
|
1270
|
-
|
|
1271
|
-
const premium = checker.resolveConditions(schema, { tier: "premium" });
|
|
1272
|
-
console.log(premium.branch); // "then"
|
|
1273
|
-
// limit requis avec minimum 1000
|
|
1274
|
-
|
|
1275
|
-
const free = checker.resolveConditions(schema, { tier: "free" });
|
|
1276
|
-
console.log(free.branch); // "else"
|
|
1277
|
-
// limit optionnel avec maximum 100
|
|
1278
|
-
```
|
|
1279
|
-
|
|
1280
|
-
#### Conditions imbriquées dans les propriétés
|
|
1281
|
-
|
|
1282
|
-
La résolution est **récursive** : les conditions à l'intérieur des propriétés sont aussi résolues.
|
|
1283
|
-
|
|
1284
|
-
```ts
|
|
1285
|
-
const schema = {
|
|
1286
|
-
type: "object",
|
|
1287
|
-
properties: {
|
|
1288
|
-
config: {
|
|
1289
|
-
type: "object",
|
|
1290
|
-
properties: {
|
|
1291
|
-
mode: { type: "string", enum: ["fast", "safe"] },
|
|
1292
|
-
retries: { type: "number" },
|
|
1293
|
-
timeout: { type: "number" },
|
|
1294
|
-
},
|
|
1295
|
-
required: ["mode"],
|
|
1296
|
-
if: {
|
|
1297
|
-
properties: { mode: { const: "safe" } },
|
|
1298
|
-
required: ["mode"],
|
|
1299
|
-
},
|
|
1300
|
-
then: {
|
|
1301
|
-
required: ["retries", "timeout"],
|
|
1302
|
-
properties: {
|
|
1303
|
-
retries: { type: "number", minimum: 3 },
|
|
1304
|
-
timeout: { type: "number", minimum: 1000 },
|
|
1305
|
-
},
|
|
1306
|
-
},
|
|
1307
|
-
},
|
|
1308
|
-
},
|
|
1309
|
-
required: ["config"],
|
|
1310
|
-
};
|
|
1311
|
-
|
|
1312
|
-
const result = checker.resolveConditions(schema, {
|
|
1313
|
-
config: { mode: "safe" },
|
|
1314
|
-
});
|
|
1315
|
-
|
|
1316
|
-
// La condition dans config a été résolue
|
|
1317
|
-
const configProp = result.resolved.properties?.config;
|
|
1318
|
-
console.log(configProp?.required); // ["mode", "retries", "timeout"]
|
|
1319
|
-
```
|
|
1320
|
-
|
|
1321
|
-
#### Évaluation avancée du `if`
|
|
1322
|
-
|
|
1323
|
-
Le `if` est évalué contre les données avec support complet de :
|
|
1324
|
-
|
|
1325
|
-
| Mot-clé | Description |
|
|
1326
|
-
|---|---|
|
|
1327
|
-
| `properties` avec `const` | Correspondance exacte d'une valeur |
|
|
1328
|
-
| `properties` avec `enum` | Valeur dans une liste |
|
|
1329
|
-
| `properties` avec `type` | Vérification du type |
|
|
1330
|
-
| `required` | Présence des clés |
|
|
1331
|
-
| `allOf` | Toutes les conditions doivent matcher |
|
|
1332
|
-
| `anyOf` | Au moins une condition doit matcher |
|
|
1333
|
-
| `oneOf` | Exactement une condition doit matcher |
|
|
1334
|
-
| `not` | Inversion du résultat |
|
|
1335
|
-
| `format` | Validation sémantique via `class-validator` |
|
|
1336
|
-
| Contraintes numériques | `minimum`, `maximum`, `exclusiveMinimum`, etc. |
|
|
1337
|
-
| Contraintes string | `minLength`, `maxLength` |
|
|
1338
|
-
| Contraintes array | `minItems`, `maxItems`, `uniqueItems` |
|
|
1339
|
-
|
|
1340
|
-
---
|
|
1341
|
-
|
|
1342
|
-
### 14. `allOf` avec conditions
|
|
1343
|
-
|
|
1344
|
-
Les conditions peuvent apparaître dans un `allOf`. Chaque entrée contenant un `if/then/else` est résolue individuellement.
|
|
1345
|
-
|
|
1346
|
-
```ts
|
|
1347
|
-
const schema = {
|
|
1348
|
-
type: "object",
|
|
1349
|
-
properties: {
|
|
1350
|
-
name: { type: "string" },
|
|
1351
|
-
age: { type: "number" },
|
|
1352
|
-
role: { type: "string", enum: ["admin", "user", "guest"] },
|
|
1353
|
-
},
|
|
1354
|
-
required: ["name"],
|
|
1355
|
-
allOf: [
|
|
1356
|
-
{
|
|
1357
|
-
if: {
|
|
1358
|
-
properties: { age: { type: "number", exclusiveMinimum: 20 } },
|
|
1359
|
-
required: ["age"],
|
|
1360
|
-
},
|
|
1361
|
-
then: {
|
|
1362
|
-
required: ["email"],
|
|
1363
|
-
properties: { email: { type: "string" } },
|
|
1364
|
-
},
|
|
1365
|
-
},
|
|
1366
|
-
{
|
|
1367
|
-
if: {
|
|
1368
|
-
properties: { role: { const: "admin" } },
|
|
1369
|
-
required: ["role"],
|
|
1370
|
-
},
|
|
1371
|
-
then: {
|
|
1372
|
-
properties: {
|
|
1373
|
-
permissions: { type: "array", items: { type: "string" } },
|
|
1374
|
-
},
|
|
1375
|
-
required: ["permissions"],
|
|
1376
|
-
},
|
|
1377
|
-
},
|
|
1378
|
-
],
|
|
1379
|
-
};
|
|
1380
|
-
|
|
1381
|
-
// Les deux conditions matchent
|
|
1382
|
-
const result = checker.resolveConditions(schema, {
|
|
1383
|
-
name: "Alice",
|
|
1384
|
-
age: 25,
|
|
1385
|
-
role: "admin",
|
|
1386
|
-
});
|
|
1387
|
-
|
|
1388
|
-
console.log(result.resolved.required);
|
|
1389
|
-
// → ["name", "email", "permissions"]
|
|
1390
|
-
// email requis car age > 20, permissions requis car role = admin
|
|
1391
|
-
|
|
1392
|
-
console.log(result.resolved.properties?.email);
|
|
1393
|
-
// → { type: "string" }
|
|
1394
|
-
|
|
1395
|
-
console.log(result.resolved.properties?.permissions);
|
|
1396
|
-
// → { type: "array", items: { type: "string" } }
|
|
1397
|
-
```
|
|
1398
|
-
|
|
1399
|
-
#### `allOf` combiné avec un `if/then/else` au niveau racine
|
|
1400
|
-
|
|
1401
|
-
```ts
|
|
1402
|
-
const schema = {
|
|
1403
|
-
type: "object",
|
|
1404
|
-
properties: {
|
|
1405
|
-
kind: { type: "string" },
|
|
1406
|
-
value: {},
|
|
1407
|
-
},
|
|
1408
|
-
required: ["kind"],
|
|
1409
|
-
// Condition racine
|
|
1410
|
-
if: {
|
|
1411
|
-
properties: { kind: { const: "numeric" } },
|
|
1412
|
-
required: ["kind"],
|
|
1413
|
-
},
|
|
1414
|
-
then: {
|
|
1415
|
-
properties: { value: { type: "number" } },
|
|
1416
|
-
},
|
|
1417
|
-
else: {
|
|
1418
|
-
properties: { value: { type: "string" } },
|
|
1419
|
-
},
|
|
1420
|
-
// Condition dans allOf
|
|
1421
|
-
allOf: [
|
|
1422
|
-
{
|
|
1423
|
-
if: {
|
|
1424
|
-
properties: { kind: { const: "numeric" } },
|
|
1425
|
-
required: ["kind"],
|
|
1426
|
-
},
|
|
1427
|
-
then: {
|
|
1428
|
-
properties: { precision: { type: "number" } },
|
|
1429
|
-
},
|
|
1430
|
-
},
|
|
1431
|
-
],
|
|
1432
|
-
};
|
|
1433
|
-
|
|
1434
|
-
const result = checker.resolveConditions(schema, { kind: "numeric" });
|
|
1435
|
-
|
|
1436
|
-
// Les deux conditions (racine + allOf) sont résolues
|
|
1437
|
-
console.log(result.resolved.properties?.value); // { type: "number" }
|
|
1438
|
-
console.log(result.resolved.properties?.precision); // { type: "number" }
|
|
1439
|
-
```
|
|
1440
|
-
|
|
1441
|
-
---
|
|
1442
|
-
|
|
1443
|
-
## Fonctions utilitaires
|
|
1444
|
-
|
|
1445
|
-
En plus de la classe principale, la librairie exporte des fonctions utilitaires pour travailler avec les patterns regex.
|
|
1446
|
-
|
|
1447
|
-
```ts
|
|
1448
|
-
import {
|
|
1449
|
-
isPatternSubset,
|
|
1450
|
-
arePatternsEquivalent,
|
|
1451
|
-
isTrivialPattern,
|
|
1452
|
-
} from "json-schema-compatibility-checker";
|
|
1453
|
-
```
|
|
1454
|
-
|
|
1455
|
-
---
|
|
1456
|
-
|
|
1457
|
-
### `isPatternSubset(sub, sup)`
|
|
1458
|
-
|
|
1459
|
-
```ts
|
|
1460
|
-
isPatternSubset(
|
|
1461
|
-
subPattern: string,
|
|
1462
|
-
supPattern: string,
|
|
1463
|
-
sampleCount?: number // défaut: 200
|
|
1464
|
-
): boolean | null
|
|
1465
|
-
```
|
|
1466
|
-
|
|
1467
|
-
Vérifie si le langage du pattern `sub` est un sous-ensemble du langage du pattern `sup` via **échantillonnage**.
|
|
1468
|
-
|
|
1469
|
-
**Contrat ternaire :**
|
|
1470
|
-
- `true` — toutes les strings échantillonnées de sub matchent sup (confiance haute)
|
|
1471
|
-
- `false` — au moins une string de sub ne matche PAS sup (certain, c'est un contre-exemple)
|
|
1472
|
-
- `null` — impossible de déterminer (pattern invalide, génération échouée)
|
|
1473
|
-
|
|
1474
|
-
```ts
|
|
1475
|
-
import { isPatternSubset } from "json-schema-compatibility-checker";
|
|
1476
|
-
|
|
1477
|
-
isPatternSubset("^[a-z]{3}$", "^[a-z]+$"); // true — 3 lettres ⊆ 1+ lettres
|
|
1478
|
-
isPatternSubset("^[a-z]+$", "^[0-9]+$"); // false — lettres ⊄ chiffres
|
|
1479
|
-
isPatternSubset("^[a-z]+$", "^[a-z]{3}$"); // false — "ab" matche sub mais pas sup
|
|
1480
|
-
isPatternSubset("invalid[", "^[a-z]+$"); // null — pattern invalide
|
|
1481
|
-
|
|
1482
|
-
// Cas réalistes
|
|
1483
|
-
isPatternSubset("^SKU-[0-9]{6}$", "^[A-Z]+-[0-9]+$"); // true
|
|
1484
|
-
isPatternSubset("^FR[0-9]{5}$", "^[A-Z]{2}[0-9]+$"); // true
|
|
1485
|
-
isPatternSubset("^(75|92|93|94)[0-9]{3}$", "^[0-9]{5}$"); // true
|
|
1486
|
-
```
|
|
1487
|
-
|
|
1488
|
-
---
|
|
1489
|
-
|
|
1490
|
-
### `arePatternsEquivalent(a, b)`
|
|
1491
|
-
|
|
1492
|
-
```ts
|
|
1493
|
-
arePatternsEquivalent(
|
|
1494
|
-
patternA: string,
|
|
1495
|
-
patternB: string,
|
|
1496
|
-
sampleCount?: number // défaut: 200
|
|
1497
|
-
): boolean | null
|
|
1498
|
-
```
|
|
1499
|
-
|
|
1500
|
-
Vérifie si deux patterns acceptent le **même langage** via un échantillonnage bidirectionnel (`A ⊆ B` ET `B ⊆ A`).
|
|
1501
|
-
|
|
1502
|
-
```ts
|
|
1503
|
-
import { arePatternsEquivalent } from "json-schema-compatibility-checker";
|
|
1504
|
-
|
|
1505
|
-
arePatternsEquivalent("^[a-z]+$", "^[a-z]+$"); // true — identiques
|
|
1506
|
-
arePatternsEquivalent("^[a-z]+$", "^[a-z]{3}$"); // false — cardinalité différente
|
|
1507
|
-
arePatternsEquivalent("^[a-f]+$", "^[a-z]+$"); // false — a-f ⊆ a-z mais pas l'inverse
|
|
1508
|
-
```
|
|
1509
|
-
|
|
1510
|
-
---
|
|
1511
|
-
|
|
1512
|
-
### `isTrivialPattern(pattern)`
|
|
1513
|
-
|
|
1514
|
-
```ts
|
|
1515
|
-
isTrivialPattern(pattern: string): boolean
|
|
1516
|
-
```
|
|
1517
|
-
|
|
1518
|
-
Vérifie si un pattern est **universellement permissif** (matche toute string). Utile pour détecter les patterns qui n'ajoutent aucune contrainte réelle.
|
|
1519
|
-
|
|
1520
|
-
```ts
|
|
1521
|
-
import { isTrivialPattern } from "json-schema-compatibility-checker";
|
|
1522
|
-
|
|
1523
|
-
isTrivialPattern(".*"); // true
|
|
1524
|
-
isTrivialPattern(".+"); // true
|
|
1525
|
-
isTrivialPattern("^.*$"); // true
|
|
1526
|
-
isTrivialPattern("^.+$"); // true
|
|
1527
|
-
isTrivialPattern(""); // true (pattern vide)
|
|
1528
|
-
|
|
1529
|
-
isTrivialPattern("^[a-z]+$"); // false
|
|
1530
|
-
isTrivialPattern("^[0-9]{3}$"); // false
|
|
1531
|
-
isTrivialPattern("abc"); // false
|
|
1532
|
-
```
|
|
1533
|
-
|
|
1534
|
-
---
|
|
1535
|
-
|
|
1536
|
-
## Cas d'usage concrets
|
|
1537
|
-
|
|
1538
|
-
### Connexion de nœuds dans un orchestrateur
|
|
1539
|
-
|
|
1540
|
-
Le cas d'usage principal de la librairie : dans un système d'orchestration visuel (style n8n, Node-RED, Zapier), vérifier que la sortie d'un nœud est compatible avec l'entrée du suivant.
|
|
1541
|
-
|
|
1542
|
-
```ts
|
|
1543
|
-
const checker = new JsonSchemaCompatibilityChecker();
|
|
1544
|
-
|
|
1545
|
-
// Nœud A : API qui retourne des utilisateurs paginés
|
|
1546
|
-
const nodeAOutput = {
|
|
1547
|
-
type: "object",
|
|
1548
|
-
properties: {
|
|
1549
|
-
items: {
|
|
1550
|
-
type: "array",
|
|
1551
|
-
items: {
|
|
1552
|
-
type: "object",
|
|
1553
|
-
properties: {
|
|
1554
|
-
id: { type: "integer", minimum: 1 },
|
|
1555
|
-
name: { type: "string", minLength: 1, maxLength: 255 },
|
|
1556
|
-
tags: {
|
|
1557
|
-
type: "array",
|
|
1558
|
-
items: { type: "string" },
|
|
1559
|
-
uniqueItems: true,
|
|
1560
|
-
},
|
|
1561
|
-
},
|
|
1562
|
-
required: ["id", "name"],
|
|
1563
|
-
},
|
|
1564
|
-
},
|
|
1565
|
-
page: { type: "integer", minimum: 1 },
|
|
1566
|
-
pageSize: { type: "integer", minimum: 1, maximum: 100 },
|
|
1567
|
-
totalPages: { type: "integer", minimum: 0 },
|
|
1568
|
-
},
|
|
1569
|
-
required: ["items", "page", "pageSize", "totalPages"],
|
|
1570
|
-
};
|
|
1571
|
-
|
|
1572
|
-
// Nœud B : traitement qui attend une liste avec pagination
|
|
1573
|
-
const nodeBInput = {
|
|
1574
|
-
type: "object",
|
|
1575
|
-
properties: {
|
|
1576
|
-
items: {
|
|
1577
|
-
type: "array",
|
|
1578
|
-
items: {
|
|
1579
|
-
type: "object",
|
|
1580
|
-
properties: {
|
|
1581
|
-
id: { type: "number" },
|
|
1582
|
-
name: { type: "string" },
|
|
1583
|
-
},
|
|
1584
|
-
required: ["id"],
|
|
1585
|
-
},
|
|
1586
|
-
},
|
|
1587
|
-
page: { type: "number" },
|
|
1588
|
-
totalPages: { type: "number" },
|
|
1589
|
-
},
|
|
1590
|
-
required: ["items"],
|
|
1591
|
-
};
|
|
1592
|
-
|
|
1593
|
-
const result = checker.check(nodeAOutput, nodeBInput);
|
|
1594
|
-
console.log(result.isSubset); // true ✅
|
|
1595
|
-
|
|
1596
|
-
// Si incompatible, le diagnostic explique pourquoi
|
|
1597
|
-
if (!result.isSubset) {
|
|
1598
|
-
console.log(checker.formatResult("NodeA → NodeB", result));
|
|
1599
|
-
}
|
|
1600
|
-
```
|
|
1601
|
-
|
|
1602
|
-
---
|
|
1603
|
-
|
|
1604
|
-
### Validation de réponse API
|
|
1605
|
-
|
|
1606
|
-
Vérifier qu'une réponse API réelle est compatible avec ce qu'un consommateur attend.
|
|
1607
|
-
|
|
1608
|
-
```ts
|
|
1609
|
-
const apiResponse = {
|
|
1610
|
-
type: "object",
|
|
1611
|
-
properties: {
|
|
1612
|
-
status: { type: "integer", minimum: 100, maximum: 599 },
|
|
1613
|
-
data: {
|
|
1614
|
-
type: "object",
|
|
1615
|
-
properties: {
|
|
1616
|
-
users: {
|
|
1617
|
-
type: "array",
|
|
1618
|
-
items: {
|
|
1619
|
-
type: "object",
|
|
1620
|
-
properties: {
|
|
1621
|
-
id: { type: "string", format: "uuid" },
|
|
1622
|
-
email: { type: "string", format: "email" },
|
|
1623
|
-
name: { type: "string", minLength: 1 },
|
|
1624
|
-
role: { type: "string", enum: ["admin", "user", "viewer"] },
|
|
1625
|
-
},
|
|
1626
|
-
required: ["id", "email", "name", "role"],
|
|
1627
|
-
},
|
|
1628
|
-
},
|
|
1629
|
-
total: { type: "integer", minimum: 0 },
|
|
1630
|
-
},
|
|
1631
|
-
required: ["users", "total"],
|
|
1632
|
-
},
|
|
1633
|
-
},
|
|
1634
|
-
required: ["status", "data"],
|
|
1635
|
-
};
|
|
1636
|
-
|
|
1637
|
-
const consumerExpects = {
|
|
1638
|
-
type: "object",
|
|
1639
|
-
properties: {
|
|
1640
|
-
status: { type: "integer" },
|
|
1641
|
-
data: {
|
|
1642
|
-
type: "object",
|
|
1643
|
-
properties: {
|
|
1644
|
-
users: {
|
|
1645
|
-
type: "array",
|
|
1646
|
-
items: {
|
|
1647
|
-
type: "object",
|
|
1648
|
-
properties: {
|
|
1649
|
-
id: { type: "string" },
|
|
1650
|
-
email: { type: "string" },
|
|
1651
|
-
},
|
|
1652
|
-
required: ["id", "email"],
|
|
1653
|
-
},
|
|
1654
|
-
},
|
|
1655
|
-
},
|
|
1656
|
-
required: ["users"],
|
|
1657
|
-
},
|
|
1658
|
-
},
|
|
1659
|
-
required: ["data"],
|
|
1660
|
-
};
|
|
1661
|
-
|
|
1662
|
-
const result = checker.check(apiResponse, consumerExpects);
|
|
1663
|
-
console.log(result.isSubset); // true ✅
|
|
1664
|
-
// L'API retourne plus de données que ce que le consommateur attend,
|
|
1665
|
-
// mais TOUTES les données requises sont présentes et du bon type.
|
|
1666
|
-
```
|
|
1667
|
-
|
|
1668
|
-
---
|
|
1669
|
-
|
|
1670
|
-
### Union discriminée
|
|
1671
|
-
|
|
1672
|
-
Vérifier qu'une union discriminée (`oneOf` avec un champ discriminant) est compatible avec un schema d'entrée flexible.
|
|
1673
|
-
|
|
1674
|
-
```ts
|
|
1675
|
-
const output = {
|
|
1676
|
-
oneOf: [
|
|
1677
|
-
{
|
|
1678
|
-
type: "object",
|
|
1679
|
-
properties: {
|
|
1680
|
-
type: { const: "success" },
|
|
1681
|
-
data: { type: "object" },
|
|
1682
|
-
},
|
|
1683
|
-
required: ["type", "data"],
|
|
1684
|
-
},
|
|
1685
|
-
{
|
|
1686
|
-
type: "object",
|
|
1687
|
-
properties: {
|
|
1688
|
-
type: { const: "error" },
|
|
1689
|
-
message: { type: "string" },
|
|
1690
|
-
code: { type: "integer" },
|
|
1691
|
-
},
|
|
1692
|
-
required: ["type", "message"],
|
|
1693
|
-
},
|
|
1694
|
-
],
|
|
1695
|
-
};
|
|
1696
|
-
|
|
1697
|
-
const input = {
|
|
1698
|
-
type: "object",
|
|
1699
|
-
properties: {
|
|
1700
|
-
type: { type: "string" },
|
|
1701
|
-
},
|
|
1702
|
-
required: ["type"],
|
|
1703
|
-
};
|
|
1704
|
-
|
|
1705
|
-
// Chaque branche de l'union a un champ "type" de type string
|
|
1706
|
-
checker.isSubset(output, input); // true ✅
|
|
1707
|
-
```
|
|
1708
|
-
|
|
1709
|
-
---
|
|
1710
|
-
|
|
1711
|
-
### Formulaire conditionnel
|
|
1712
|
-
|
|
1713
|
-
Valider qu'un formulaire rempli par l'utilisateur est compatible avec un schema conditionnel.
|
|
1714
|
-
|
|
1715
|
-
```ts
|
|
1716
|
-
const formSchema = {
|
|
1717
|
-
type: "object",
|
|
1718
|
-
properties: {
|
|
1719
|
-
accountType: { type: "string", enum: ["personal", "business"] },
|
|
1720
|
-
email: { type: "string", format: "email" },
|
|
1721
|
-
companyName: { type: "string" },
|
|
1722
|
-
taxId: { type: "string" },
|
|
1723
|
-
firstName: { type: "string" },
|
|
1724
|
-
lastName: { type: "string" },
|
|
1725
|
-
},
|
|
1726
|
-
required: ["accountType", "email"],
|
|
1727
|
-
if: {
|
|
1728
|
-
properties: { accountType: { const: "business" } },
|
|
1729
|
-
required: ["accountType"],
|
|
1730
|
-
},
|
|
1731
|
-
then: { required: ["companyName", "taxId"] },
|
|
1732
|
-
else: { required: ["firstName", "lastName"] },
|
|
1733
|
-
};
|
|
1734
|
-
|
|
1735
|
-
// Output d'un formulaire "business" rempli
|
|
1736
|
-
const businessOutput = {
|
|
1737
|
-
type: "object",
|
|
1738
|
-
properties: {
|
|
1739
|
-
accountType: { const: "business", type: "string", enum: ["personal", "business"] },
|
|
1740
|
-
email: { type: "string", format: "email" },
|
|
1741
|
-
companyName: { type: "string", minLength: 1 },
|
|
1742
|
-
taxId: { type: "string", minLength: 1 },
|
|
1743
|
-
},
|
|
1744
|
-
required: ["accountType", "email", "companyName", "taxId"],
|
|
1745
|
-
additionalProperties: false,
|
|
1746
|
-
};
|
|
1747
|
-
|
|
1748
|
-
// Sans résolution, le if/then/else brut cause un faux négatif
|
|
1749
|
-
checker.isSubset(businessOutput, formSchema); // false ❌
|
|
1750
|
-
|
|
1751
|
-
// Avec résolution, le schéma conditionnel est aplati
|
|
1752
|
-
const result = checker.check(businessOutput, formSchema, {
|
|
1753
|
-
subData: { accountType: "business" },
|
|
1754
|
-
});
|
|
1755
|
-
console.log(result.isSubset); // true ✅
|
|
1756
|
-
console.log(result.resolvedSup.branch); // "then"
|
|
1757
|
-
|
|
1758
|
-
// Output d'un formulaire "personal" rempli
|
|
1759
|
-
const personalOutput = {
|
|
1760
|
-
type: "object",
|
|
1761
|
-
properties: {
|
|
1762
|
-
accountType: { const: "personal", type: "string", enum: ["personal", "business"] },
|
|
1763
|
-
email: { type: "string", format: "email" },
|
|
1764
|
-
firstName: { type: "string", minLength: 1 },
|
|
1765
|
-
lastName: { type: "string", minLength: 1 },
|
|
1766
|
-
},
|
|
1767
|
-
required: ["accountType", "email", "firstName", "lastName"],
|
|
1768
|
-
additionalProperties: false,
|
|
1769
|
-
};
|
|
1770
|
-
|
|
1771
|
-
const personalResult = checker.check(personalOutput, formSchema, {
|
|
1772
|
-
subData: { accountType: "personal" },
|
|
1773
|
-
});
|
|
1774
|
-
console.log(personalResult.isSubset); // true ✅
|
|
1775
|
-
console.log(personalResult.resolvedSup.branch); // "else"
|
|
1776
|
-
```
|
|
1777
|
-
|
|
1778
|
-
---
|
|
1779
|
-
|
|
1780
|
-
## Types exportés
|
|
1781
|
-
|
|
1782
|
-
```ts
|
|
1783
|
-
import type {
|
|
1784
|
-
SubsetResult,
|
|
1785
|
-
SchemaError,
|
|
1786
|
-
ResolvedConditionResult,
|
|
1787
|
-
ResolvedSubsetResult,
|
|
1788
|
-
CheckConditionsOptions,
|
|
1789
|
-
} from "json-schema-compatibility-checker";
|
|
1790
|
-
```
|
|
1791
|
-
|
|
1792
|
-
### `SchemaError`
|
|
1793
|
-
|
|
1794
|
-
```ts
|
|
1795
|
-
interface SchemaError {
|
|
1796
|
-
/** Chemin normalisé vers la propriété concernée (ex: "user.name", "users[].name", "accountId") */
|
|
1797
|
-
key: string;
|
|
1798
|
-
/** Type ou valeur attendu(e) par le schema cible (sup) */
|
|
1799
|
-
expected: string;
|
|
1800
|
-
/** Type ou valeur reçu(e) depuis le schema source (sub) */
|
|
1801
|
-
received: string;
|
|
1802
|
-
}
|
|
1803
|
-
```
|
|
1804
|
-
|
|
1805
|
-
### `SubsetResult`
|
|
1806
|
-
|
|
1807
|
-
```ts
|
|
1808
|
-
interface SubsetResult {
|
|
1809
|
-
/** true si sub ⊆ sup */
|
|
1810
|
-
isSubset: boolean;
|
|
1811
|
-
/** Le schema résultant de l'intersection allOf(sub, sup), ou null si incompatible */
|
|
1812
|
-
merged: JSONSchema7Definition | null;
|
|
1813
|
-
/** Erreurs sémantiques décrivant les incompatibilités entre les deux schemas */
|
|
1814
|
-
errors: SchemaError[];
|
|
1815
|
-
}
|
|
1816
|
-
```
|
|
1817
|
-
|
|
1818
|
-
### `ResolvedConditionResult`
|
|
1819
|
-
|
|
1820
|
-
```ts
|
|
1821
|
-
interface ResolvedConditionResult {
|
|
1822
|
-
/** Le schema avec les if/then/else résolus (aplatis) */
|
|
1823
|
-
resolved: JSONSchema7;
|
|
1824
|
-
/** La branche qui a été appliquée ("then" | "else" | null si pas de condition) */
|
|
1825
|
-
branch: "then" | "else" | null;
|
|
1826
|
-
/** Le discriminant utilisé pour résoudre */
|
|
1827
|
-
discriminant: Record<string, unknown>;
|
|
1828
|
-
}
|
|
1829
|
-
```
|
|
1830
|
-
|
|
1831
|
-
### `ResolvedSubsetResult`
|
|
1832
|
-
|
|
1833
|
-
```ts
|
|
1834
|
-
interface ResolvedSubsetResult extends SubsetResult {
|
|
1835
|
-
/** Résultat de résolution des conditions du sub */
|
|
1836
|
-
resolvedSub: ResolvedConditionResult;
|
|
1837
|
-
/** Résultat de résolution des conditions du sup */
|
|
1838
|
-
resolvedSup: ResolvedConditionResult;
|
|
1839
|
-
}
|
|
1840
|
-
```
|
|
1841
|
-
|
|
1842
|
-
### `CheckConditionsOptions`
|
|
1843
|
-
|
|
1844
|
-
```ts
|
|
1845
|
-
interface CheckConditionsOptions {
|
|
1846
|
-
/** Runtime data for the sub schema — used for condition resolution and enum narrowing */
|
|
1847
|
-
subData: unknown;
|
|
1848
|
-
/** Runtime data for the sup schema (defaults to subData) — used for condition resolution and enum narrowing */
|
|
1849
|
-
supData?: unknown;
|
|
1850
|
-
}
|
|
1851
|
-
```
|
|
1852
|
-
|
|
1853
|
-
---
|
|
1854
|
-
|
|
1855
|
-
## Limitations connues
|
|
1856
|
-
|
|
1857
|
-
### 1. Cross-keyword constraints
|
|
1858
|
-
|
|
1859
|
-
La librairie utilise une comparaison **structurelle** : elle compare les mots-clés individuellement. Elle ne peut pas raisonner sur des relations entre mots-clés différents mais sémantiquement liés.
|
|
1860
|
-
|
|
1861
|
-
```ts
|
|
1862
|
-
// Sémantiquement, {exclusiveMinimum: 5} ⊆ {minimum: 0} est VRAI (x>5 implique x≥0)
|
|
1863
|
-
// Mais le merge ajoute minimum:0, ce qui rend merged ≠ sub structurellement
|
|
1864
|
-
checker.isSubset(
|
|
1865
|
-
{ type: "number", exclusiveMinimum: 5 },
|
|
1866
|
-
{ type: "number", minimum: 0 }
|
|
1867
|
-
); // false (faux négatif)
|
|
1868
|
-
```
|
|
1869
|
-
|
|
1870
|
-
### 2. `oneOf` — exclusivité non vérifiée
|
|
1871
|
-
|
|
1872
|
-
La librairie traite `oneOf` comme `anyOf` pour la vérification de sous-ensemble. L'exclusivité sémantique (exactement une branche doit matcher) n'est **pas** vérifiée.
|
|
1873
|
-
|
|
1874
|
-
```ts
|
|
1875
|
-
const overlapping = {
|
|
1876
|
-
oneOf: [
|
|
1877
|
-
{ type: "string", minLength: 1 }, // branches qui se chevauchent
|
|
1878
|
-
{ type: "string", maxLength: 100 },
|
|
1879
|
-
],
|
|
1880
|
-
};
|
|
1881
|
-
// En strict oneOf, "abc" matcherait les DEUX branches → rejeté
|
|
1882
|
-
// La librairie ne détecte pas ce chevauchement
|
|
1883
|
-
```
|
|
1884
|
-
|
|
1885
|
-
### 3. Patterns regex — approche probabiliste
|
|
1886
|
-
|
|
1887
|
-
La comparaison de patterns regex utilise un **échantillonnage** (200 samples par défaut). C'est une heuristique, pas une preuve formelle.
|
|
1888
|
-
|
|
1889
|
-
- **Faux négatifs** certains : si un counter-example est trouvé, l'exclusion est garantie
|
|
1890
|
-
- **Faux positifs** possibles : si tous les échantillons passent, ce n'est pas une preuve formelle (mais très improbable avec 200 samples)
|
|
1891
|
-
- Les patterns avec backreferences complexes peuvent poser problème
|
|
1892
|
-
|
|
1893
|
-
### 4. `if/then/else` — nécessite des données discriminantes
|
|
1894
|
-
|
|
1895
|
-
Les schemas avec `if/then/else` ne peuvent pas être comparés directement via `isSubset` car le merge brut ajoute les mots-clés conditionnels. Il faut utiliser `check(sub, sup, { subData })` avec les données discriminantes.
|
|
1896
|
-
|
|
1897
|
-
### 5. `$ref` — non supporté
|
|
1898
|
-
|
|
1899
|
-
Les références `$ref` ne sont pas résolues par la librairie. Il faut dé-référencer le schema avant de l'utiliser.
|
|
1900
|
-
|
|
1901
|
-
### 6. `patternProperties` — support partiel
|
|
1902
|
-
|
|
1903
|
-
Les `patternProperties` sont normalisés et comparés structurellement, mais la comparaison sémantique des patterns comme clés n'est pas effectuée.
|
|
1904
|
-
|
|
1905
|
-
---
|
|
1906
|
-
|
|
1907
|
-
## Architecture interne
|
|
1908
|
-
|
|
1909
|
-
La librairie est organisée en modules spécialisés, orchestrés par la façade `JsonSchemaCompatibilityChecker` :
|
|
1910
|
-
|
|
1911
|
-
```
|
|
1912
|
-
┌──────────────────────────────────────────────────┐
|
|
1913
|
-
│ JsonSchemaCompatibilityChecker │
|
|
1914
|
-
│ (Façade) │
|
|
1915
|
-
├──────────────────────────────────────────────────┤
|
|
1916
|
-
│ │
|
|
1917
|
-
│ ┌──────────────┐ ┌──────────────────────────┐ │
|
|
1918
|
-
│ │ Normalizer │ │ Condition Resolver │ │
|
|
1919
|
-
│ │ │ │ │ │
|
|
1920
|
-
│ │ - Infer type │ │ - Evaluate if │ │
|
|
1921
|
-
│ │ - enum→const │ │ - Merge then/else │ │
|
|
1922
|
-
│ │ - not(not(X))│ │ - Recurse in allOf │ │
|
|
1923
|
-
│ │ - Recurse │ │ - Nested properties │ │
|
|
1924
|
-
│ └──────────────┘ └──────────────────────────┘ │
|
|
1925
|
-
│ │
|
|
1926
|
-
│ ┌──────────────┐ ┌──────────────────────────┐ │
|
|
1927
|
-
│ │ Merge Engine │ │ Subset Checker │ │
|
|
1928
|
-
│ │ │ │ │ │
|
|
1929
|
-
│ │ - allOf merge│ │ - Atomic: A∩B ≡ A ? │ │
|
|
1930
|
-
│ │ - Conflict │ │ - Branched sub (anyOf) │ │
|
|
1931
|
-
│ │ detection │ │ - Branched sup (anyOf) │ │
|
|
1932
|
-
│ │ - Compare │ │ - evaluateNot │ │
|
|
1933
|
-
│ └──────────────┘ │ - stripNotFromSup │ │
|
|
1934
|
-
│ │ - stripPatternFromSup │ │
|
|
1935
|
-
│ ┌──────────────┐ └──────────────────────────┘ │
|
|
1936
|
-
│ │Semantic Errors│ │
|
|
1937
|
-
│ │ │ ┌──────────────────────────┐ │
|
|
1938
|
-
│ │-computeErrors│ │ Pattern Subset │ │
|
|
1939
|
-
│ │ - Recurse │ │ │ │
|
|
1940
|
-
│ │ - Properties │ │ - isPatternSubset │ │
|
|
1941
|
-
│ └──────────────┘ │ - arePatternsEquivalent │ │
|
|
1942
|
-
│ │ - isTrivialPattern │ │
|
|
1943
|
-
│ ┌──────────────┐ └──────────────────────────┘ │
|
|
1944
|
-
│ │ Formatter │ │
|
|
1945
|
-
│ │ │ ┌──────────────────────────┐ │
|
|
1946
|
-
│ │ - formatResult│ │ Format Validator │ │
|
|
1947
|
-
│ │ - Error lines│ │ │ │
|
|
1948
|
-
│ └──────────────┘ │ - validateFormat │ │
|
|
1949
|
-
│ │ - isFormatSubset │ │
|
|
1950
|
-
│ ┌──────────────┐ │ - Format hierarchy │ │
|
|
1951
|
-
│ │Data Narrowing │ └──────────────────────────┘ │
|
|
1952
|
-
│ │ │ │
|
|
1953
|
-
│ │-narrowSchema │ │
|
|
1954
|
-
│ │ WithData │ │
|
|
1955
|
-
│ │ - enum match │ │
|
|
1956
|
-
│ │ - Recurse │ │
|
|
1957
|
-
│ └──────────────┘ │
|
|
1958
|
-
└──────────────────────────────────────────────────┘
|
|
1959
|
-
```
|
|
1960
|
-
|
|
1961
|
-
### Flux de vérification `isSubset(sub, sup)`
|
|
1962
|
-
|
|
1963
|
-
```
|
|
1964
|
-
1. Normalize(sub), Normalize(sup)
|
|
1965
|
-
2. Detect branches (anyOf/oneOf) in sub and sup
|
|
1966
|
-
3. For each branch combination:
|
|
1967
|
-
a. evaluateNot() — pre-check not compatibility
|
|
1968
|
-
b. stripNotFromSup() — remove compatible not constraints
|
|
1969
|
-
c. stripPatternFromSup() — handle pattern inclusion via sampling
|
|
1970
|
-
d. engine.merge(sub, sup) — compute intersection
|
|
1971
|
-
e. normalize(merged)
|
|
1972
|
-
f. engine.isEqual(normalized_sub, normalized_merged) ?
|
|
1973
|
-
→ true: sub ⊆ sup ✅
|
|
1974
|
-
→ false: compute diffs, sub ⊄ sup ❌
|
|
1975
|
-
```
|
|
1976
|
-
|
|
1977
|
-
### Dépendances
|
|
1978
|
-
|
|
1979
|
-
| Package | Usage |
|
|
1980
|
-
|---|---|
|
|
1981
|
-
| `@x0k/json-schema-merge` | Merge engine pour `allOf` resolution |
|
|
1982
|
-
| `class-validator` | Validation des formats (email, URL, UUID, etc.) |
|
|
1983
|
-
| `randexp` | Génération de strings pour le sampling de patterns |
|
|
186
|
+
👉 Détails et exemples dans **[Limitations connues](./docs/limitations.md)**.
|
|
1984
187
|
|
|
1985
188
|
---
|
|
1986
189
|
|
|
1987
190
|
## Licence
|
|
1988
191
|
|
|
1989
|
-
MIT
|
|
192
|
+
MIT
|