@cloudflare/cabidela 0.0.19 → 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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
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
+
5
12
  ## [0.0.19] - 2025-02-26
6
13
 
7
14
  ### Added
package/README.md CHANGED
@@ -180,9 +180,78 @@ example, using this schema:
180
180
  - The payload `{ moon: 10}` will be modified to `{ sun: 9000, moon: 10 }`.
181
181
  - The payload `{ saturn: 10}` will throw an error because no condition is met.
182
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
+
183
252
  ## Custom errors
184
253
 
185
- 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.
186
255
 
187
256
  ```js
188
257
  const schema = {
@@ -204,7 +273,7 @@ const payload = {
204
273
 
205
274
  cabidela.validate(payload);
206
275
  // throws "Error: prompt required"
207
- ````
276
+ ```
208
277
 
209
278
  ## Tests
210
279
 
@@ -262,7 +331,7 @@ Here are some results:
262
331
  59.75x faster than Ajv
263
332
 
264
333
  Cabidela - benchmarks/80-big-ops.bench.js > allOf, two properties
265
- 1701.95x faster than Ajv
334
+ 1701.95x faster than Ajv
266
335
 
267
336
  Cabidela - benchmarks/80-big-ops.bench.js > allOf, two objects
268
337
  1307.04x faster than Ajv
@@ -285,10 +354,7 @@ npm run benchmark
285
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:
286
355
 
287
356
  - Multiple (array of) types `{ "type": ["number", "string"] }`
288
- - Regular expressions
289
357
  - Pattern properties
290
- - `not`
291
358
  - `dependentRequired`, `dependentSchemas`, `If-Then-Else`
292
- - `$ref`, `$defs` and `$id`
293
359
 
294
360
  yet.
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>;
@@ -15,10 +16,13 @@ type SchemaNavigation = {
15
16
  defaultsCallbacks: Array<any>;
16
17
  };
17
18
  declare class Cabidela {
18
- schema: any;
19
- options: CabidelaOptions;
19
+ private schema;
20
+ private options;
21
+ private definitions;
20
22
  constructor(schema: any, options?: CabidelaOptions);
21
23
  setSchema(schema: any): void;
24
+ addSchema(subSchema: any, combine?: boolean): void;
25
+ getSchema(): any;
22
26
  setOptions(options: CabidelaOptions): void;
23
27
  throw(message: string, needle: SchemaNavigation): void;
24
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>;
@@ -15,10 +16,13 @@ type SchemaNavigation = {
15
16
  defaultsCallbacks: Array<any>;
16
17
  };
17
18
  declare class Cabidela {
18
- schema: any;
19
- options: CabidelaOptions;
19
+ private schema;
20
+ private options;
21
+ private definitions;
20
22
  constructor(schema: any, options?: CabidelaOptions);
21
23
  setSchema(schema: any): void;
24
+ addSchema(subSchema: any, combine?: boolean): void;
25
+ getSchema(): any;
22
26
  setOptions(options: CabidelaOptions): void;
23
27
  throw(message: string, needle: SchemaNavigation): void;
24
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,7 +58,7 @@ 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;
@@ -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(", ")}` : ``}`;
@@ -109,7 +165,7 @@ var Cabidela = class {
109
165
  } else {
110
166
  for (let property of unevaluatedProperties) {
111
167
  if (this.parseSubSchema({
112
- path: [property.split(".").slice(-1)[0]],
168
+ path: [property.split("/").slice(-1)[0]],
113
169
  schema: contextAdditionalProperties,
114
170
  payload: resolvedObject,
115
171
  evaluatedProperties: /* @__PURE__ */ new Set(),
@@ -175,7 +231,7 @@ var Cabidela = class {
175
231
  if (new Set(needle.schema.required.map((r) => pathToString([...needle.path, r]))).difference(
176
232
  needle.evaluatedProperties.union(localEvaluatedProperties)
177
233
  ).size > 0) {
178
- this.throw(`required properties at '${pathToString(needle.path)}' is '${needle.schema.required}'`, needle);
234
+ this.throw(`required properties at '${pathToString(needle.path)}' are '${needle.schema.required}'`, needle);
179
235
  }
180
236
  }
181
237
  return matchCount ? true : false;
@@ -210,6 +266,20 @@ var Cabidela = class {
210
266
  if (needle.schema == void 0) {
211
267
  this.throw(`No schema for path '${pathToString(needle.path)}'`, needle);
212
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
+ }
213
283
  if (needle.schema.hasOwnProperty("oneOf")) {
214
284
  const rounds = this.parseList(needle.schema.oneOf, needle, (r) => r !== 1);
215
285
  if (rounds !== 1) {
@@ -322,6 +392,14 @@ var Cabidela = class {
322
392
  break;
323
393
  }
324
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
+ }
325
403
  if (needle.carryProperties) {
326
404
  needle.evaluatedProperties.add(pathToString(needle.path));
327
405
  }
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,7 +32,7 @@ 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;
@@ -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(", ")}` : ``}`;
@@ -83,7 +139,7 @@ var Cabidela = class {
83
139
  } else {
84
140
  for (let property of unevaluatedProperties) {
85
141
  if (this.parseSubSchema({
86
- path: [property.split(".").slice(-1)[0]],
142
+ path: [property.split("/").slice(-1)[0]],
87
143
  schema: contextAdditionalProperties,
88
144
  payload: resolvedObject,
89
145
  evaluatedProperties: /* @__PURE__ */ new Set(),
@@ -149,7 +205,7 @@ var Cabidela = class {
149
205
  if (new Set(needle.schema.required.map((r) => pathToString([...needle.path, r]))).difference(
150
206
  needle.evaluatedProperties.union(localEvaluatedProperties)
151
207
  ).size > 0) {
152
- this.throw(`required properties at '${pathToString(needle.path)}' is '${needle.schema.required}'`, needle);
208
+ this.throw(`required properties at '${pathToString(needle.path)}' are '${needle.schema.required}'`, needle);
153
209
  }
154
210
  }
155
211
  return matchCount ? true : false;
@@ -184,6 +240,20 @@ var Cabidela = class {
184
240
  if (needle.schema == void 0) {
185
241
  this.throw(`No schema for path '${pathToString(needle.path)}'`, needle);
186
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
+ }
187
257
  if (needle.schema.hasOwnProperty("oneOf")) {
188
258
  const rounds = this.parseList(needle.schema.oneOf, needle, (r) => r !== 1);
189
259
  if (rounds !== 1) {
@@ -296,6 +366,14 @@ var Cabidela = class {
296
366
  break;
297
367
  }
298
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
+ }
299
377
  if (needle.carryProperties) {
300
378
  needle.evaluatedProperties.add(pathToString(needle.path));
301
379
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/cabidela",
3
- "version": "0.0.19",
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",