@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 +27 -0
- package/README.md +116 -8
- package/dist/index.d.mts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +131 -30
- package/dist/index.mjs +131 -30
- package/package.json +3 -2
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
|
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
|
-
|
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
|
17
|
-
options
|
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
|
17
|
-
options
|
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 ?
|
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 =
|
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 =
|
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 =
|
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
|
-
|
204
|
+
const matches = this.parseSubSchema({
|
146
205
|
...needle,
|
147
206
|
path: [...needle.path, property],
|
148
207
|
schema: needle.schema.properties[property]
|
149
|
-
})
|
150
|
-
|
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
|
172
|
-
|
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
|
-
|
244
|
+
const matches = this.parseSubSchema({
|
182
245
|
...needle,
|
183
246
|
schema: { type: needle.schema.type, ...list[option] },
|
184
|
-
carryProperties:
|
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
|
-
|
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 ${
|
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
|
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
|
-
|
317
|
-
|
318
|
-
prev[curr]
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
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 ?
|
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 =
|
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 =
|
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 =
|
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
|
-
|
178
|
+
const matches = this.parseSubSchema({
|
120
179
|
...needle,
|
121
180
|
path: [...needle.path, property],
|
122
181
|
schema: needle.schema.properties[property]
|
123
|
-
})
|
124
|
-
|
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
|
146
|
-
|
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
|
-
|
218
|
+
const matches = this.parseSubSchema({
|
156
219
|
...needle,
|
157
220
|
schema: { type: needle.schema.type, ...list[option] },
|
158
|
-
carryProperties:
|
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
|
-
|
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 ${
|
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
|
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
|
-
|
291
|
-
|
292
|
-
prev[curr]
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
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.
|
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",
|