json-schema-compatibility-checker 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2021 -0
- package/dist/chunk-2vpph7cy.js +5 -0
- package/dist/chunk-2vpph7cy.js.map +10 -0
- package/dist/chunk-cspd2zw4.js +6 -0
- package/dist/chunk-cspd2zw4.js.map +10 -0
- package/dist/chunk-e4501gq7.js +5 -0
- package/dist/chunk-e4501gq7.js.map +10 -0
- package/dist/chunk-g0pfcnm5.js +5 -0
- package/dist/chunk-g0pfcnm5.js.map +10 -0
- package/dist/chunk-h080ggvf.js +5 -0
- package/dist/chunk-h080ggvf.js.map +10 -0
- package/dist/chunk-pw49kj6f.js +5 -0
- package/dist/chunk-pw49kj6f.js.map +10 -0
- package/dist/chunk-v5tqyc67.js +5 -0
- package/dist/chunk-v5tqyc67.js.map +10 -0
- package/dist/chunk-vcwsxmk4.js +5 -0
- package/dist/chunk-vcwsxmk4.js.map +10 -0
- package/dist/chunk-w7qcey06.js +5 -0
- package/dist/chunk-w7qcey06.js.map +10 -0
- package/dist/condition-resolver.d.ts +26 -0
- package/dist/condition-resolver.js +4 -0
- package/dist/condition-resolver.js.map +9 -0
- package/dist/differ.d.ts +15 -0
- package/dist/differ.js +4 -0
- package/dist/differ.js.map +9 -0
- package/dist/format-validator.d.ts +78 -0
- package/dist/format-validator.js +4 -0
- package/dist/format-validator.js.map +9 -0
- package/dist/formatter.d.ts +22 -0
- package/dist/formatter.js +4 -0
- package/dist/formatter.js.map +9 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +9 -0
- package/dist/json-schema-compatibility-checker.d.ts +73 -0
- package/dist/json-schema-compatibility-checker.js +4 -0
- package/dist/json-schema-compatibility-checker.js.map +9 -0
- package/dist/merge-engine.d.ts +30 -0
- package/dist/merge-engine.js +4 -0
- package/dist/merge-engine.js.map +9 -0
- package/dist/normalizer.d.ts +18 -0
- package/dist/normalizer.js +4 -0
- package/dist/normalizer.js.map +9 -0
- package/dist/pattern-subset.d.ts +55 -0
- package/dist/pattern-subset.js +4 -0
- package/dist/pattern-subset.js.map +9 -0
- package/dist/subset-checker.d.ts +76 -0
- package/dist/subset-checker.js +4 -0
- package/dist/subset-checker.js.map +9 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +9 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,2021 @@
|
|
|
1
|
+
# JSON Schema Compatibility Checker
|
|
2
|
+
|
|
3
|
+
> Vérifiez la compatibilité structurelle entre JSON Schemas (Draft-07) grâce à une approche mathématique par intersection ensembliste.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Sommaire
|
|
8
|
+
|
|
9
|
+
- [Introduction](#introduction)
|
|
10
|
+
- [Principe mathématique](#principe-mathématique)
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Démarrage rapide](#démarrage-rapide)
|
|
13
|
+
- [API Reference](#api-reference)
|
|
14
|
+
- [`isSubset(sub, sup)`](#issubsetsub-sup)
|
|
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)
|
|
48
|
+
- [Limitations connues](#limitations-connues)
|
|
49
|
+
- [Architecture interne](#architecture-interne)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Introduction
|
|
54
|
+
|
|
55
|
+
**JSON Schema Compatibility Checker** est une librairie TypeScript qui permet de vérifier la compatibilité structurelle entre deux JSON Schemas au format Draft-07.
|
|
56
|
+
|
|
57
|
+
### Pourquoi cette librairie ?
|
|
58
|
+
|
|
59
|
+
Dans les systèmes de type workflow, orchestration de nœuds, ou intégration d'API, une question revient constamment :
|
|
60
|
+
|
|
61
|
+
> *"Est-ce que la sortie du composant A est compatible avec l'entrée du composant B ?"*
|
|
62
|
+
|
|
63
|
+
Autrement dit : **toute donnée produite par A sera-t-elle acceptée par B ?**
|
|
64
|
+
|
|
65
|
+
Cette librairie répond à cette question en vérifiant si un schema est un **sous-ensemble** d'un autre, avec un diagnostic structurel détaillé en cas d'incompatibilité.
|
|
66
|
+
|
|
67
|
+
### Ce que fait la librairie
|
|
68
|
+
|
|
69
|
+
- ✅ Vérifie si un schema est un sous-ensemble d'un autre (`sub ⊆ sup`)
|
|
70
|
+
- ✅ Produit un diagnostic détaillé avec les différences structurelles
|
|
71
|
+
- ✅ Calcule l'intersection de deux schemas
|
|
72
|
+
- ✅ Résout les conditions `if/then/else` avec des données discriminantes
|
|
73
|
+
- ✅ Gère `anyOf`, `oneOf`, `not`, `format`, `pattern`, `dependencies`, etc.
|
|
74
|
+
- ✅ Compare des patterns regex par échantillonnage
|
|
75
|
+
- ✅ Fournit un formatage lisible des résultats pour le debug
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Principe mathématique
|
|
80
|
+
|
|
81
|
+
Le cœur de la librairie repose sur un principe ensembliste simple :
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
A ⊆ B ⟺ A ∩ B ≡ A
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Un schema A est sous-ensemble de B si et seulement si l'intersection de A et B est structurellement identique à A.**
|
|
88
|
+
|
|
89
|
+
En JSON Schema, cela se traduit par :
|
|
90
|
+
|
|
91
|
+
| Concept mathématique | Traduction JSON Schema |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `A ∩ B` | `allOf([A, B])` résolu via merge |
|
|
94
|
+
| `≡` (équivalence) | Comparaison structurelle profonde |
|
|
95
|
+
|
|
96
|
+
Si après le merge (intersection), le résultat est identique au schema original `A`, alors `A` n'a pas été "restreint" par `B` — ce qui signifie que `A` est déjà inclus dans `B`.
|
|
97
|
+
|
|
98
|
+
Si le merge produit un résultat différent de `A`, les différences structurelles constituent le **diagnostic** de l'incompatibilité.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Installation
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
bun add json-schema-compatibility-checker
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> **Prérequis** : TypeScript ≥ 5, runtime compatible ESM (Bun, Node 18+).
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Démarrage rapide
|
|
113
|
+
|
|
114
|
+
L'exemple le plus simple : vérifier si un schema strict est compatible avec un schema plus permissif.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { JsonSchemaCompatibilityChecker } from "json-schema-compatibility-checker";
|
|
118
|
+
|
|
119
|
+
const checker = new JsonSchemaCompatibilityChecker();
|
|
120
|
+
|
|
121
|
+
// Schema strict : exige name ET age
|
|
122
|
+
const strict = {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
name: { type: "string" },
|
|
126
|
+
age: { type: "number" },
|
|
127
|
+
},
|
|
128
|
+
required: ["name", "age"],
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Schema permissif : exige seulement name
|
|
132
|
+
const loose = {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
name: { type: "string" },
|
|
136
|
+
},
|
|
137
|
+
required: ["name"],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Un objet valide pour strict est-il toujours valide pour loose ?
|
|
141
|
+
console.log(checker.isSubset(strict, loose)); // true ✅
|
|
142
|
+
|
|
143
|
+
// L'inverse est-il vrai ?
|
|
144
|
+
console.log(checker.isSubset(loose, strict)); // false ❌
|
|
145
|
+
// → Un objet { name: "Alice" } (sans age) est valide pour loose mais pas pour strict
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## API Reference
|
|
151
|
+
|
|
152
|
+
Toutes les méthodes sont exposées par la classe `JsonSchemaCompatibilityChecker`.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { JsonSchemaCompatibilityChecker } from "json-schema-compatibility-checker";
|
|
156
|
+
|
|
157
|
+
const checker = new JsonSchemaCompatibilityChecker();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### `isSubset(sub, sup)`
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
isSubset(sub: JSONSchema7Definition, sup: JSONSchema7Definition): boolean
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Vérifie si `sub ⊆ sup` — c'est-à-dire si **toute valeur valide pour `sub` est aussi valide pour `sup`**.
|
|
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
|
+
```
|
|
224
|
+
|
|
225
|
+
#### Exemple — Check compatible
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
const result = checker.check(
|
|
229
|
+
{ type: "string", minLength: 5 },
|
|
230
|
+
{ type: "string" }
|
|
231
|
+
);
|
|
232
|
+
|
|
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
|
+
console.log(result.isSubset); // false
|
|
259
|
+
console.log(result.diffs);
|
|
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
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Résout les `if/then/else` d'un schema en évaluant le `if` contre des données partielles (discriminants). Le schema résultant est un schema "aplati" sans `if/then/else`.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
interface ResolvedConditionResult {
|
|
437
|
+
resolved: JSONSchema7; // Schema avec if/then/else résolus
|
|
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",
|
|
476
|
+
});
|
|
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
|
+
console.log(result.isSubset); // true ✅
|
|
535
|
+
console.log(result.resolvedSup.branch); // "then"
|
|
536
|
+
```
|
|
537
|
+
|
|
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)
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## Guide des fonctionnalités
|
|
607
|
+
|
|
608
|
+
Cette section présente les fonctionnalités supportées, du plus simple au plus complexe, avec des exemples illustratifs.
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
### 1. Compatibilité de types
|
|
613
|
+
|
|
614
|
+
La librairie comprend le système de types JSON Schema et ses relations d'inclusion.
|
|
615
|
+
|
|
616
|
+
```ts
|
|
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
|
+
```
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
### 4. Contraintes de chaînes
|
|
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
|
+
```
|
|
745
|
+
|
|
746
|
+
Pour les patterns regex, voir la section [12. Patterns regex](#12-patterns-regex-pattern).
|
|
747
|
+
|
|
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 |
|
|
2016
|
+
|
|
2017
|
+
---
|
|
2018
|
+
|
|
2019
|
+
## Licence
|
|
2020
|
+
|
|
2021
|
+
Projet privé.
|