@cloudflare/cabidela 0.0.18 → 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.1.1] - 2025-02-26
6
+
7
+ ### Added
8
+
9
+ - Added support for $id, $ref and $defs - https://json-schema.org/understanding-json-schema/structuring
10
+ - Added support for not - https://json-schema.org/understanding-json-schema/reference/combining#not
11
+
12
+ ## [0.0.19] - 2025-02-26
13
+
14
+ ### Added
15
+
16
+ - Added support for applying defaults with anyOf/oneOf and tests
17
+
18
+ ### Changed
19
+
20
+ - Minor code refactoring for better readability
21
+
22
+ ## [0.0.18] - 2025-02-20
23
+
24
+ ### Added
25
+
26
+ - Constant values support https://json-schema.org/understanding-json-schema/reference/const
27
+ - CHANGELOG.md file
package/README.md CHANGED
@@ -4,7 +4,6 @@
4
4
  </a>
5
5
  </div>
6
6
 
7
-
8
7
  <p align="center">
9
8
  <em>Small, fast, eval-less, <a href="https://developers.cloudflare.com/workers/">Cloudflare Workers</a> compatible, dynamic JSON Schema validator.</em>
10
9
  </p>
@@ -136,9 +135,123 @@ console.log(payload);
136
135
 
137
136
  ```
138
137
 
138
+ ### oneOf defaults
139
+
140
+ Using `applyDefaults` with `oneOf` will one apply the default value of the sub-schema that matches the condition. For
141
+ example, using this schema:
142
+
143
+ ```javascript
144
+ {
145
+ type: "object",
146
+ oneOf: [
147
+ {
148
+ type: "object",
149
+ properties: {
150
+ sun: {
151
+ type: "number",
152
+ default: 9000,
153
+ },
154
+ moon: {
155
+ type: "number",
156
+ default: 9000,
157
+ },
158
+ },
159
+ required: ["sun"],
160
+ },
161
+ {
162
+ type: "object",
163
+ properties: {
164
+ sun: {
165
+ type: "number",
166
+ default: 9000,
167
+ },
168
+ moon: {
169
+ type: "number",
170
+ default: 9000,
171
+ },
172
+ },
173
+ required: ["moon"],
174
+ },
175
+ ],
176
+ };
177
+ ```
178
+
179
+ - The payload `{ sun: 10}` will be modified to `{ sun: 10, moon: 9000 }`.
180
+ - The payload `{ moon: 10}` will be modified to `{ sun: 9000, moon: 10 }`.
181
+ - The payload `{ saturn: 10}` will throw an error because no condition is met.
182
+
183
+ ### $id, $ref, $defs
184
+
185
+ The keywords [$id](https://json-schema.org/understanding-json-schema/structuring#id), [$ref](https://json-schema.org/understanding-json-schema/structuring#dollarref) and [$defs](https://json-schema.org/understanding-json-schema/structuring#defs) can be used to build and maintain complex schemas where the reusable parts are defined in separate schemas.
186
+
187
+ The following is the main schema and a `customer` sub-schema that defines the `contacts` and `address` properties.
188
+
189
+ ```js
190
+ import { Cabidela } from "@cloudflare/cabidela";
191
+
192
+ const schema = {
193
+ $id: "http://example.com/schemas/main",
194
+ type: "object",
195
+ properties: {
196
+ name: { type: "string" },
197
+ contacts: { $ref: "customer#/contacts" },
198
+ address: { $ref: "customer#/address" },
199
+ balance: { $ref: "$defs#/balance" },
200
+ },
201
+ required: ["name", "contacts", "address"],
202
+ "$defs": {
203
+ "balance": {
204
+ type: "object",
205
+ prope properties: {
206
+ currency: { type: "string" },
207
+ amount: { type: "number" },
208
+ },
209
+ }
210
+ }
211
+ };
212
+
213
+ const contactSchema = {
214
+ $id: "http://example.com/schemas/customer",
215
+ contacts: {
216
+ type: "object",
217
+ properties: {
218
+ email: { type: "string" },
219
+ phone: { type: "string" },
220
+ },
221
+ required: ["email", "phone"],
222
+ },
223
+ address: {
224
+ type: "object",
225
+ properties: {
226
+ street: { type: "string" },
227
+ city: { type: "string" },
228
+ zip: { type: "string" },
229
+ country: { type: "string" },
230
+ },
231
+ required: ["street", "city", "zip", "country"],
232
+ },
233
+ };
234
+
235
+ const cabidela = new Cabidela(schema, { subSchemas: [contactSchema] });
236
+
237
+ cabidela.validate({
238
+ name: "John",
239
+ contacts: {
240
+ email: "john@example.com",
241
+ phone: "+123456789",
242
+ },
243
+ address: {
244
+ street: "123 Main St",
245
+ city: "San Francisco",
246
+ zip: "94105",
247
+ country: "USA",
248
+ },
249
+ });
250
+ ```
251
+
139
252
  ## Custom errors
140
253
 
141
- If the new instance options has the `errorMessages` flag set to true, you can use the property `errorMessage` in the schema to define custom error messages.
254
+ If the new instance options has the `errorMessages` flag set to true, you can use the property `errorMessage` in the schema to define custom error messages.
142
255
 
143
256
  ```js
144
257
  const schema = {
@@ -218,7 +331,7 @@ Here are some results:
218
331
  59.75x faster than Ajv
219
332
 
220
333
  Cabidela - benchmarks/80-big-ops.bench.js > allOf, two properties
221
- 1701.95x faster than Ajv
334
+ 1701.95x faster than Ajv
222
335
 
223
336
  Cabidela - benchmarks/80-big-ops.bench.js > allOf, two objects
224
337
  1307.04x faster than Ajv
@@ -236,17 +349,12 @@ We use Vitest's [bench](https://vitest.dev/api/#bench) feature to run the benchm
236
349
  npm run benchmark
237
350
  ```
238
351
 
239
-
240
352
  ## Current limitations
241
353
 
242
354
  Cabidela supports most of JSON Schema specification, and should be useful for many applications, but it's not complete. **Currently** we do not support:
243
355
 
244
356
  - Multiple (array of) types `{ "type": ["number", "string"] }`
245
- - Regular expressions
246
357
  - Pattern properties
247
- - `not`
248
358
  - `dependentRequired`, `dependentSchemas`, `If-Then-Else`
249
- - `$ref`, `$defs` and `$id`
250
359
 
251
360
  yet.
252
-
package/dist/index.d.mts CHANGED
@@ -2,6 +2,7 @@ type CabidelaOptions = {
2
2
  applyDefaults?: boolean;
3
3
  errorMessages?: boolean;
4
4
  fullErrors?: boolean;
5
+ subSchemas?: Array<any>;
5
6
  };
6
7
  type SchemaNavigation = {
7
8
  path: Array<string>;
@@ -9,14 +10,19 @@ type SchemaNavigation = {
9
10
  payload: any;
10
11
  evaluatedProperties: Set<string>;
11
12
  carryProperties?: boolean;
13
+ deferredApplyDefaults?: boolean;
12
14
  absorvErrors?: boolean;
13
15
  errors: Set<string>;
16
+ defaultsCallbacks: Array<any>;
14
17
  };
15
18
  declare class Cabidela {
16
- schema: any;
17
- options: CabidelaOptions;
19
+ private schema;
20
+ private options;
21
+ private definitions;
18
22
  constructor(schema: any, options?: CabidelaOptions);
19
23
  setSchema(schema: any): void;
24
+ addSchema(subSchema: any, combine?: boolean): void;
25
+ getSchema(): any;
20
26
  setOptions(options: CabidelaOptions): void;
21
27
  throw(message: string, needle: SchemaNavigation): void;
22
28
  parseAdditionalProperties(needle: SchemaNavigation, contextAdditionalProperties: any, contextEvaluatedProperties: Set<string>): number;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ type CabidelaOptions = {
2
2
  applyDefaults?: boolean;
3
3
  errorMessages?: boolean;
4
4
  fullErrors?: boolean;
5
+ subSchemas?: Array<any>;
5
6
  };
6
7
  type SchemaNavigation = {
7
8
  path: Array<string>;
@@ -9,14 +10,19 @@ type SchemaNavigation = {
9
10
  payload: any;
10
11
  evaluatedProperties: Set<string>;
11
12
  carryProperties?: boolean;
13
+ deferredApplyDefaults?: boolean;
12
14
  absorvErrors?: boolean;
13
15
  errors: Set<string>;
16
+ defaultsCallbacks: Array<any>;
14
17
  };
15
18
  declare class Cabidela {
16
- schema: any;
17
- options: CabidelaOptions;
19
+ private schema;
20
+ private options;
21
+ private definitions;
18
22
  constructor(schema: any, options?: CabidelaOptions);
19
23
  setSchema(schema: any): void;
24
+ addSchema(subSchema: any, combine?: boolean): void;
25
+ getSchema(): any;
20
26
  setOptions(options: CabidelaOptions): void;
21
27
  throw(message: string, needle: SchemaNavigation): void;
22
28
  parseAdditionalProperties(needle: SchemaNavigation, contextAdditionalProperties: any, contextEvaluatedProperties: Set<string>): number;
package/dist/index.js CHANGED
@@ -25,6 +25,32 @@ __export(index_exports, {
25
25
  module.exports = __toCommonJS(index_exports);
26
26
 
27
27
  // src/helpers.ts
28
+ var parse$ref = (ref) => {
29
+ const parts = ref.split("#");
30
+ const $id = parts[0];
31
+ const $path = parts[1].split("/").filter((part) => part !== "");
32
+ return { $id, $path };
33
+ };
34
+ var traverseSchema = (definitions, obj, cb = () => {
35
+ }) => {
36
+ Object.keys(obj).forEach((key) => {
37
+ if (obj[key] !== null && typeof obj[key] === "object") {
38
+ traverseSchema(definitions, obj[key], (value) => {
39
+ obj[key] = value;
40
+ });
41
+ } else {
42
+ if (key === "$ref") {
43
+ const { $id, $path } = parse$ref(obj[key]);
44
+ const { resolvedObject } = resolvePayload($path, definitions[$id]);
45
+ if (resolvedObject) {
46
+ cb(resolvedObject);
47
+ } else {
48
+ throw new Error(`Could not resolve '${obj[key]}' $ref`);
49
+ }
50
+ }
51
+ }
52
+ });
53
+ };
28
54
  var resolvePayload = (path, obj) => {
29
55
  let resolvedObject = path.reduce(function(prev, curr) {
30
56
  return prev ? prev[curr] : void 0;
@@ -32,12 +58,12 @@ var resolvePayload = (path, obj) => {
32
58
  return { metadata: getMetaData(resolvedObject), resolvedObject };
33
59
  };
34
60
  var pathToString = (path) => {
35
- return path.length == 0 ? `.` : path.map((item) => typeof item === "number" ? `[${item}]` : `.${item}`).join("");
61
+ return path.length == 0 ? `/` : path.map((item) => `/${item}`).join("");
36
62
  };
37
63
  var getMetaData = (value) => {
38
64
  let size = 0;
39
65
  let types = /* @__PURE__ */ new Set([]);
40
- let properties = /* @__PURE__ */ new Set([]);
66
+ let properties = [];
41
67
  if (value === null) {
42
68
  types.add("null");
43
69
  } else if (typeof value == "string") {
@@ -61,7 +87,7 @@ var getMetaData = (value) => {
61
87
  } else if (typeof value == "object") {
62
88
  types.add("object");
63
89
  size = Object.keys(value).length;
64
- properties = new Set(Object.keys(value));
90
+ properties = Object.keys(value);
65
91
  }
66
92
  return { types, size, properties };
67
93
  };
@@ -70,20 +96,50 @@ var getMetaData = (value) => {
70
96
  var Cabidela = class {
71
97
  schema;
72
98
  options;
99
+ definitions = {};
73
100
  constructor(schema, options) {
74
101
  this.schema = schema;
75
102
  this.options = {
76
103
  fullErrors: true,
104
+ subSchemas: [],
77
105
  applyDefaults: false,
78
106
  errorMessages: false,
79
107
  ...options || {}
80
108
  };
109
+ if (this.schema.hasOwnProperty("$defs")) {
110
+ this.definitions["$defs"] = this.schema["$defs"];
111
+ delete this.schema["$defs"];
112
+ }
113
+ if (this.options.subSchemas.length > 0) {
114
+ for (const subSchema of this.options.subSchemas) {
115
+ this.addSchema(subSchema, false);
116
+ }
117
+ traverseSchema(this.definitions, this.schema);
118
+ }
81
119
  }
82
120
  setSchema(schema) {
83
121
  this.schema = schema;
84
122
  }
123
+ addSchema(subSchema, combine = true) {
124
+ if (subSchema.hasOwnProperty("$id")) {
125
+ const url = URL.parse(subSchema["$id"]);
126
+ if (url) {
127
+ this.definitions[url.pathname.split("/").slice(-1)[0]] = subSchema;
128
+ } else {
129
+ throw new Error(
130
+ "subSchemas need a valid retrieval URI $id https://json-schema.org/understanding-json-schema/structuring#retrieval-uri"
131
+ );
132
+ }
133
+ } else {
134
+ throw new Error("subSchemas need $id https://json-schema.org/understanding-json-schema/structuring#id");
135
+ }
136
+ if (combine == true) traverseSchema(this.definitions, this.schema);
137
+ }
138
+ getSchema() {
139
+ return this.schema;
140
+ }
85
141
  setOptions(options) {
86
- this.options = options;
142
+ this.options = { ...this.options, ...options };
87
143
  }
88
144
  throw(message, needle) {
89
145
  const error = `${message}${this.options.fullErrors && needle.absorvErrors !== true && needle.errors.size > 0 ? `: ${Array.from(needle.errors).join(", ")}` : ``}`;
@@ -92,7 +148,9 @@ var Cabidela = class {
92
148
  parseAdditionalProperties(needle, contextAdditionalProperties, contextEvaluatedProperties) {
93
149
  let matchCount = 0;
94
150
  const { metadata, resolvedObject } = resolvePayload(needle.path, needle.payload);
95
- const unevaluatedProperties = metadata.properties.difference(contextEvaluatedProperties);
151
+ const unevaluatedProperties = new Set(
152
+ metadata.properties.map((r) => pathToString([...needle.path, r]))
153
+ ).difference(contextEvaluatedProperties);
96
154
  if (contextAdditionalProperties === false) {
97
155
  if (unevaluatedProperties.size > 0) {
98
156
  this.throw(
@@ -107,14 +165,15 @@ var Cabidela = class {
107
165
  } else {
108
166
  for (let property of unevaluatedProperties) {
109
167
  if (this.parseSubSchema({
110
- path: [property],
168
+ path: [property.split("/").slice(-1)[0]],
111
169
  schema: contextAdditionalProperties,
112
170
  payload: resolvedObject,
113
171
  evaluatedProperties: /* @__PURE__ */ new Set(),
114
- errors: /* @__PURE__ */ new Set()
172
+ errors: /* @__PURE__ */ new Set(),
173
+ defaultsCallbacks: []
115
174
  })) {
116
175
  matchCount++;
117
- needle.evaluatedProperties.add(property);
176
+ needle.evaluatedProperties.add(pathToString([property]));
118
177
  }
119
178
  }
120
179
  }
@@ -142,12 +201,13 @@ var Cabidela = class {
142
201
  let matchCount = 0;
143
202
  if (needle.schema.hasOwnProperty("properties")) {
144
203
  for (let property in needle.schema.properties) {
145
- if (this.parseSubSchema({
204
+ const matches = this.parseSubSchema({
146
205
  ...needle,
147
206
  path: [...needle.path, property],
148
207
  schema: needle.schema.properties[property]
149
- })) {
150
- localEvaluatedProperties.add(property);
208
+ });
209
+ if (matches > 0) {
210
+ localEvaluatedProperties.add(pathToString([...needle.path, property]));
151
211
  matchCount++;
152
212
  }
153
213
  }
@@ -168,27 +228,37 @@ var Cabidela = class {
168
228
  );
169
229
  }
170
230
  if (needle.schema.hasOwnProperty("required")) {
171
- if (new Set(needle.schema.required).difference(needle.evaluatedProperties.union(localEvaluatedProperties)).size > 0) {
172
- this.throw(`required properties at '${pathToString(needle.path)}' is '${needle.schema.required}'`, needle);
231
+ if (new Set(needle.schema.required.map((r) => pathToString([...needle.path, r]))).difference(
232
+ needle.evaluatedProperties.union(localEvaluatedProperties)
233
+ ).size > 0) {
234
+ this.throw(`required properties at '${pathToString(needle.path)}' are '${needle.schema.required}'`, needle);
173
235
  }
174
236
  }
175
237
  return matchCount ? true : false;
176
238
  }
177
239
  parseList(list, needle, breakCondition) {
178
240
  let rounds = 0;
241
+ const defaultsCallbacks = [];
179
242
  for (let option in list) {
180
243
  try {
181
- rounds += this.parseSubSchema({
244
+ const matches = this.parseSubSchema({
182
245
  ...needle,
183
246
  schema: { type: needle.schema.type, ...list[option] },
184
- carryProperties: true,
185
- absorvErrors: true
247
+ carryProperties: false,
248
+ absorvErrors: true,
249
+ deferredApplyDefaults: true
186
250
  });
251
+ rounds += matches;
187
252
  if (breakCondition && breakCondition(rounds)) break;
253
+ defaultsCallbacks.push(...needle.defaultsCallbacks);
254
+ needle.defaultsCallbacks = [];
188
255
  } catch (e) {
189
256
  needle.errors.add(e.message);
257
+ needle.defaultsCallbacks = [];
190
258
  }
191
259
  }
260
+ for (const callback of defaultsCallbacks) callback();
261
+ needle.defaultsCallbacks = [];
192
262
  return rounds;
193
263
  }
194
264
  // Parses a JSON Schema sub-schema object - reentrant
@@ -196,10 +266,25 @@ var Cabidela = class {
196
266
  if (needle.schema == void 0) {
197
267
  this.throw(`No schema for path '${pathToString(needle.path)}'`, needle);
198
268
  }
269
+ if (needle.schema.hasOwnProperty("not")) {
270
+ let pass = false;
271
+ try {
272
+ this.parseSubSchema({
273
+ ...needle,
274
+ schema: needle.schema.not
275
+ });
276
+ } catch (e) {
277
+ pass = true;
278
+ }
279
+ if (pass == false) {
280
+ this.throw(`not at '${pathToString(needle.path)}' not met`, needle);
281
+ }
282
+ }
199
283
  if (needle.schema.hasOwnProperty("oneOf")) {
200
- if (this.parseList(needle.schema.oneOf, needle) !== 1) {
284
+ const rounds = this.parseList(needle.schema.oneOf, needle, (r) => r !== 1);
285
+ if (rounds !== 1) {
201
286
  if (needle.path.length == 0) {
202
- this.throw(`oneOf at '${pathToString(needle.path)}' not met`, needle);
287
+ this.throw(`oneOf at '${pathToString(needle.path)}' not met, ${rounds} matches`, needle);
203
288
  }
204
289
  return 0;
205
290
  }
@@ -270,7 +355,7 @@ var Cabidela = class {
270
355
  }
271
356
  if (needle.schema.hasOwnProperty("type") && !metadata.types.has(needle.schema.type)) {
272
357
  this.throw(
273
- `Type mismatch of '${pathToString(needle.path)}', '${needle.schema.type}' not in ${JSON.stringify(Array.from(metadata.types))}`,
358
+ `Type mismatch of '${pathToString(needle.path)}', '${needle.schema.type}' not in ${Array.from(metadata.types).map((e) => `'${e}'`).join(",")}`,
274
359
  needle
275
360
  );
276
361
  }
@@ -307,28 +392,44 @@ var Cabidela = class {
307
392
  break;
308
393
  }
309
394
  }
395
+ if (needle.schema.hasOwnProperty("pattern")) {
396
+ let passes = false;
397
+ try {
398
+ if (new RegExp(needle.schema.pattern).test(resolvedObject)) passes = true;
399
+ } catch (e) {
400
+ }
401
+ if (!passes) this.throw(`'${pathToString(needle.path)}' failed test ${needle.schema.pattern} patttern`, needle);
402
+ }
310
403
  if (needle.carryProperties) {
311
- needle.evaluatedProperties.add(needle.path[needle.path.length - 1]);
404
+ needle.evaluatedProperties.add(pathToString(needle.path));
312
405
  }
313
406
  return 1;
314
407
  }
315
408
  if (this.options.applyDefaults === true && needle.schema.hasOwnProperty("default")) {
316
- needle.path.reduce(function(prev, curr, index) {
317
- if (prev[curr] === void 0) {
318
- prev[curr] = {};
319
- }
320
- if (index == needle.path.length - 1) {
321
- prev[curr] = needle.schema.default;
322
- needle.evaluatedProperties.add(needle.path[needle.path.length - 1]);
323
- }
324
- return prev ? prev[curr] : void 0;
325
- }, needle.payload);
409
+ const applyDefaults = () => {
410
+ needle.path.reduce(function(prev, curr, index) {
411
+ if (prev[curr] === void 0) {
412
+ prev[curr] = {};
413
+ }
414
+ if (index == needle.path.length - 1) {
415
+ prev[curr] = needle.schema.default;
416
+ needle.evaluatedProperties.add(pathToString(needle.path));
417
+ }
418
+ return prev ? prev[curr] : void 0;
419
+ }, needle.payload);
420
+ };
421
+ if (needle.deferredApplyDefaults === true) {
422
+ needle.defaultsCallbacks.push(applyDefaults);
423
+ } else {
424
+ applyDefaults();
425
+ }
326
426
  }
327
427
  return 0;
328
428
  }
329
429
  validate(payload) {
330
430
  const needle = {
331
431
  errors: /* @__PURE__ */ new Set(),
432
+ defaultsCallbacks: [],
332
433
  evaluatedProperties: /* @__PURE__ */ new Set(),
333
434
  path: [],
334
435
  schema: this.schema,
package/dist/index.mjs CHANGED
@@ -1,4 +1,30 @@
1
1
  // src/helpers.ts
2
+ var parse$ref = (ref) => {
3
+ const parts = ref.split("#");
4
+ const $id = parts[0];
5
+ const $path = parts[1].split("/").filter((part) => part !== "");
6
+ return { $id, $path };
7
+ };
8
+ var traverseSchema = (definitions, obj, cb = () => {
9
+ }) => {
10
+ Object.keys(obj).forEach((key) => {
11
+ if (obj[key] !== null && typeof obj[key] === "object") {
12
+ traverseSchema(definitions, obj[key], (value) => {
13
+ obj[key] = value;
14
+ });
15
+ } else {
16
+ if (key === "$ref") {
17
+ const { $id, $path } = parse$ref(obj[key]);
18
+ const { resolvedObject } = resolvePayload($path, definitions[$id]);
19
+ if (resolvedObject) {
20
+ cb(resolvedObject);
21
+ } else {
22
+ throw new Error(`Could not resolve '${obj[key]}' $ref`);
23
+ }
24
+ }
25
+ }
26
+ });
27
+ };
2
28
  var resolvePayload = (path, obj) => {
3
29
  let resolvedObject = path.reduce(function(prev, curr) {
4
30
  return prev ? prev[curr] : void 0;
@@ -6,12 +32,12 @@ var resolvePayload = (path, obj) => {
6
32
  return { metadata: getMetaData(resolvedObject), resolvedObject };
7
33
  };
8
34
  var pathToString = (path) => {
9
- return path.length == 0 ? `.` : path.map((item) => typeof item === "number" ? `[${item}]` : `.${item}`).join("");
35
+ return path.length == 0 ? `/` : path.map((item) => `/${item}`).join("");
10
36
  };
11
37
  var getMetaData = (value) => {
12
38
  let size = 0;
13
39
  let types = /* @__PURE__ */ new Set([]);
14
- let properties = /* @__PURE__ */ new Set([]);
40
+ let properties = [];
15
41
  if (value === null) {
16
42
  types.add("null");
17
43
  } else if (typeof value == "string") {
@@ -35,7 +61,7 @@ var getMetaData = (value) => {
35
61
  } else if (typeof value == "object") {
36
62
  types.add("object");
37
63
  size = Object.keys(value).length;
38
- properties = new Set(Object.keys(value));
64
+ properties = Object.keys(value);
39
65
  }
40
66
  return { types, size, properties };
41
67
  };
@@ -44,20 +70,50 @@ var getMetaData = (value) => {
44
70
  var Cabidela = class {
45
71
  schema;
46
72
  options;
73
+ definitions = {};
47
74
  constructor(schema, options) {
48
75
  this.schema = schema;
49
76
  this.options = {
50
77
  fullErrors: true,
78
+ subSchemas: [],
51
79
  applyDefaults: false,
52
80
  errorMessages: false,
53
81
  ...options || {}
54
82
  };
83
+ if (this.schema.hasOwnProperty("$defs")) {
84
+ this.definitions["$defs"] = this.schema["$defs"];
85
+ delete this.schema["$defs"];
86
+ }
87
+ if (this.options.subSchemas.length > 0) {
88
+ for (const subSchema of this.options.subSchemas) {
89
+ this.addSchema(subSchema, false);
90
+ }
91
+ traverseSchema(this.definitions, this.schema);
92
+ }
55
93
  }
56
94
  setSchema(schema) {
57
95
  this.schema = schema;
58
96
  }
97
+ addSchema(subSchema, combine = true) {
98
+ if (subSchema.hasOwnProperty("$id")) {
99
+ const url = URL.parse(subSchema["$id"]);
100
+ if (url) {
101
+ this.definitions[url.pathname.split("/").slice(-1)[0]] = subSchema;
102
+ } else {
103
+ throw new Error(
104
+ "subSchemas need a valid retrieval URI $id https://json-schema.org/understanding-json-schema/structuring#retrieval-uri"
105
+ );
106
+ }
107
+ } else {
108
+ throw new Error("subSchemas need $id https://json-schema.org/understanding-json-schema/structuring#id");
109
+ }
110
+ if (combine == true) traverseSchema(this.definitions, this.schema);
111
+ }
112
+ getSchema() {
113
+ return this.schema;
114
+ }
59
115
  setOptions(options) {
60
- this.options = options;
116
+ this.options = { ...this.options, ...options };
61
117
  }
62
118
  throw(message, needle) {
63
119
  const error = `${message}${this.options.fullErrors && needle.absorvErrors !== true && needle.errors.size > 0 ? `: ${Array.from(needle.errors).join(", ")}` : ``}`;
@@ -66,7 +122,9 @@ var Cabidela = class {
66
122
  parseAdditionalProperties(needle, contextAdditionalProperties, contextEvaluatedProperties) {
67
123
  let matchCount = 0;
68
124
  const { metadata, resolvedObject } = resolvePayload(needle.path, needle.payload);
69
- const unevaluatedProperties = metadata.properties.difference(contextEvaluatedProperties);
125
+ const unevaluatedProperties = new Set(
126
+ metadata.properties.map((r) => pathToString([...needle.path, r]))
127
+ ).difference(contextEvaluatedProperties);
70
128
  if (contextAdditionalProperties === false) {
71
129
  if (unevaluatedProperties.size > 0) {
72
130
  this.throw(
@@ -81,14 +139,15 @@ var Cabidela = class {
81
139
  } else {
82
140
  for (let property of unevaluatedProperties) {
83
141
  if (this.parseSubSchema({
84
- path: [property],
142
+ path: [property.split("/").slice(-1)[0]],
85
143
  schema: contextAdditionalProperties,
86
144
  payload: resolvedObject,
87
145
  evaluatedProperties: /* @__PURE__ */ new Set(),
88
- errors: /* @__PURE__ */ new Set()
146
+ errors: /* @__PURE__ */ new Set(),
147
+ defaultsCallbacks: []
89
148
  })) {
90
149
  matchCount++;
91
- needle.evaluatedProperties.add(property);
150
+ needle.evaluatedProperties.add(pathToString([property]));
92
151
  }
93
152
  }
94
153
  }
@@ -116,12 +175,13 @@ var Cabidela = class {
116
175
  let matchCount = 0;
117
176
  if (needle.schema.hasOwnProperty("properties")) {
118
177
  for (let property in needle.schema.properties) {
119
- if (this.parseSubSchema({
178
+ const matches = this.parseSubSchema({
120
179
  ...needle,
121
180
  path: [...needle.path, property],
122
181
  schema: needle.schema.properties[property]
123
- })) {
124
- localEvaluatedProperties.add(property);
182
+ });
183
+ if (matches > 0) {
184
+ localEvaluatedProperties.add(pathToString([...needle.path, property]));
125
185
  matchCount++;
126
186
  }
127
187
  }
@@ -142,27 +202,37 @@ var Cabidela = class {
142
202
  );
143
203
  }
144
204
  if (needle.schema.hasOwnProperty("required")) {
145
- if (new Set(needle.schema.required).difference(needle.evaluatedProperties.union(localEvaluatedProperties)).size > 0) {
146
- this.throw(`required properties at '${pathToString(needle.path)}' is '${needle.schema.required}'`, needle);
205
+ if (new Set(needle.schema.required.map((r) => pathToString([...needle.path, r]))).difference(
206
+ needle.evaluatedProperties.union(localEvaluatedProperties)
207
+ ).size > 0) {
208
+ this.throw(`required properties at '${pathToString(needle.path)}' are '${needle.schema.required}'`, needle);
147
209
  }
148
210
  }
149
211
  return matchCount ? true : false;
150
212
  }
151
213
  parseList(list, needle, breakCondition) {
152
214
  let rounds = 0;
215
+ const defaultsCallbacks = [];
153
216
  for (let option in list) {
154
217
  try {
155
- rounds += this.parseSubSchema({
218
+ const matches = this.parseSubSchema({
156
219
  ...needle,
157
220
  schema: { type: needle.schema.type, ...list[option] },
158
- carryProperties: true,
159
- absorvErrors: true
221
+ carryProperties: false,
222
+ absorvErrors: true,
223
+ deferredApplyDefaults: true
160
224
  });
225
+ rounds += matches;
161
226
  if (breakCondition && breakCondition(rounds)) break;
227
+ defaultsCallbacks.push(...needle.defaultsCallbacks);
228
+ needle.defaultsCallbacks = [];
162
229
  } catch (e) {
163
230
  needle.errors.add(e.message);
231
+ needle.defaultsCallbacks = [];
164
232
  }
165
233
  }
234
+ for (const callback of defaultsCallbacks) callback();
235
+ needle.defaultsCallbacks = [];
166
236
  return rounds;
167
237
  }
168
238
  // Parses a JSON Schema sub-schema object - reentrant
@@ -170,10 +240,25 @@ var Cabidela = class {
170
240
  if (needle.schema == void 0) {
171
241
  this.throw(`No schema for path '${pathToString(needle.path)}'`, needle);
172
242
  }
243
+ if (needle.schema.hasOwnProperty("not")) {
244
+ let pass = false;
245
+ try {
246
+ this.parseSubSchema({
247
+ ...needle,
248
+ schema: needle.schema.not
249
+ });
250
+ } catch (e) {
251
+ pass = true;
252
+ }
253
+ if (pass == false) {
254
+ this.throw(`not at '${pathToString(needle.path)}' not met`, needle);
255
+ }
256
+ }
173
257
  if (needle.schema.hasOwnProperty("oneOf")) {
174
- if (this.parseList(needle.schema.oneOf, needle) !== 1) {
258
+ const rounds = this.parseList(needle.schema.oneOf, needle, (r) => r !== 1);
259
+ if (rounds !== 1) {
175
260
  if (needle.path.length == 0) {
176
- this.throw(`oneOf at '${pathToString(needle.path)}' not met`, needle);
261
+ this.throw(`oneOf at '${pathToString(needle.path)}' not met, ${rounds} matches`, needle);
177
262
  }
178
263
  return 0;
179
264
  }
@@ -244,7 +329,7 @@ var Cabidela = class {
244
329
  }
245
330
  if (needle.schema.hasOwnProperty("type") && !metadata.types.has(needle.schema.type)) {
246
331
  this.throw(
247
- `Type mismatch of '${pathToString(needle.path)}', '${needle.schema.type}' not in ${JSON.stringify(Array.from(metadata.types))}`,
332
+ `Type mismatch of '${pathToString(needle.path)}', '${needle.schema.type}' not in ${Array.from(metadata.types).map((e) => `'${e}'`).join(",")}`,
248
333
  needle
249
334
  );
250
335
  }
@@ -281,28 +366,44 @@ var Cabidela = class {
281
366
  break;
282
367
  }
283
368
  }
369
+ if (needle.schema.hasOwnProperty("pattern")) {
370
+ let passes = false;
371
+ try {
372
+ if (new RegExp(needle.schema.pattern).test(resolvedObject)) passes = true;
373
+ } catch (e) {
374
+ }
375
+ if (!passes) this.throw(`'${pathToString(needle.path)}' failed test ${needle.schema.pattern} patttern`, needle);
376
+ }
284
377
  if (needle.carryProperties) {
285
- needle.evaluatedProperties.add(needle.path[needle.path.length - 1]);
378
+ needle.evaluatedProperties.add(pathToString(needle.path));
286
379
  }
287
380
  return 1;
288
381
  }
289
382
  if (this.options.applyDefaults === true && needle.schema.hasOwnProperty("default")) {
290
- needle.path.reduce(function(prev, curr, index) {
291
- if (prev[curr] === void 0) {
292
- prev[curr] = {};
293
- }
294
- if (index == needle.path.length - 1) {
295
- prev[curr] = needle.schema.default;
296
- needle.evaluatedProperties.add(needle.path[needle.path.length - 1]);
297
- }
298
- return prev ? prev[curr] : void 0;
299
- }, needle.payload);
383
+ const applyDefaults = () => {
384
+ needle.path.reduce(function(prev, curr, index) {
385
+ if (prev[curr] === void 0) {
386
+ prev[curr] = {};
387
+ }
388
+ if (index == needle.path.length - 1) {
389
+ prev[curr] = needle.schema.default;
390
+ needle.evaluatedProperties.add(pathToString(needle.path));
391
+ }
392
+ return prev ? prev[curr] : void 0;
393
+ }, needle.payload);
394
+ };
395
+ if (needle.deferredApplyDefaults === true) {
396
+ needle.defaultsCallbacks.push(applyDefaults);
397
+ } else {
398
+ applyDefaults();
399
+ }
300
400
  }
301
401
  return 0;
302
402
  }
303
403
  validate(payload) {
304
404
  const needle = {
305
405
  errors: /* @__PURE__ */ new Set(),
406
+ defaultsCallbacks: [],
306
407
  evaluatedProperties: /* @__PURE__ */ new Set(),
307
408
  path: [],
308
409
  schema: this.schema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/cabidela",
3
- "version": "0.0.18",
3
+ "version": "0.1.1",
4
4
  "description": "Cabidela is a small, fast, eval-less, Cloudflare Workers compatible, dynamic JSON Schema validator",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -24,7 +24,8 @@
24
24
  "files": [
25
25
  "dist",
26
26
  "LICENSE",
27
- "README.md"
27
+ "README.md",
28
+ "CHANGELOG.md"
28
29
  ],
29
30
  "repository": {
30
31
  "type": "git",