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