json-schema-compatibility-checker 1.0.11 → 1.1.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.
Files changed (80) hide show
  1. package/README.md +158 -83
  2. package/dist/cjs/condition-resolver.d.ts +6 -6
  3. package/dist/cjs/condition-resolver.js +1 -1
  4. package/dist/cjs/condition-resolver.js.map +1 -1
  5. package/dist/cjs/constraint-validator.d.ts +21 -0
  6. package/dist/cjs/constraint-validator.js +2 -0
  7. package/dist/cjs/constraint-validator.js.map +1 -0
  8. package/dist/cjs/data-narrowing.d.ts +4 -0
  9. package/dist/cjs/data-narrowing.js +1 -1
  10. package/dist/cjs/data-narrowing.js.map +1 -1
  11. package/dist/cjs/format-validator.d.ts +40 -40
  12. package/dist/cjs/format-validator.js.map +1 -1
  13. package/dist/cjs/formatter.d.ts +4 -4
  14. package/dist/cjs/formatter.js.map +1 -1
  15. package/dist/cjs/index.d.ts +1 -1
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/cjs/json-schema-compatibility-checker.d.ts +58 -36
  18. package/dist/cjs/json-schema-compatibility-checker.js +1 -1
  19. package/dist/cjs/json-schema-compatibility-checker.js.map +1 -1
  20. package/dist/cjs/merge-engine.d.ts +10 -10
  21. package/dist/cjs/merge-engine.js +1 -1
  22. package/dist/cjs/merge-engine.js.map +1 -1
  23. package/dist/cjs/normalizer.d.ts +15 -15
  24. package/dist/cjs/normalizer.js +1 -1
  25. package/dist/cjs/normalizer.js.map +1 -1
  26. package/dist/cjs/pattern-subset.d.ts +33 -33
  27. package/dist/cjs/pattern-subset.js.map +1 -1
  28. package/dist/cjs/runtime-validator.d.ts +30 -0
  29. package/dist/cjs/runtime-validator.js +2 -0
  30. package/dist/cjs/runtime-validator.js.map +1 -0
  31. package/dist/cjs/semantic-errors.d.ts +7 -7
  32. package/dist/cjs/semantic-errors.js +1 -1
  33. package/dist/cjs/semantic-errors.js.map +1 -1
  34. package/dist/cjs/subset-checker.d.ts +37 -44
  35. package/dist/cjs/subset-checker.js +1 -1
  36. package/dist/cjs/subset-checker.js.map +1 -1
  37. package/dist/cjs/types.d.ts +89 -20
  38. package/dist/cjs/utils.d.ts +37 -20
  39. package/dist/cjs/utils.js +1 -1
  40. package/dist/cjs/utils.js.map +1 -1
  41. package/dist/esm/condition-resolver.d.ts +6 -6
  42. package/dist/esm/condition-resolver.js +1 -1
  43. package/dist/esm/condition-resolver.js.map +1 -1
  44. package/dist/esm/constraint-validator.d.ts +21 -0
  45. package/dist/esm/constraint-validator.js +2 -0
  46. package/dist/esm/constraint-validator.js.map +1 -0
  47. package/dist/esm/data-narrowing.d.ts +4 -0
  48. package/dist/esm/data-narrowing.js +1 -1
  49. package/dist/esm/data-narrowing.js.map +1 -1
  50. package/dist/esm/format-validator.d.ts +40 -40
  51. package/dist/esm/format-validator.js.map +1 -1
  52. package/dist/esm/formatter.d.ts +4 -4
  53. package/dist/esm/formatter.js.map +1 -1
  54. package/dist/esm/index.d.ts +1 -1
  55. package/dist/esm/index.js.map +1 -1
  56. package/dist/esm/json-schema-compatibility-checker.d.ts +58 -36
  57. package/dist/esm/json-schema-compatibility-checker.js +1 -1
  58. package/dist/esm/json-schema-compatibility-checker.js.map +1 -1
  59. package/dist/esm/merge-engine.d.ts +10 -10
  60. package/dist/esm/merge-engine.js +1 -1
  61. package/dist/esm/merge-engine.js.map +1 -1
  62. package/dist/esm/normalizer.d.ts +15 -15
  63. package/dist/esm/normalizer.js +1 -1
  64. package/dist/esm/normalizer.js.map +1 -1
  65. package/dist/esm/pattern-subset.d.ts +33 -33
  66. package/dist/esm/pattern-subset.js.map +1 -1
  67. package/dist/esm/runtime-validator.d.ts +30 -0
  68. package/dist/esm/runtime-validator.js +2 -0
  69. package/dist/esm/runtime-validator.js.map +1 -0
  70. package/dist/esm/semantic-errors.d.ts +7 -7
  71. package/dist/esm/semantic-errors.js +1 -1
  72. package/dist/esm/semantic-errors.js.map +1 -1
  73. package/dist/esm/subset-checker.d.ts +37 -44
  74. package/dist/esm/subset-checker.js.map +1 -1
  75. package/dist/esm/types.d.ts +89 -20
  76. package/dist/esm/types.js.map +1 -1
  77. package/dist/esm/utils.d.ts +37 -20
  78. package/dist/esm/utils.js +1 -1
  79. package/dist/esm/utils.js.map +1 -1
  80. package/package.json +3 -1
package/README.md CHANGED
@@ -1,70 +1,73 @@
1
1
  # JSON Schema Compatibility Checker
2
2
 
3
- > Vérifiez la compatibilité structurelle entre JSON Schemas (Draft-07) grâce à une approche mathématique par intersection ensembliste.
3
+ > Check structural compatibility between JSON Schemas (Draft-07) using a set-theoretic intersection approach.
4
4
 
5
5
  ---
6
6
 
7
7
  ## Sommaire
8
8
 
9
9
  - [Introduction](#introduction)
10
- - [Principe mathématique](#principe-mathématique)
10
+ - [Mathematical Principle](#mathematical-principle)
11
11
  - [Installation](#installation)
12
- - [Démarrage rapide](#démarrage-rapide)
12
+ - [Quick Start](#quick-start)
13
13
  - [API Reference](#api-reference)
14
- - [Documentation complète](#-documentation-complète)
15
- - [Limitations connues](#limitations-connues)
16
- - [Licence](#licence)
14
+ - [Runtime Validation & Custom Constraints](#runtime-validation--custom-constraints)
15
+ - [Full Documentation](#-full-documentation)
16
+ - [Known Limitations](#known-limitations)
17
+ - [License](#license)
17
18
 
18
19
  ---
19
20
 
20
21
  ## Introduction
21
22
 
22
- **JSON Schema Compatibility Checker** est une librairie TypeScript qui permet de vérifier la compatibilité structurelle entre deux JSON Schemas au format Draft-07.
23
+ **JSON Schema Compatibility Checker** is a TypeScript library that checks structural compatibility between two JSON Schemas (Draft-07).
23
24
 
24
- ### Pourquoi cette librairie ?
25
+ ### Why this library?
25
26
 
26
- Dans les systèmes de type workflow, orchestration de nœuds, ou intégration d'API, une question revient constamment :
27
+ In workflow systems, node orchestration, or API integration, a recurring question is:
27
28
 
28
- > *"Est-ce que la sortie du composant A est compatible avec l'entrée du composant B ?"*
29
+ > *"Is the output of component A compatible with the input of component B?"*
29
30
 
30
- Autrement dit : **toute donnée produite par A sera-t-elle acceptée par B ?**
31
+ In other words: **will every value produced by A be accepted by B?**
31
32
 
32
- 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é.
33
+ This library answers that question by checking if one schema is a **subset** of another, with detailed structural diagnostics when incompatible.
33
34
 
34
- ### Ce que fait la librairie
35
+ ### What it does
35
36
 
36
- - ✅ Vérifie si un schema est un sous-ensemble d'un autre (`sub ⊆ sup`)
37
- - ✅ Produit un diagnostic détaillé avec les différences structurelles
38
- - ✅ Calcule l'intersection de deux schemas (`allOf` merge)
39
- - ✅ Accumule des schemas séquentiellement via deep spread (`overlay`)
40
- - ✅ Résout les conditions `if/then/else` avec des données discriminantes
41
- - ✅ Gère `anyOf`, `oneOf`, `not`, `format`, `pattern`, `dependencies`, etc.
42
- - ✅ Gère `oneOf`/`anyOf` imbriqués dans les propriétés d'objets et les items de tableaux
43
- - ✅ Compare des patterns regex par échantillonnage
44
- - ✅ Fournit un formatage lisible des résultats pour le debug
37
+ - ✅ Checks if a schema is a subset of another (`sub ⊆ sup`)
38
+ - ✅ Produces detailed diagnostics with structural differences
39
+ - ✅ Computes the intersection of two schemas (`allOf` merge)
40
+ - ✅ Accumulates schemas sequentially via deep spread (`overlay`)
41
+ - ✅ Resolves `if/then/else` conditions with discriminant data
42
+ - ✅ Handles `anyOf`, `oneOf`, `not`, `format`, `pattern`, `dependencies`, etc.
43
+ - ✅ Handles `oneOf`/`anyOf` nested inside object properties and array items
44
+ - ✅ Compares regex patterns via sampling
45
+ - ✅ Validates runtime data against resolved schemas (via [AJV](https://ajv.js.org/))
46
+ - ✅ Supports custom `constraints` keyword with user-provided validators
47
+ - ✅ Provides human-readable formatting of results for debugging
45
48
 
46
49
  ---
47
50
 
48
- ## Principe mathématique
51
+ ## Mathematical Principle
49
52
 
50
- Le cœur de la librairie repose sur un principe ensembliste simple :
53
+ The core of the library relies on a simple set-theoretic principle:
51
54
 
52
55
  ```
53
56
  A ⊆ B ⟺ A ∩ B ≡ A
54
57
  ```
55
58
 
56
- **Un schema A est sous-ensemble de B si et seulement si l'intersection de A et B est structurellement identique à A.**
59
+ **A schema A is a subset of B if and only if the intersection of A and B is structurally identical to A.**
57
60
 
58
- En JSON Schema, cela se traduit par :
61
+ In JSON Schema terms:
59
62
 
60
- | Concept mathématique | Traduction JSON Schema |
63
+ | Mathematical concept | JSON Schema translation |
61
64
  |---|---|
62
- | `A ∩ B` | `allOf([A, B])` résolu via merge |
63
- | `≡` (équivalence) | Comparaison structurelle profonde |
65
+ | `A ∩ B` | `allOf([A, B])` resolved via merge |
66
+ | `≡` (equivalence) | Deep structural comparison |
64
67
 
65
- 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`.
68
+ If after the merge (intersection) the result is identical to the original schema `A`, then `A` was not "restricted" by `B` — meaning `A` is already contained within `B`.
66
69
 
67
- Si le merge produit un résultat différent de `A`, les différences structurelles constituent le **diagnostic** de l'incompatibilité.
70
+ If the merge produces a result different from `A`, the structural differences constitute the **diagnostic** of the incompatibility.
68
71
 
69
72
  ---
70
73
 
@@ -74,20 +77,29 @@ Si le merge produit un résultat différent de `A`, les différences structurell
74
77
  bun add json-schema-compatibility-checker
75
78
  ```
76
79
 
77
- > **Prérequis** : TypeScript ≥ 5, runtime compatible ESM (Bun, Node 18+).
80
+ > **Prerequisites**: TypeScript ≥ 5, ESM-compatible runtime (Bun, Node 18+).
81
+
82
+ ### Runtime dependencies
83
+
84
+ | Package | Role | Size impact |
85
+ |---|---|---|
86
+ | `@x0k/json-schema-merge` | Schema intersection (`allOf` merge) | lightweight |
87
+ | `ajv` + `ajv-formats` | Runtime JSON Schema validation (condition evaluation, data validation) | ~250KB |
88
+ | `class-validator` | Format validation helpers | lightweight |
89
+ | `randexp` | Regex pattern sampling for subset analysis | lightweight |
78
90
 
79
91
  ---
80
92
 
81
- ## Démarrage rapide
93
+ ## Quick Start
82
94
 
83
- L'exemple le plus simple : vérifier si un schema strict est compatible avec un schema plus permissif.
95
+ The simplest example: check if a strict schema is compatible with a more permissive one.
84
96
 
85
97
  ```ts
86
98
  import { JsonSchemaCompatibilityChecker } from "json-schema-compatibility-checker";
87
99
 
88
100
  const checker = new JsonSchemaCompatibilityChecker();
89
101
 
90
- // Schema strict : exige name ET age
102
+ // Strict schema: requires name AND age
91
103
  const strict = {
92
104
  type: "object",
93
105
  properties: {
@@ -97,7 +109,7 @@ const strict = {
97
109
  required: ["name", "age"],
98
110
  };
99
111
 
100
- // Schema permissif : exige seulement name
112
+ // Permissive schema: requires only name
101
113
  const loose = {
102
114
  type: "object",
103
115
  properties: {
@@ -106,12 +118,12 @@ const loose = {
106
118
  required: ["name"],
107
119
  };
108
120
 
109
- // Un objet valide pour strict est-il toujours valide pour loose ?
121
+ // Is every value valid for strict also valid for loose?
110
122
  console.log(checker.isSubset(strict, loose)); // true ✅
111
123
 
112
- // L'inverse est-il vrai ?
124
+ // Is the reverse true?
113
125
  console.log(checker.isSubset(loose, strict)); // false ❌
114
- // → Un objet { name: "Alice" } (sans age) est valide pour loose mais pas pour strict
126
+ // → An object { name: "Alice" } (no age) is valid for loose but not for strict
115
127
  ```
116
128
 
117
129
  ---
@@ -120,26 +132,27 @@ console.log(checker.isSubset(loose, strict)); // false ❌
120
132
 
121
133
  ### `JsonSchemaCompatibilityChecker`
122
134
 
123
- Toutes les méthodes de vérification de compatibilité sont exposées par la classe `JsonSchemaCompatibilityChecker`.
135
+ All compatibility checking methods are exposed by the `JsonSchemaCompatibilityChecker` class.
124
136
 
125
137
  ```ts
126
138
  const checker = new JsonSchemaCompatibilityChecker();
127
139
  ```
128
140
 
129
- | Méthode | Description | Retour |
141
+ | Method | Description | Returns |
130
142
  |---|---|---|
131
- | `isSubset(sub, sup)` | Vérifie si `sub ⊆ sup` | `boolean` |
132
- | `check(sub, sup)` | Vérifie avec diagnostic détaillé | `SubsetResult` |
133
- | `check(sub, sup, options)` | Vérifie avec résolution des conditions `if/then/else` | `ResolvedSubsetResult` |
134
- | `isEqual(a, b)` | Égalité structurelle après normalisation | `boolean` |
135
- | `intersect(a, b)` | Intersection de deux schemas | `JSONSchema7Definition \| null` |
136
- | `resolveConditions(schema, data)` | Résout les `if/then/else` avec des données | `ResolvedConditionResult` |
137
- | `normalize(schema)` | Normalise un schema (infère types, résout double négation) | `JSONSchema7Definition` |
138
- | `formatResult(label, result)` | Formate un résultat pour le debug | `string` |
143
+ | `isSubset(sub, sup)` | Checks if `sub ⊆ sup` | `boolean` |
144
+ | `check(sub, sup)` | Checks with detailed diagnostics | `SubsetResult` |
145
+ | `check(sub, sup, options)` | Checks with `if/then/else` condition resolution and runtime validation | `ResolvedSubsetResult` |
146
+ | `isEqual(a, b)` | Structural equality after normalization | `boolean` |
147
+ | `intersect(a, b)` | Intersection of two schemas | `JSONSchema7Definition \| null` |
148
+ | `resolveConditions(schema, data)` | Resolves `if/then/else` with runtime data | `ResolvedConditionResult` |
149
+ | `normalize(schema)` | Normalizes a schema (infers types, resolves double negation, canonicalizes constraints) | `JSONSchema7Definition` |
150
+ | `formatResult(label, result)` | Formats a result for debug output | `string` |
151
+ | `clearValidatorCache()` | Clears the AJV compiled validator caches (useful for long-running processes or tests) | `void` |
139
152
 
140
153
  ### `MergeEngine`
141
154
 
142
- Opérations bas-niveau sur les schemas : intersection (`allOf` merge) et overlay (deep spread séquentiel).
155
+ Low-level schema operations: intersection (`allOf` merge) and overlay (sequential deep spread).
143
156
 
144
157
  ```ts
145
158
  import { MergeEngine } from "json-schema-compatibility-checker";
@@ -147,15 +160,15 @@ import { MergeEngine } from "json-schema-compatibility-checker";
147
160
  const engine = new MergeEngine();
148
161
  ```
149
162
 
150
- | Méthode | Description | Retour |
163
+ | Method | Description | Returns |
151
164
  |---|---|---|
152
- | `merge(a, b)` | Intersection `allOf([a, b])` — retourne `null` si incompatible | `JSONSchema7Definition \| null` |
153
- | `mergeOrThrow(a, b)` | Comme `merge`, mais lève une exception si incompatible | `JSONSchema7Definition` |
154
- | `overlay(base, override)` | Deep spread séquentiel — last writer wins par propriété | `JSONSchema7Definition` |
155
- | `compare(a, b)` | Comparaison structurelle (0 = identique) | `number` |
156
- | `isEqual(a, b)` | Égalité structurelle | `boolean` |
165
+ | `merge(a, b)` | Intersection `allOf([a, b])` — returns `null` if incompatible | `JSONSchema7Definition \| null` |
166
+ | `mergeOrThrow(a, b)` | Like `merge`, but throws if incompatible | `JSONSchema7Definition` |
167
+ | `overlay(base, override)` | Sequential deep spread — last writer wins per property | `JSONSchema7Definition` |
168
+ | `compare(a, b)` | Structural comparison (0 = identical) | `number` |
169
+ | `isEqual(a, b)` | Structural equality | `boolean` |
157
170
 
158
- **Exemple rapide — `check` avec diagnostic :**
171
+ **Quick example — `check` with diagnostics:**
159
172
 
160
173
  ```ts
161
174
  const result = checker.check(
@@ -168,78 +181,140 @@ console.log(result.errors);
168
181
  // [{ key: "age", expected: "number", received: "undefined" }]
169
182
  ```
170
183
 
171
- **Exemple rapiderésolution de conditions :**
184
+ **Quick examplecondition resolution:**
172
185
 
173
186
  ```ts
174
187
  const result = checker.check(sub, conditionalSup, {
175
- subData: { kind: "text" },
188
+ data: { kind: "text" },
176
189
  });
177
190
  console.log(result.isSubset); // true ✅
178
191
  console.log(result.resolvedSup.branch); // "then"
179
192
  ```
180
193
 
181
- **Exemple rapide — `overlay` pour accumulation séquentielle :**
194
+ **Quick example — `overlay` for sequential accumulation:**
182
195
 
183
196
  ```ts
184
197
  import { MergeEngine } from "json-schema-compatibility-checker";
185
198
 
186
199
  const engine = new MergeEngine();
187
200
 
188
- // Node1 produit accountId avec enum
201
+ // Node1 produces accountId with enum
189
202
  const node1Output = {
190
203
  type: "object",
191
204
  properties: { accountId: { type: "string", enum: ["a", "b"] } },
192
205
  required: ["accountId"],
193
206
  };
194
207
 
195
- // Node2 redéfinit accountId en string simple (plus large)
208
+ // Node2 redefines accountId as a simple string (wider)
196
209
  const node2Output = {
197
210
  type: "object",
198
211
  properties: { accountId: { type: "string" } },
199
212
  required: ["accountId"],
200
213
  };
201
214
 
202
- // ❌ merge (intersection) : garde l'enum — FAUX pour un pipeline séquentiel
215
+ // ❌ merge (intersection): keeps the enum — WRONG for a sequential pipeline
203
216
  engine.merge(node1Output, node2Output);
204
217
  // → { ..., properties: { accountId: { type: "string", enum: ["a", "b"] } } }
205
218
 
206
- // ✅ overlay (deep spread) : le dernier écrivain gagne — CORRECT
219
+ // ✅ overlay (deep spread): last writer wins — CORRECT
207
220
  engine.overlay(node1Output, node2Output);
208
221
  // → { ..., properties: { accountId: { type: "string" } } }
209
222
  ```
210
223
 
211
- 👉 Pour la documentation complète de chaque méthode avec tous les exemples, consultez la **[Référence API](./docs/api-reference.md)**.
224
+ 👉 For full documentation of every method with examples, see the **[API Reference](./docs/api-reference.md)**.
225
+
226
+ ---
227
+
228
+ ## Runtime Validation & Custom Constraints
229
+
230
+ ### Runtime data validation
231
+
232
+ When `check()` is called with `{ data }`, the library uses [AJV](https://ajv.js.org/) (JSON Schema validator) to:
233
+
234
+ 1. **Resolve `if/then/else` conditions** — evaluates the `if` schema against the data to determine which branch applies
235
+ 2. **Validate data against both resolved schemas** — catches data-level violations (wrong format, out of range, etc.)
236
+
237
+ ```ts
238
+ const result = checker.check(sub, sup, {
239
+ data: { kind: "email", value: "test@example.com" },
240
+ });
241
+ // result.isSubset — structural compatibility
242
+ // result.errors — includes runtime validation errors prefixed with $sub / $sup
243
+ ```
244
+
245
+ ### Custom `constraints` keyword
246
+
247
+ The library extends JSON Schema with a custom `constraints` keyword for domain-specific validation rules that go beyond what JSON Schema can express (e.g. "is a valid UUID", "belongs to scope", "minimum age"):
248
+
249
+ ```ts
250
+ import { JsonSchemaCompatibilityChecker } from "json-schema-compatibility-checker";
251
+
252
+ const checker = new JsonSchemaCompatibilityChecker({
253
+ constraints: {
254
+ IsUuid: (value) => ({
255
+ valid: typeof value === "string" && /^[0-9a-f]{8}-/.test(value),
256
+ message: "Value must be a valid UUID",
257
+ }),
258
+ MinAge: (value, params) => ({
259
+ valid: typeof value === "number" && value >= (params?.min ?? 0),
260
+ message: `Value must be at least ${params?.min}`,
261
+ }),
262
+ },
263
+ });
264
+
265
+ // Constraints in schemas
266
+ const sub = {
267
+ type: "object",
268
+ properties: {
269
+ id: { type: "string", constraints: ["IsUuid"] },
270
+ age: { type: "number", constraints: [{ name: "MinAge", params: { min: 18 } }] },
271
+ },
272
+ required: ["id", "age"],
273
+ };
274
+
275
+ // Static subset checking works with constraints (structural comparison)
276
+ checker.isSubset(sub, sup); // compares constraints via deepEqual after merge
277
+
278
+ // Runtime validation evaluates constraints against actual data
279
+ checker.check(sub, sup, { data: { id: "not-a-uuid", age: 15 } });
280
+ // → errors include constraint violations
281
+ ```
282
+
283
+ Constraints are handled at three levels:
284
+ - **Structurally**: the merge engine unions them (intersection semantics — `allOf`)
285
+ - **Statically**: the subset checker compares them via `deepEqual` after merge
286
+ - **At runtime**: the constraint validator evaluates them against concrete data
212
287
 
213
288
  ---
214
289
 
215
- ## 📖 Documentation complète
290
+ ## 📖 Full Documentation
216
291
 
217
292
  | Page | Description |
218
293
  |---|---|
219
- | **[Référence API](./docs/api-reference.md)** | Documentation détaillée de chaque méthode (`JsonSchemaCompatibilityChecker` + `MergeEngine`) avec exemples |
220
- | **[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`... |
221
- | **[Fonctions utilitaires](./docs/utilities.md)** | `isPatternSubset`, `arePatternsEquivalent`, `isTrivialPattern` |
222
- | **[Cas d'usage concrets](./docs/use-cases.md)** | Connexion de nœuds, pipeline séquentiel (overlay), validation de réponse API, unions discriminées, formulaires conditionnels |
223
- | **[Types exportés](./docs/types.md)** | `SubsetResult`, `SchemaError`, `ResolvedConditionResult`, `ResolvedSubsetResult`, `CheckConditionsOptions` |
224
- | **[Limitations connues](./docs/limitations.md)** | Cross-keyword constraints, `oneOf` exclusivité, patterns probabilistes, `$ref` non supporté |
225
- | **[Architecture interne](./docs/architecture.md)** | Diagramme des modules, flux de vérification, merge vs overlay, dépendances |
294
+ | **[API Reference](./docs/api-reference.md)** | Detailed documentation of every method (`JsonSchemaCompatibilityChecker` + `MergeEngine`) with examples |
295
+ | **[Features Guide](./docs/features-guide.md)** | Complete feature tour: types, `required`, numeric constraints, `enum`/`const`, `anyOf`/`oneOf`, `not`, `format`, `pattern`, `if/then/else` conditions, `allOf`, custom `constraints`... |
296
+ | **[Utility Functions](./docs/utilities.md)** | `isPatternSubset`, `arePatternsEquivalent`, `isTrivialPattern` |
297
+ | **[Use Cases](./docs/use-cases.md)** | Node connection, sequential pipeline (overlay), API response validation, discriminated unions, conditional forms |
298
+ | **[Exported Types](./docs/types.md)** | `SubsetResult`, `SchemaError`, `ResolvedConditionResult`, `ResolvedSubsetResult`, `CheckRuntimeOptions`, `ConstraintValidator`, `CheckerOptions` |
299
+ | **[Known Limitations](./docs/limitations.md)** | Cross-keyword constraints, `oneOf` exclusivity, probabilistic patterns, `$ref` not supported |
300
+ | **[Internal Architecture](./docs/architecture.md)** | Module diagram, verification flow, merge vs overlay, dependencies |
226
301
 
227
302
  ---
228
303
 
229
- ## Limitations connues
304
+ ## Known Limitations
230
305
 
231
- - **Cross-keyword constraints** : `exclusiveMinimum` vs `minimum` peut produire des faux négatifs (limitation structurelle)
232
- - **`oneOf` exclusivité** : traité comme `anyOf` — l'exclusivité sémantique n'est pas vérifiée
233
- - **Patterns regex** : approche probabiliste par échantillonnage (200 samples), pas une preuve formelle
234
- - **`if/then/else`** : nécessite des données discriminantes via `check(sub, sup, { subData })`
235
- - **`$ref`** : non supporté les schemas doivent être pré-déréférencés
236
- - **`patternProperties`** : support partiel
237
- - **Nested branching fallback** : le fallback propriété-par-propriété pour `oneOf`/`anyOf` imbriqués ne vérifie pas les mots-clés au niveau objet (`minProperties`/`maxProperties`) — ceux-ci sont gérés par le merge quand le branching n'est pas impliqué
306
+ - **Cross-keyword constraints**: `exclusiveMinimum` vs `minimum` comparison may produce false negatives (structural limitation)
307
+ - **`oneOf` exclusivity**: treated like `anyOf` — semantic exclusivity is not verified
308
+ - **Regex patterns**: probabilistic approach via sampling (200 samples), not a formal proof
309
+ - **`if/then/else`**: requires discriminant data via `check(sub, sup, { data })`
310
+ - **`$ref`**: not supported — schemas must be pre-dereferenced
311
+ - **`patternProperties`**: partial support only
312
+ - **Nested branching fallback**: the property-by-property fallback for nested `oneOf`/`anyOf` does not check object-level keywords (`minProperties`/`maxProperties`) — those are handled by the merge when branching is not involved
238
313
 
239
- 👉 Détails et exemples dans **[Limitations connues](./docs/limitations.md)**.
314
+ 👉 Details and examples in **[Known Limitations](./docs/limitations.md)**.
240
315
 
241
316
  ---
242
317
 
243
- ## Licence
318
+ ## License
244
319
 
245
320
  MIT
@@ -2,12 +2,12 @@ import type { JSONSchema7 } from "json-schema";
2
2
  import type { MergeEngine } from "./merge-engine.js";
3
3
  import type { ResolvedConditionResult } from "./types.js";
4
4
  /**
5
- * Résout les `if/then/else` d'un schema en évaluant le `if` contre
6
- * des données partielles (discriminants).
5
+ * Resolves `if/then/else` in a schema by evaluating the `if` against
6
+ * partial data (discriminants).
7
7
  *
8
- * @param schema Le schema contenant potentiellement des if/then/else
9
- * @param data Données partielles utilisées pour évaluer les conditions
10
- * @param engine Le MergeEngine pour merger les branches
8
+ * @param schema The schema potentially containing if/then/else
9
+ * @param data Partial data used to evaluate the conditions
10
+ * @param engine The MergeEngine for merging branches
11
11
  *
12
12
  * @example
13
13
  * ```ts
@@ -20,7 +20,7 @@ import type { ResolvedConditionResult } from "./types.js";
20
20
  * };
21
21
  *
22
22
  * const { resolved } = resolveConditions(form, { accountType: "business" }, engine);
23
- * // → resolved n'a plus de if/then/else, mais a required: ["companyName"]
23
+ * // → resolved no longer has if/then/else, but has required: ["companyName"]
24
24
  * ```
25
25
  */
26
26
  export declare function resolveConditions(schema: JSONSchema7, data: Record<string, unknown>, engine: MergeEngine): ResolvedConditionResult;
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});Object.defineProperty(exports,"resolveConditions",{enumerable:true,get:function(){return resolveConditions}});const _formatvalidator=require("./format-validator.js");const _normalizer=require("./normalizer.js");const _utils=require("./utils.js");const SPECIAL_MERGE_KEYS=new Set(["required","properties","dependencies"]);const SUB_SCHEMA_KEYS=new Set(["additionalProperties","items","contains","propertyNames","not"]);const MIN_KEYS=new Set(["minimum","exclusiveMinimum","minLength","minItems","minProperties"]);const MAX_KEYS=new Set(["maximum","exclusiveMaximum","maxLength","maxItems","maxProperties"]);function matchesType(value,type){if(type===undefined)return true;const types=Array.isArray(type)?type:[type];const actualType=(0,_normalizer.inferType)(value);return types.some(t=>t===actualType||t==="number"&&actualType==="integer")}function evaluateNumericConstraints(value,prop){if(prop.minimum!==undefined&&!(value>=prop.minimum))return false;if(prop.maximum!==undefined&&!(value<=prop.maximum))return false;if(prop.exclusiveMinimum!==undefined&&!(value>prop.exclusiveMinimum))return false;if(prop.exclusiveMaximum!==undefined&&!(value<prop.exclusiveMaximum))return false;if(prop.multipleOf!==undefined&&value%prop.multipleOf!==0)return false;return true}const patternRegexCache=new Map;function getOrCompileRegex(pattern){let regex=patternRegexCache.get(pattern);if(regex===undefined){regex=new RegExp(pattern);patternRegexCache.set(pattern,regex)}return regex}function evaluateStringConstraints(value,prop){if(prop.minLength!==undefined&&!(value.length>=prop.minLength))return false;if(prop.maxLength!==undefined&&!(value.length<=prop.maxLength))return false;if(prop.pattern!==undefined&&!getOrCompileRegex(prop.pattern).test(value))return false;return true}function evaluateArrayConstraints(value,prop){if(prop.minItems!==undefined&&!(value.length>=prop.minItems))return false;if(prop.maxItems!==undefined&&!(value.length<=prop.maxItems))return false;if(prop.uniqueItems===true){const len=value.length;for(let i=0;i<len;i++){for(let j=i+1;j<len;j++){if((0,_utils.deepEqual)(value[i],value[j]))return false}}}return true}function evaluateCondition(ifSchema,data){if((0,_utils.isPlainObj)(ifSchema.properties)){const propsOk=Object.keys(ifSchema.properties).every(key=>{const propDef=ifSchema.properties?.[key];if(typeof propDef==="boolean")return true;const prop=propDef;const value=data[key];if(value===undefined)return true;if((0,_utils.hasOwn)(prop,"const")){if(!(0,_utils.deepEqual)(value,prop.const))return false}if((0,_utils.hasOwn)(prop,"enum")){if(!prop.enum?.some(v=>(0,_utils.deepEqual)(v,value)))return false}if((0,_utils.hasOwn)(prop,"type")&&value!==undefined){if(!matchesType(value,prop.type))return false}if(typeof value==="number"){if(!evaluateNumericConstraints(value,prop))return false}if(typeof value==="string"){if(!evaluateStringConstraints(value,prop))return false}if(Array.isArray(value)){if(!evaluateArrayConstraints(value,prop))return false}if(prop.format!==undefined&&typeof value==="string"){const formatResult=(0,_formatvalidator.validateFormat)(value,prop.format);if(formatResult===false)return false}if((0,_utils.isPlainObj)(prop.properties)||Array.isArray(prop.required)){if((0,_utils.isPlainObj)(value)){if(!evaluateCondition(prop,value)){return false}}}return true});if(!propsOk)return false}if(Array.isArray(ifSchema.required)){const allRequired=ifSchema.required.every(key=>(0,_utils.hasOwn)(data,key));if(!allRequired)return false}if(Array.isArray(ifSchema.allOf)){const allMatch=ifSchema.allOf.every(entry=>{if(typeof entry==="boolean")return entry;return evaluateCondition(entry,data)});if(!allMatch)return false}if(Array.isArray(ifSchema.anyOf)){const anyMatch=ifSchema.anyOf.some(entry=>{if(typeof entry==="boolean")return entry;return evaluateCondition(entry,data)});if(!anyMatch)return false}if(Array.isArray(ifSchema.oneOf)){let matchCount=0;for(const entry of ifSchema.oneOf){const matches=typeof entry==="boolean"?entry:evaluateCondition(entry,data);if(matches)matchCount++;if(matchCount>1)break}if(matchCount!==1)return false}if((0,_utils.hasOwn)(ifSchema,"not")&&(0,_utils.isPlainObj)(ifSchema.not)&&typeof ifSchema.not!=="boolean"){const notResult=evaluateCondition(ifSchema.not,data);if(notResult)return false}return true}const DISCRIMINANT_INDICATORS=["const","enum","minimum","maximum","exclusiveMinimum","exclusiveMaximum","pattern","minLength","maxLength","multipleOf","minItems","maxItems","format"];function extractDiscriminants(ifSchema,data,out){if(!(0,_utils.isPlainObj)(ifSchema.properties))return;const props=ifSchema.properties;for(const key of Object.keys(props)){const propDef=props[key];if(typeof propDef==="boolean")continue;const prop=propDef;const hasIndicator=DISCRIMINANT_INDICATORS.some(indicator=>(0,_utils.hasOwn)(prop,indicator));if(hasIndicator&&(0,_utils.hasOwn)(data,key)){out[key]=data[key]}}}function mergeBranchInto(resolved,branchDef,engine){if(typeof branchDef==="boolean")return;const branchSchema=branchDef;if(Array.isArray(branchSchema.required)){resolved.required=(0,_utils.unionStrings)(resolved.required??[],branchSchema.required)}if((0,_utils.isPlainObj)(branchSchema.properties)){const branchProps=branchSchema.properties;const mergedProps={...resolved.properties??{}};for(const key of Object.keys(branchProps)){const branchProp=branchProps[key];if(branchProp===undefined)continue;const existing=resolved.properties?.[key];if(existing!==undefined&&typeof existing!=="boolean"&&typeof branchProp!=="boolean"){const merged=engine.merge(existing,branchProp);mergedProps[key]=merged??branchProp}else{mergedProps[key]=branchProp}}resolved.properties=mergedProps}if((0,_utils.isPlainObj)(branchSchema.dependencies)){const resolvedDeps=resolved.dependencies??{};const branchDeps=branchSchema.dependencies;const acc={...resolvedDeps};for(const depKey of Object.keys(branchDeps)){const branchVal=branchDeps[depKey];if(branchVal===undefined)continue;const existingVal=acc[depKey];if(existingVal===undefined){acc[depKey]=branchVal}else if(Array.isArray(existingVal)&&Array.isArray(branchVal)){acc[depKey]=(0,_utils.unionStrings)(existingVal,branchVal)}else if((0,_utils.isPlainObj)(existingVal)&&(0,_utils.isPlainObj)(branchVal)){const merged=engine.merge(existingVal,branchVal);acc[depKey]=merged??branchVal}else{acc[depKey]=branchVal}}resolved.dependencies=acc}for(const key of Object.keys(branchSchema)){if(SPECIAL_MERGE_KEYS.has(key))return;const branchVal=branchSchema[key];const resolvedVal=resolved[key];if(resolvedVal===undefined){resolved[key]=branchVal;return}if((0,_utils.deepEqual)(resolvedVal,branchVal))return;if(SUB_SCHEMA_KEYS.has(key)){const merged=engine.merge(resolvedVal,branchVal);if(merged!==null){resolved[key]=merged}else{resolved[key]=branchVal}return}if(MIN_KEYS.has(key)){if(typeof resolvedVal==="number"&&typeof branchVal==="number"){resolved[key]=Math.max(resolvedVal,branchVal)}else{resolved[key]=branchVal}return}if(MAX_KEYS.has(key)){if(typeof resolvedVal==="number"&&typeof branchVal==="number"){resolved[key]=Math.min(resolvedVal,branchVal)}else{resolved[key]=branchVal}return}if(key==="uniqueItems"){resolved[key]=resolvedVal===true||branchVal===true;return}if(key==="pattern"||key==="format"){resolved[key]=branchVal;return}const base={[key]:resolvedVal};const branch={[key]:branchVal};const merged=engine.merge(base,branch);if(merged&&typeof merged!=="boolean"&&(0,_utils.hasOwn)(merged,key)){resolved[key]=merged[key]}else{resolved[key]=branchVal}}}function resolveConditions(schema,data,engine){let branch=null;const discriminant={};const hasTopLevelIf=schema.if!==undefined;const hasAllOfConditions=Array.isArray(schema.allOf)&&schema.allOf.some(e=>typeof e!=="boolean"&&(0,_utils.hasOwn)(e,"if"));if(!hasTopLevelIf&&!hasAllOfConditions){const resolved=resolveNestedProperties(schema,data,engine,discriminant);return{resolved,branch,discriminant}}let resolved={...schema};if(hasAllOfConditions){resolved=resolveAllOfConditions(resolved,data,engine,discriminant)}if(resolved.if!==undefined){const ifSchema=resolved.if;const matches=evaluateCondition(ifSchema,data);extractDiscriminants(ifSchema,data,discriminant);const applicableBranch=matches?resolved.then:resolved.else;branch=matches?"then":"else";if(applicableBranch){mergeBranchInto(resolved,applicableBranch,engine)}delete resolved.if;delete resolved.then;delete resolved.else}resolved=resolveNestedProperties(resolved,data,engine,discriminant);return{resolved,branch,discriminant}}function resolveAllOfConditions(resolved,data,engine,discriminant){if(!Array.isArray(resolved.allOf))return resolved;const remainingAllOf=[];for(const entry of resolved.allOf){if(typeof entry==="boolean"){remainingAllOf.push(entry);continue}const subSchema=entry;if(subSchema.if===undefined){remainingAllOf.push(entry);continue}const ifSchema=subSchema.if;const matches=evaluateCondition(ifSchema,data);extractDiscriminants(ifSchema,data,discriminant);const applicableBranch=matches?subSchema.then:subSchema.else;if(applicableBranch){mergeBranchInto(resolved,applicableBranch,engine)}const remaining=(0,_utils.omitKeys)(subSchema,["if","then","else"]);if(Object.keys(remaining).length>0){remainingAllOf.push(remaining)}}resolved={...resolved};if(remainingAllOf.length===0){delete resolved.allOf}else{resolved.allOf=remainingAllOf}return resolved}function resolveNestedProperties(resolved,data,engine,discriminant){if(!(0,_utils.isPlainObj)(resolved.properties))return resolved;const props=resolved.properties;const propKeys=Object.keys(props);let changed=false;const resolvedProps={};for(const key of propKeys){const propDef=props[key];if(propDef===undefined)continue;if(typeof propDef==="boolean"){resolvedProps[key]=propDef;continue}const propSchema=propDef;const hasConditions=propSchema.if!==undefined||Array.isArray(propSchema.allOf)&&propSchema.allOf.some(e=>typeof e!=="boolean"&&(0,_utils.hasOwn)(e,"if"));if(!hasConditions){resolvedProps[key]=propDef;continue}const nestedData=(0,_utils.isPlainObj)(data[key])?data[key]:{};const nested=resolveConditions(propSchema,nestedData,engine);for(const dk of Object.keys(nested.discriminant)){discriminant[`${key}.${dk}`]=nested.discriminant[dk]}resolvedProps[key]=nested.resolved;changed=true}return changed?{...resolved,properties:resolvedProps}:resolved}
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});Object.defineProperty(exports,"resolveConditions",{enumerable:true,get:function(){return resolveConditions}});const _runtimevalidatorts=require("./runtime-validator.js");const _utilsts=require("./utils.js");const SPECIAL_MERGE_KEYS=new Set(["required","properties","dependencies"]);const SUB_SCHEMA_KEYS=new Set(["additionalProperties","items","contains","propertyNames","not"]);const MIN_KEYS=new Set(["minimum","exclusiveMinimum","minLength","minItems","minProperties"]);const MAX_KEYS=new Set(["maximum","exclusiveMaximum","maxLength","maxItems","maxProperties"]);function evaluateCondition(ifSchema,data){return(0,_runtimevalidatorts.isDataValidForSchema)(ifSchema,data)}const DISCRIMINANT_INDICATORS=["const","enum","minimum","maximum","exclusiveMinimum","exclusiveMaximum","pattern","minLength","maxLength","multipleOf","minItems","maxItems","format"];function extractDiscriminants(ifSchema,data,out){if(!(0,_utilsts.isPlainObj)(ifSchema.properties))return;const props=ifSchema.properties;for(const key of Object.keys(props)){const propDef=props[key];if(typeof propDef==="boolean")continue;const prop=propDef;const hasIndicator=DISCRIMINANT_INDICATORS.some(indicator=>(0,_utilsts.hasOwn)(prop,indicator));if(hasIndicator&&(0,_utilsts.hasOwn)(data,key)){out[key]=data[key]}}}function mergeBranchInto(resolved,branchDef,engine){if(typeof branchDef==="boolean")return;const branchSchema=branchDef;if(Array.isArray(branchSchema.required)){resolved.required=(0,_utilsts.unionStrings)(resolved.required??[],branchSchema.required)}if((0,_utilsts.isPlainObj)(branchSchema.properties)){const branchProps=branchSchema.properties;const mergedProps={...resolved.properties??{}};for(const key of Object.keys(branchProps)){const branchProp=branchProps[key];if(branchProp===undefined)continue;const existing=resolved.properties?.[key];if(existing!==undefined&&typeof existing!=="boolean"&&typeof branchProp!=="boolean"){const merged=engine.merge(existing,branchProp);mergedProps[key]=merged??branchProp}else{mergedProps[key]=branchProp}}resolved.properties=mergedProps}if((0,_utilsts.isPlainObj)(branchSchema.dependencies)){const resolvedDeps=resolved.dependencies??{};const branchDeps=branchSchema.dependencies;const acc={...resolvedDeps};for(const depKey of Object.keys(branchDeps)){const branchVal=branchDeps[depKey];if(branchVal===undefined)continue;const existingVal=acc[depKey];if(existingVal===undefined){acc[depKey]=branchVal}else if(Array.isArray(existingVal)&&Array.isArray(branchVal)){acc[depKey]=(0,_utilsts.unionStrings)(existingVal,branchVal)}else if((0,_utilsts.isPlainObj)(existingVal)&&(0,_utilsts.isPlainObj)(branchVal)){const merged=engine.merge(existingVal,branchVal);acc[depKey]=merged??branchVal}else{acc[depKey]=branchVal}}resolved.dependencies=acc}for(const key of Object.keys(branchSchema)){if(SPECIAL_MERGE_KEYS.has(key))continue;const branchVal=branchSchema[key];const resolvedVal=resolved[key];if(resolvedVal===undefined){resolved[key]=branchVal;continue}if((0,_utilsts.deepEqual)(resolvedVal,branchVal))continue;if(SUB_SCHEMA_KEYS.has(key)){const merged=engine.merge(resolvedVal,branchVal);if(merged!==null){resolved[key]=merged}else{resolved[key]=branchVal}continue}if(MIN_KEYS.has(key)){if(typeof resolvedVal==="number"&&typeof branchVal==="number"){resolved[key]=Math.max(resolvedVal,branchVal)}else{resolved[key]=branchVal}continue}if(MAX_KEYS.has(key)){if(typeof resolvedVal==="number"&&typeof branchVal==="number"){resolved[key]=Math.min(resolvedVal,branchVal)}else{resolved[key]=branchVal}continue}if(key==="uniqueItems"){resolved[key]=resolvedVal===true||branchVal===true;continue}if(key==="pattern"||key==="format"){resolved[key]=branchVal;continue}if(key==="constraints"){const merged=(0,_utilsts.mergeConstraints)(resolvedVal,branchVal);if(merged!==undefined){resolved[key]=merged}continue}const base={[key]:resolvedVal};const branch={[key]:branchVal};const merged=engine.merge(base,branch);if(merged&&typeof merged!=="boolean"&&(0,_utilsts.hasOwn)(merged,key)){resolved[key]=merged[key]}else{resolved[key]=branchVal}}}function resolveConditions(schema,data,engine){let branch=null;const discriminant={};const hasTopLevelIf=schema.if!==undefined;const hasAllOfConditions=Array.isArray(schema.allOf)&&schema.allOf.some(e=>typeof e!=="boolean"&&(0,_utilsts.hasOwn)(e,"if"));if(!hasTopLevelIf&&!hasAllOfConditions){const resolved=resolveNestedProperties(schema,data,engine,discriminant);return{resolved,branch,discriminant}}let resolved={...schema};if(hasAllOfConditions){resolved=resolveAllOfConditions(resolved,data,engine,discriminant)}if(resolved.if!==undefined){const ifSchema=resolved.if;const matches=evaluateCondition(ifSchema,data);extractDiscriminants(ifSchema,data,discriminant);const applicableBranch=matches?resolved.then:resolved.else;branch=matches?"then":"else";if(applicableBranch){mergeBranchInto(resolved,applicableBranch,engine)}delete resolved.if;delete resolved.then;delete resolved.else}resolved=resolveNestedProperties(resolved,data,engine,discriminant);return{resolved,branch,discriminant}}function resolveAllOfConditions(resolved,data,engine,discriminant){if(!Array.isArray(resolved.allOf))return resolved;const remainingAllOf=[];for(const entry of resolved.allOf){if(typeof entry==="boolean"){remainingAllOf.push(entry);continue}const subSchema=entry;if(subSchema.if===undefined){remainingAllOf.push(entry);continue}const ifSchema=subSchema.if;const matches=evaluateCondition(ifSchema,data);extractDiscriminants(ifSchema,data,discriminant);const applicableBranch=matches?subSchema.then:subSchema.else;if(applicableBranch){mergeBranchInto(resolved,applicableBranch,engine)}const remaining=(0,_utilsts.omitKeys)(subSchema,["if","then","else"]);if(Object.keys(remaining).length>0){remainingAllOf.push(remaining)}}resolved={...resolved};if(remainingAllOf.length===0){delete resolved.allOf}else{resolved.allOf=remainingAllOf}return resolved}function resolveNestedProperties(resolved,data,engine,discriminant){if(!(0,_utilsts.isPlainObj)(resolved.properties))return resolved;const props=resolved.properties;const propKeys=Object.keys(props);let changed=false;const resolvedProps={};for(const key of propKeys){const propDef=props[key];if(propDef===undefined)continue;if(typeof propDef==="boolean"){resolvedProps[key]=propDef;continue}const propSchema=propDef;const hasConditions=propSchema.if!==undefined||Array.isArray(propSchema.allOf)&&propSchema.allOf.some(e=>typeof e!=="boolean"&&(0,_utilsts.hasOwn)(e,"if"));if(!hasConditions){resolvedProps[key]=propDef;continue}const nestedData=(0,_utilsts.isPlainObj)(data[key])?data[key]:{};const nested=resolveConditions(propSchema,nestedData,engine);for(const dk of Object.keys(nested.discriminant)){discriminant[`${key}.${dk}`]=nested.discriminant[dk]}resolvedProps[key]=nested.resolved;changed=true}return changed?{...resolved,properties:resolvedProps}:resolved}
2
2
  //# sourceMappingURL=condition-resolver.js.map