toolcraft-schema 0.0.232 → 0.0.233
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/dist/clone-default.d.ts +1 -0
- package/dist/clone-default.js +26 -0
- package/dist/index.compile-check.js +1 -0
- package/dist/index.d.ts +18 -9
- package/dist/index.js +46 -8
- package/dist/json-schema/compiler.d.ts +1 -0
- package/dist/json-schema/compiler.js +6 -0
- package/dist/json-schema/evaluate.js +6 -3
- package/dist/json-schema/normalize-nullability.d.ts +1 -0
- package/dist/json-schema/normalize-nullability.js +69 -0
- package/dist/json-schema/types.d.ts +1 -0
- package/dist/json.d.ts +4 -1
- package/dist/json.js +57 -1
- package/dist/native-json-schema.d.ts +10 -0
- package/dist/native-json-schema.js +11 -0
- package/dist/validate.d.ts +5 -1
- package/dist/validate.js +61 -47
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cloneDefaultValue<Value>(value: Value): Value;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function cloneDefaultValue(value) {
|
|
2
|
+
const copies = new WeakMap();
|
|
3
|
+
function copy(current) {
|
|
4
|
+
if (current === null || typeof current !== "object")
|
|
5
|
+
return current;
|
|
6
|
+
const existing = copies.get(current);
|
|
7
|
+
if (existing !== undefined)
|
|
8
|
+
return existing;
|
|
9
|
+
const prototype = Object.getPrototypeOf(current);
|
|
10
|
+
if (!Array.isArray(current) && prototype !== Object.prototype && prototype !== null) {
|
|
11
|
+
return current;
|
|
12
|
+
}
|
|
13
|
+
const result = Array.isArray(current) ? new Array(current.length) : Object.create(prototype);
|
|
14
|
+
copies.set(current, result);
|
|
15
|
+
for (const key of Object.keys(current)) {
|
|
16
|
+
Object.defineProperty(result, key, {
|
|
17
|
+
value: copy(current[key]),
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
return copy(value);
|
|
26
|
+
}
|
|
@@ -3,6 +3,7 @@ const ignoredStringSchema = S.String({ description: "Name", default: "guest" });
|
|
|
3
3
|
const ignoredNumberSchema = S.Number({ description: "Count", default: 1 });
|
|
4
4
|
const ignoredBooleanSchema = S.Boolean({ description: "Enabled", default: false });
|
|
5
5
|
const ignoredEnumSchema = S.Enum(["admin", "user"], { default: "admin" });
|
|
6
|
+
const ignoredNullEnumSchema = S.Enum([null]);
|
|
6
7
|
const ignoredIntegerEnumSchema = S.Enum([1, 2], { jsonType: "integer" });
|
|
7
8
|
const ignoredArraySchema = S.Array(S.String(), { default: ["a"] });
|
|
8
9
|
const ignoredObjectSchema = S.Object({
|
package/dist/index.d.ts
CHANGED
|
@@ -2,17 +2,16 @@ import { Json } from "./json.js";
|
|
|
2
2
|
import { OneOf } from "./oneof.js";
|
|
3
3
|
import { Record as RecordBuilder } from "./record.js";
|
|
4
4
|
import { Union } from "./union.js";
|
|
5
|
-
import { validate } from "./validate.js";
|
|
5
|
+
import { isPlainRecord, validate } from "./validate.js";
|
|
6
6
|
import type { JsonValue, JsonValueSchema } from "./json.js";
|
|
7
7
|
import type { JsonSchemaDocument, JsonSchemaDocumentOptions } from "./json-schema-document.js";
|
|
8
8
|
import type { OneOfSchema } from "./oneof.js";
|
|
9
9
|
import type { RecordSchema } from "./record.js";
|
|
10
10
|
import type { UnionSchema } from "./union.js";
|
|
11
|
-
import type { ValidationIssue, ValidationResult } from "./validate.js";
|
|
12
|
-
type JsonSchemaType = "string" | "number" | "integer" | "boolean" | "array" | "object";
|
|
11
|
+
import type { ValidationIssue, ValidationOptions, ValidationResult } from "./validate.js";
|
|
12
|
+
type JsonSchemaType = "string" | "number" | "integer" | "boolean" | "array" | "object" | "null";
|
|
13
13
|
type SchemaKind = "string" | "number" | "boolean" | "enum" | "array" | "object" | "optional" | "oneOf" | "union" | "record" | "json";
|
|
14
|
-
type EnumValue = string | number | boolean;
|
|
15
|
-
type JsonSchemaEnumValue = EnumValue | null;
|
|
14
|
+
type EnumValue = string | number | boolean | null;
|
|
16
15
|
type NumberJsonType = "number" | "integer";
|
|
17
16
|
type NonEmptyReadonlyArray<T> = readonly [T, ...T[]];
|
|
18
17
|
type ObjectShape = Record<string, AnySchema>;
|
|
@@ -101,10 +100,13 @@ export interface SchemaBase<TKind extends SchemaKind, TStatic> {
|
|
|
101
100
|
}
|
|
102
101
|
export interface JsonSchema {
|
|
103
102
|
additionalProperties?: boolean | JsonSchema;
|
|
104
|
-
|
|
103
|
+
allOf?: JsonSchema[];
|
|
104
|
+
anyOf?: JsonSchema[];
|
|
105
|
+
type?: JsonSchemaType | JsonSchemaType[];
|
|
105
106
|
description?: string;
|
|
106
107
|
default?: unknown;
|
|
107
|
-
enum?: ReadonlyArray<
|
|
108
|
+
enum?: ReadonlyArray<JsonValue>;
|
|
109
|
+
const?: JsonValue;
|
|
108
110
|
format?: string;
|
|
109
111
|
items?: JsonSchema;
|
|
110
112
|
maxItems?: number;
|
|
@@ -114,6 +116,7 @@ export interface JsonSchema {
|
|
|
114
116
|
minimum?: number;
|
|
115
117
|
minLength?: number;
|
|
116
118
|
nullable?: boolean;
|
|
119
|
+
not?: JsonSchema;
|
|
117
120
|
oneOf?: JsonSchema[];
|
|
118
121
|
pattern?: string;
|
|
119
122
|
properties?: Record<string, JsonSchema>;
|
|
@@ -177,8 +180,14 @@ export declare const S: {
|
|
|
177
180
|
};
|
|
178
181
|
export declare function toJsonSchema(schema: AnySchema): JsonSchema;
|
|
179
182
|
export declare function toJsonSchemaDocument(schema: AnySchema, options?: JsonSchemaDocumentOptions): JsonSchemaDocument;
|
|
180
|
-
export { Json, OneOf, RecordBuilder as Record, Union, validate };
|
|
183
|
+
export { Json, OneOf, RecordBuilder as Record, Union, isPlainRecord, validate };
|
|
184
|
+
export { isJsonValue } from "./json.js";
|
|
185
|
+
export { cloneDefaultValue } from "./clone-default.js";
|
|
186
|
+
export { unicodeLength } from "./json-schema/utils.js";
|
|
181
187
|
export { compileJsonSchema, formatIssues } from "./json-schema/index.js";
|
|
188
|
+
export { normalizeLegacyNullability } from "./json-schema/normalize-nullability.js";
|
|
189
|
+
export { withJsonSchema, nativeJsonSchema } from "./native-json-schema.js";
|
|
190
|
+
export type { NativeSchema } from "./native-json-schema.js";
|
|
182
191
|
export type { CompileJsonSchemaOptions, CompiledJsonSchema } from "./json-schema/index.js";
|
|
183
192
|
export type { JsonSchemaDocument, JsonSchemaDocumentOptions } from "./json-schema-document.js";
|
|
184
|
-
export type { JsonValue, JsonValueSchema, OneOfSchema, RecordSchema, UnionSchema, ValidationIssue, ValidationResult };
|
|
193
|
+
export type { JsonValue, JsonValueSchema, OneOfSchema, RecordSchema, UnionSchema, ValidationIssue, ValidationOptions, ValidationResult };
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { Json } from "./json.js";
|
|
2
|
+
import { nativeJsonSchema } from "./native-json-schema.js";
|
|
3
|
+
import { cloneDefaultValue } from "./clone-default.js";
|
|
2
4
|
import { createJsonSchemaDocument } from "./json-schema-document.js";
|
|
3
5
|
import { OneOf } from "./oneof.js";
|
|
4
6
|
import { Record as RecordBuilder } from "./record.js";
|
|
5
7
|
import { Union } from "./union.js";
|
|
6
|
-
import { validate } from "./validate.js";
|
|
8
|
+
import { isPlainRecord, validate } from "./validate.js";
|
|
7
9
|
function withMetadata(schema, jsonSchema) {
|
|
8
10
|
if (schema.description !== undefined) {
|
|
9
11
|
jsonSchema.description = schema.description;
|
|
10
12
|
}
|
|
11
13
|
if (schema.default !== undefined) {
|
|
12
|
-
jsonSchema.default = schema.default;
|
|
14
|
+
jsonSchema.default = cloneDefaultValue(schema.default);
|
|
13
15
|
}
|
|
14
16
|
if (schema.nullable === true) {
|
|
15
|
-
jsonSchema.
|
|
17
|
+
if (jsonSchema.type !== undefined) {
|
|
18
|
+
jsonSchema.type = [...new Set([...(Array.isArray(jsonSchema.type) ? jsonSchema.type : [jsonSchema.type]), "null"])];
|
|
19
|
+
}
|
|
20
|
+
if (jsonSchema.oneOf !== undefined) {
|
|
21
|
+
jsonSchema.oneOf.push({ enum: [null] });
|
|
22
|
+
}
|
|
16
23
|
}
|
|
17
24
|
return jsonSchema;
|
|
18
25
|
}
|
|
@@ -59,6 +66,8 @@ function getEnumJsonType(values) {
|
|
|
59
66
|
return undefined;
|
|
60
67
|
}
|
|
61
68
|
const firstType = typeof firstValue;
|
|
69
|
+
if (firstValue === null && values.every((value) => value === null))
|
|
70
|
+
return "null";
|
|
62
71
|
const isSinglePrimitiveType = values.every((value) => typeof value === firstType);
|
|
63
72
|
if (!isSinglePrimitiveType) {
|
|
64
73
|
return undefined;
|
|
@@ -124,8 +133,22 @@ function unwrapOptional(schema) {
|
|
|
124
133
|
}
|
|
125
134
|
return schema;
|
|
126
135
|
}
|
|
136
|
+
function toObjectBranchJsonSchema(schema) {
|
|
137
|
+
const objectSchema = { ...schema, nullable: false };
|
|
138
|
+
const branchJsonSchema = toJsonSchema(objectSchema);
|
|
139
|
+
if (branchJsonSchema.default === null) {
|
|
140
|
+
delete branchJsonSchema.default;
|
|
141
|
+
}
|
|
142
|
+
return branchJsonSchema;
|
|
143
|
+
}
|
|
127
144
|
function withInjectedDiscriminator(schema, discriminator, branchName) {
|
|
128
|
-
const branchJsonSchema =
|
|
145
|
+
const branchJsonSchema = toObjectBranchJsonSchema(schema);
|
|
146
|
+
if (branchJsonSchema.default !== undefined) {
|
|
147
|
+
branchJsonSchema.default = {
|
|
148
|
+
...branchJsonSchema.default,
|
|
149
|
+
[discriminator]: branchName
|
|
150
|
+
};
|
|
151
|
+
}
|
|
129
152
|
const properties = {
|
|
130
153
|
...(branchJsonSchema.properties ?? {}),
|
|
131
154
|
[discriminator]: {
|
|
@@ -227,6 +250,9 @@ export const S = {
|
|
|
227
250
|
};
|
|
228
251
|
export function toJsonSchema(schema) {
|
|
229
252
|
const unwrappedSchema = unwrapOptional(schema);
|
|
253
|
+
const native = unwrappedSchema[nativeJsonSchema];
|
|
254
|
+
if (native !== undefined)
|
|
255
|
+
return structuredClone(native.document);
|
|
230
256
|
switch (unwrappedSchema.kind) {
|
|
231
257
|
case "string":
|
|
232
258
|
return withStringMetadata(unwrappedSchema, { type: "string" });
|
|
@@ -277,19 +303,31 @@ export function toJsonSchema(schema) {
|
|
|
277
303
|
});
|
|
278
304
|
case "union":
|
|
279
305
|
return withMetadata(unwrappedSchema, {
|
|
280
|
-
oneOf: unwrappedSchema.branches.map((branchSchema) =>
|
|
306
|
+
oneOf: unwrappedSchema.branches.map((branchSchema) => toObjectBranchJsonSchema(branchSchema))
|
|
281
307
|
});
|
|
282
308
|
case "record":
|
|
283
309
|
return withMetadata(unwrappedSchema, {
|
|
284
310
|
type: "object",
|
|
285
311
|
additionalProperties: toJsonSchema(unwrappedSchema.value)
|
|
286
312
|
});
|
|
287
|
-
case "json":
|
|
288
|
-
|
|
313
|
+
case "json": {
|
|
314
|
+
const constraints = {
|
|
315
|
+
...(unwrappedSchema.const !== undefined ? { const: unwrappedSchema.const } : {}),
|
|
316
|
+
...(unwrappedSchema.enum !== undefined ? { enum: unwrappedSchema.enum } : {})
|
|
317
|
+
};
|
|
318
|
+
return withMetadata(unwrappedSchema, unwrappedSchema.nullable === true && Object.keys(constraints).length > 0
|
|
319
|
+
? { anyOf: [constraints, { type: "null" }] }
|
|
320
|
+
: constraints);
|
|
321
|
+
}
|
|
289
322
|
}
|
|
290
323
|
}
|
|
291
324
|
export function toJsonSchemaDocument(schema, options = {}) {
|
|
292
325
|
return createJsonSchemaDocument(toJsonSchema(schema), options);
|
|
293
326
|
}
|
|
294
|
-
export { Json, OneOf, RecordBuilder as Record, Union, validate };
|
|
327
|
+
export { Json, OneOf, RecordBuilder as Record, Union, isPlainRecord, validate };
|
|
328
|
+
export { isJsonValue } from "./json.js";
|
|
329
|
+
export { cloneDefaultValue } from "./clone-default.js";
|
|
330
|
+
export { unicodeLength } from "./json-schema/utils.js";
|
|
295
331
|
export { compileJsonSchema, formatIssues } from "./json-schema/index.js";
|
|
332
|
+
export { normalizeLegacyNullability } from "./json-schema/normalize-nullability.js";
|
|
333
|
+
export { withJsonSchema, nativeJsonSchema } from "./native-json-schema.js";
|
|
@@ -173,6 +173,11 @@ function validateSchemaObject(schema, dialect) {
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
export function compileGraph(schema, options = {}) {
|
|
176
|
+
const formats = new Map(Object.entries(options.formats ?? {}));
|
|
177
|
+
for (const [name, validator] of formats) {
|
|
178
|
+
if (typeof validator !== "function")
|
|
179
|
+
throw new Error(`Format validator must be a function: ${name}`);
|
|
180
|
+
}
|
|
176
181
|
if (!isSchema(schema)) {
|
|
177
182
|
throw new Error("JSON Schema must be a boolean or object.");
|
|
178
183
|
}
|
|
@@ -377,6 +382,7 @@ export function compileGraph(schema, options = {}) {
|
|
|
377
382
|
}
|
|
378
383
|
}
|
|
379
384
|
return {
|
|
385
|
+
formats,
|
|
380
386
|
root,
|
|
381
387
|
locations,
|
|
382
388
|
resources,
|
|
@@ -39,6 +39,12 @@ function evaluateNode(graph, node, value, context) {
|
|
|
39
39
|
if (node.validationVocabulary) {
|
|
40
40
|
results.push(...evaluateValidationKeywords(node, schema, value, nextContext.instancePath));
|
|
41
41
|
}
|
|
42
|
+
if (typeof value === "string" && typeof schema.format === "string") {
|
|
43
|
+
const validator = graph.formats.get(schema.format);
|
|
44
|
+
if (validator !== undefined && validator(value) !== true) {
|
|
45
|
+
results.push(invalidResult(issue(nextContext.instancePath, `format ${schema.format}`, value, `must match format ${schema.format}`)));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
42
48
|
const merged = mergeResults(results);
|
|
43
49
|
if (isObject(value)) {
|
|
44
50
|
evaluateUnevaluatedProperties(graph, node, schema, value, nextContext, merged);
|
|
@@ -246,9 +252,6 @@ function evaluateArrayApplicators(graph, node, schema, value, context) {
|
|
|
246
252
|
}
|
|
247
253
|
function evaluateValidationKeywords(node, schema, value, path) {
|
|
248
254
|
const results = [];
|
|
249
|
-
if (schema.nullable === true && value === null) {
|
|
250
|
-
return results;
|
|
251
|
-
}
|
|
252
255
|
const types = typeof schema.type === "string" ? [schema.type] : Array.isArray(schema.type) ? schema.type : [];
|
|
253
256
|
if (types.length > 0 &&
|
|
254
257
|
!types.some((type) => typeof type === "string" && typeMatches(type, value))) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeLegacyNullability(source: object): Record<string, unknown>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { dialectFor } from "./utils.js";
|
|
2
|
+
const schemaMaps = new Set(["properties", "$defs", "definitions", "patternProperties", "dependentSchemas", "dependencies"]);
|
|
3
|
+
const schemaArrays = new Set(["allOf", "anyOf", "oneOf", "prefixItems", "items"]);
|
|
4
|
+
const schemaValues = new Set(["items", "additionalProperties", "additionalItems", "contains", "not", "if", "then", "else", "propertyNames", "unevaluatedProperties", "unevaluatedItems"]);
|
|
5
|
+
const schemaIdentity = new Set(["$id", "id", "$schema", "$anchor", "$dynamicAnchor", "$defs", "definitions", "description", "default"]);
|
|
6
|
+
export function normalizeLegacyNullability(source) {
|
|
7
|
+
const locations = new Map();
|
|
8
|
+
const resourceIds = new Map();
|
|
9
|
+
const references = [];
|
|
10
|
+
const pointer = (path) => path.length === 0 ? "" : `/${path.map((part) => part.split("~").join("~0").split("/").join("~1")).join("/")}`;
|
|
11
|
+
const walk = (node, before, after, resourceBefore, resourceAfter, base = "https://toolcraft.invalid/schema", inheritedDialect = "draft2020-12") => {
|
|
12
|
+
if (typeof node !== "object" || node === null || Array.isArray(node))
|
|
13
|
+
return structuredClone(node);
|
|
14
|
+
const object = node;
|
|
15
|
+
const dialect = dialectFor(object, inheritedDialect);
|
|
16
|
+
const identifier = dialect === "draft7" ? object.$id ?? object.id : object.$id;
|
|
17
|
+
if (typeof identifier === "string") {
|
|
18
|
+
resourceBefore = before;
|
|
19
|
+
resourceAfter = after;
|
|
20
|
+
base = new URL(identifier, base).href.split("#", 1)[0];
|
|
21
|
+
}
|
|
22
|
+
const resource = pointer(resourceBefore);
|
|
23
|
+
resourceIds.set(base, resource);
|
|
24
|
+
locations.set(`${resource}|${pointer(before.slice(resourceBefore.length))}`, pointer(after.slice(resourceAfter.length)));
|
|
25
|
+
const result = {};
|
|
26
|
+
const validation = {};
|
|
27
|
+
const nullable = object.nullable === true;
|
|
28
|
+
for (const [key, value] of Object.entries(object)) {
|
|
29
|
+
if (key === "nullable")
|
|
30
|
+
continue;
|
|
31
|
+
const target = nullable && !schemaIdentity.has(key) ? validation : result;
|
|
32
|
+
const nextAfter = [...after, ...(target === validation ? ["anyOf", "0"] : []), key];
|
|
33
|
+
let converted;
|
|
34
|
+
if (schemaMaps.has(key) && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
35
|
+
converted = Object.fromEntries(Object.entries(value).map(([name, child]) => [name,
|
|
36
|
+
walk(child, [...before, key, name], [...nextAfter, name], resourceBefore, resourceAfter, base, dialect)]));
|
|
37
|
+
}
|
|
38
|
+
else if (schemaArrays.has(key) && Array.isArray(value)) {
|
|
39
|
+
converted = value.map((child, index) => walk(child, [...before, key, String(index)], [...nextAfter, String(index)], resourceBefore, resourceAfter, base, dialect));
|
|
40
|
+
}
|
|
41
|
+
else if (schemaValues.has(key)) {
|
|
42
|
+
converted = walk(value, [...before, key], nextAfter, resourceBefore, resourceAfter, base, dialect);
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
converted = structuredClone(value);
|
|
46
|
+
Object.defineProperty(target, key, { value: converted, enumerable: true, configurable: true, writable: true });
|
|
47
|
+
if ((key === "$ref" || key === "$dynamicRef") && typeof value === "string")
|
|
48
|
+
references.push({ holder: target, key, value, base });
|
|
49
|
+
}
|
|
50
|
+
if (nullable)
|
|
51
|
+
result.anyOf = [validation, { type: "null" }];
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
const normalized = walk(source, [], [], [], []);
|
|
55
|
+
for (const reference of references) {
|
|
56
|
+
const resolved = new URL(reference.value, reference.base);
|
|
57
|
+
if (!resolved.hash.startsWith("#/"))
|
|
58
|
+
continue;
|
|
59
|
+
const resource = resourceIds.get(resolved.href.slice(0, -resolved.hash.length));
|
|
60
|
+
if (resource === undefined)
|
|
61
|
+
continue;
|
|
62
|
+
const original = decodeURIComponent(resolved.hash.slice(1));
|
|
63
|
+
const destination = locations.get(`${resource}|${original}`);
|
|
64
|
+
if (destination !== undefined && destination !== original) {
|
|
65
|
+
reference.holder[reference.key] = `${reference.value.slice(0, reference.value.indexOf("#") + 1)}${destination}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { ValidationIssue, ValidationResult } from "../validate.js";
|
|
|
2
2
|
export type JsonSchemaRegistry = Record<string, unknown>;
|
|
3
3
|
export interface CompileJsonSchemaOptions {
|
|
4
4
|
registry?: JsonSchemaRegistry;
|
|
5
|
+
formats?: Readonly<Record<string, (value: string) => boolean>>;
|
|
5
6
|
}
|
|
6
7
|
export interface CompiledJsonSchema {
|
|
7
8
|
validate(value: unknown): ValidationResult<unknown>;
|
package/dist/json.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export type JsonValue = JsonPrimitive | {
|
|
|
5
5
|
} | JsonValue[];
|
|
6
6
|
export interface JsonValueSchema extends SchemaBase<"json", JsonValue> {
|
|
7
7
|
readonly kind: "json";
|
|
8
|
+
readonly const?: JsonValue;
|
|
9
|
+
readonly enum?: readonly JsonValue[];
|
|
8
10
|
}
|
|
9
|
-
export declare function Json(): JsonValueSchema;
|
|
11
|
+
export declare function Json(options?: Omit<JsonValueSchema, "kind">): JsonValueSchema;
|
|
12
|
+
export declare function isJsonValue(value: unknown): boolean;
|
|
10
13
|
export {};
|
package/dist/json.js
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
|
-
export function Json() {
|
|
1
|
+
export function Json(options = {}) {
|
|
2
2
|
return {
|
|
3
3
|
kind: "json",
|
|
4
|
+
...options,
|
|
4
5
|
};
|
|
5
6
|
}
|
|
7
|
+
export function isJsonValue(value) {
|
|
8
|
+
const ancestors = new Set();
|
|
9
|
+
let nodes = 0;
|
|
10
|
+
const visit = (item, depth) => {
|
|
11
|
+
if (++nodes > 10_000 || depth > 64)
|
|
12
|
+
return false;
|
|
13
|
+
if (item === null || typeof item === "string" || typeof item === "boolean")
|
|
14
|
+
return true;
|
|
15
|
+
if (typeof item === "number")
|
|
16
|
+
return Number.isFinite(item);
|
|
17
|
+
if (typeof item !== "object" || ancestors.has(item))
|
|
18
|
+
return false;
|
|
19
|
+
if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype &&
|
|
20
|
+
Object.getPrototypeOf(item) !== null)
|
|
21
|
+
return false;
|
|
22
|
+
let hookOwner = item;
|
|
23
|
+
let prototypeDepth = 0;
|
|
24
|
+
while (hookOwner !== null) {
|
|
25
|
+
if (++prototypeDepth > 64)
|
|
26
|
+
return false;
|
|
27
|
+
const serializationHook = Object.getOwnPropertyDescriptor(hookOwner, "toJSON");
|
|
28
|
+
if (serializationHook !== undefined) {
|
|
29
|
+
if (!("value" in serializationHook) || typeof serializationHook.value === "function")
|
|
30
|
+
return false;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
hookOwner = Object.getPrototypeOf(hookOwner);
|
|
34
|
+
}
|
|
35
|
+
ancestors.add(item);
|
|
36
|
+
let valid = true;
|
|
37
|
+
if (Array.isArray(item)) {
|
|
38
|
+
if (item.length > 10_000)
|
|
39
|
+
valid = false;
|
|
40
|
+
else
|
|
41
|
+
for (let index = 0; index < item.length; index++) {
|
|
42
|
+
const property = Object.getOwnPropertyDescriptor(item, String(index));
|
|
43
|
+
if (property === undefined || !("value" in property) || !visit(property.value, depth + 1)) {
|
|
44
|
+
valid = false;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else
|
|
50
|
+
for (const key in item) {
|
|
51
|
+
const property = Object.getOwnPropertyDescriptor(item, key);
|
|
52
|
+
if (property !== undefined && (!("value" in property) || !visit(property.value, depth + 1))) {
|
|
53
|
+
valid = false;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
ancestors.delete(item);
|
|
58
|
+
return valid;
|
|
59
|
+
};
|
|
60
|
+
return visit(value, 0);
|
|
61
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnySchema, JsonSchema } from "./index.js";
|
|
2
|
+
import type { CompiledJsonSchema } from "./json-schema/types.js";
|
|
3
|
+
export declare const nativeJsonSchema: unique symbol;
|
|
4
|
+
export type NativeSchema = AnySchema & {
|
|
5
|
+
[nativeJsonSchema]?: {
|
|
6
|
+
document: JsonSchema;
|
|
7
|
+
validator: CompiledJsonSchema;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export declare function withJsonSchema<T extends AnySchema>(projection: T, document: object): T;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isJsonValue } from "./json.js";
|
|
2
|
+
import { compileJsonSchema } from "./json-schema/index.js";
|
|
3
|
+
export const nativeJsonSchema = Symbol("toolcraft.nativeJsonSchema");
|
|
4
|
+
export function withJsonSchema(projection, document) {
|
|
5
|
+
if (!isJsonValue(document) || Array.isArray(document)) {
|
|
6
|
+
throw new Error("Native JSON Schema must be a JSON object");
|
|
7
|
+
}
|
|
8
|
+
const snapshot = structuredClone(document);
|
|
9
|
+
const validator = compileJsonSchema(snapshot);
|
|
10
|
+
return { ...projection, [nativeJsonSchema]: { document: snapshot, validator } };
|
|
11
|
+
}
|
package/dist/validate.d.ts
CHANGED
|
@@ -14,4 +14,8 @@ export type ValidationResult<T> = {
|
|
|
14
14
|
ok: false;
|
|
15
15
|
issues: readonly ValidationIssue[];
|
|
16
16
|
};
|
|
17
|
-
export
|
|
17
|
+
export interface ValidationOptions {
|
|
18
|
+
defaults?: "none" | "optional" | "all";
|
|
19
|
+
}
|
|
20
|
+
export declare function validate<S extends SchemaDescriptor>(schema: S, value: unknown, options?: ValidationOptions): ValidationResult<Static<S>>;
|
|
21
|
+
export declare function isPlainRecord(value: unknown): value is Record<string, unknown>;
|
package/dist/validate.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { cloneDefaultValue } from "./clone-default.js";
|
|
2
|
+
import { isJsonValue } from "./json.js";
|
|
3
|
+
import { deepEqual, unicodeLength } from "./json-schema/utils.js";
|
|
1
4
|
import { getRequiredKeyFingerprint } from "./union.js";
|
|
5
|
+
import { nativeJsonSchema } from "./native-json-schema.js";
|
|
2
6
|
const missingValue = Symbol("missingValue");
|
|
3
|
-
export function validate(schema, value) {
|
|
4
|
-
const state = { issues: [] };
|
|
7
|
+
export function validate(schema, value, options = {}) {
|
|
8
|
+
const state = { issues: [], defaults: options.defaults ?? "optional" };
|
|
5
9
|
const result = walkSchema(schema, value, [], state);
|
|
6
10
|
if (state.issues.length > 0) {
|
|
7
11
|
return { ok: false, issues: state.issues };
|
|
@@ -12,10 +16,25 @@ function walkSchema(schema, value, path, state) {
|
|
|
12
16
|
if (schema.kind === "optional") {
|
|
13
17
|
return walkOptional(schema, value, path, state);
|
|
14
18
|
}
|
|
19
|
+
if (value === missingValue && state.defaults === "all" && schema.default !== undefined) {
|
|
20
|
+
value = cloneDefaultValue(schema.default);
|
|
21
|
+
}
|
|
15
22
|
if (value === missingValue) {
|
|
16
23
|
addIssue(state, path, expectedFor(schema), "missing", `Expected ${expectedFor(schema)} at ${formatPath(path)}`);
|
|
17
24
|
return { present: false };
|
|
18
25
|
}
|
|
26
|
+
const native = schema[nativeJsonSchema];
|
|
27
|
+
if (native !== undefined) {
|
|
28
|
+
if (!isJsonValue(value)) {
|
|
29
|
+
addExpectedIssue(state, path, "JSON value", value);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const result = native.validator.validate(value);
|
|
33
|
+
if (!result.ok)
|
|
34
|
+
state.issues.push(...result.issues.map((issue) => ({ ...issue, path: [...path, ...issue.path] })));
|
|
35
|
+
}
|
|
36
|
+
return { present: true, value };
|
|
37
|
+
}
|
|
19
38
|
if (value === null && schema.nullable === true) {
|
|
20
39
|
return { present: true, value };
|
|
21
40
|
}
|
|
@@ -39,14 +58,26 @@ function walkSchema(schema, value, path, state) {
|
|
|
39
58
|
case "record":
|
|
40
59
|
return walkRecord(schema, value, path, state);
|
|
41
60
|
case "json":
|
|
42
|
-
return walkJson(value, path, state);
|
|
61
|
+
return walkJson(schema, value, path, state);
|
|
43
62
|
}
|
|
44
63
|
}
|
|
45
64
|
function walkOptional(schema, value, path, state) {
|
|
46
65
|
if (value === missingValue || value === undefined) {
|
|
66
|
+
if (state.defaults === "none") {
|
|
67
|
+
return { present: false };
|
|
68
|
+
}
|
|
47
69
|
const defaultValue = getDefault(schema.inner);
|
|
48
70
|
if (defaultValue.present) {
|
|
49
|
-
|
|
71
|
+
let clonedDefault;
|
|
72
|
+
try {
|
|
73
|
+
clonedDefault = structuredClone(defaultValue.value);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (!(error instanceof Error) || error.name !== "DataCloneError")
|
|
77
|
+
throw error;
|
|
78
|
+
clonedDefault = cloneDefaultValue(defaultValue.value);
|
|
79
|
+
}
|
|
80
|
+
return walkSchema(schema.inner, clonedDefault, path, state);
|
|
50
81
|
}
|
|
51
82
|
return { present: false };
|
|
52
83
|
}
|
|
@@ -57,13 +88,14 @@ function walkString(schema, value, path, state) {
|
|
|
57
88
|
addExpectedIssue(state, path, "string", value);
|
|
58
89
|
return { present: true, value };
|
|
59
90
|
}
|
|
60
|
-
|
|
91
|
+
const length = unicodeLength(value);
|
|
92
|
+
if (schema.minLength !== undefined && length < schema.minLength) {
|
|
61
93
|
const expected = `string with length at least ${schema.minLength}`;
|
|
62
|
-
addIssue(state, path, expected, `string with length ${
|
|
94
|
+
addIssue(state, path, expected, `string with length ${length}`, `Expected ${expected} at ${formatPath(path)}`);
|
|
63
95
|
}
|
|
64
|
-
if (schema.maxLength !== undefined &&
|
|
96
|
+
if (schema.maxLength !== undefined && length > schema.maxLength) {
|
|
65
97
|
const expected = `string with length at most ${schema.maxLength}`;
|
|
66
|
-
addIssue(state, path, expected, `string with length ${
|
|
98
|
+
addIssue(state, path, expected, `string with length ${length}`, `Expected ${expected} at ${formatPath(path)}`);
|
|
67
99
|
}
|
|
68
100
|
if (schema.pattern !== undefined) {
|
|
69
101
|
const pattern = compilePattern(schema.pattern);
|
|
@@ -118,7 +150,8 @@ function walkArray(schema, value, path, state) {
|
|
|
118
150
|
const expected = `array with at most ${schema.maxItems} items`;
|
|
119
151
|
addIssue(state, path, expected, `array with ${value.length} items`, `Expected ${expected} at ${formatPath(path)}`);
|
|
120
152
|
}
|
|
121
|
-
const nextValue = value.
|
|
153
|
+
const nextValue = Array.from({ length: value.length }, (_item, index) => {
|
|
154
|
+
const item = value[index];
|
|
122
155
|
const result = walkSchema(schema.item, item, [...path, String(index)], state);
|
|
123
156
|
return result.present ? result.value : item;
|
|
124
157
|
});
|
|
@@ -182,15 +215,17 @@ function walkOneOf(schema, value, path, state) {
|
|
|
182
215
|
});
|
|
183
216
|
}
|
|
184
217
|
function walkUnion(schema, value, path, state) {
|
|
185
|
-
if (isPlainRecord(value)) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
218
|
+
if (!isPlainRecord(value)) {
|
|
219
|
+
addExpectedIssue(state, path, "object", value);
|
|
220
|
+
return { present: true, value };
|
|
221
|
+
}
|
|
222
|
+
const candidateBranches = schema.branches.filter((branch) => hasRequiredKeys(branch, value));
|
|
223
|
+
if (candidateBranches.length === 1 && state.defaults !== "all") {
|
|
224
|
+
return walkObject(candidateBranches[0], value, path, state);
|
|
190
225
|
}
|
|
191
226
|
const matches = [];
|
|
192
227
|
for (const branch of schema.branches) {
|
|
193
|
-
const branchState = { issues: [] };
|
|
228
|
+
const branchState = { ...state, issues: [] };
|
|
194
229
|
const result = walkObject(branch, value, path, branchState);
|
|
195
230
|
if (branchState.issues.length === 0 && result.present) {
|
|
196
231
|
matches.push({ fingerprint: getRequiredKeyFingerprint(branch), value: result.value });
|
|
@@ -229,8 +264,14 @@ function walkRecord(schema, value, path, state) {
|
|
|
229
264
|
}
|
|
230
265
|
return { present: true, value: nextValue };
|
|
231
266
|
}
|
|
232
|
-
function walkJson(value, path, state) {
|
|
267
|
+
function walkJson(schema, value, path, state) {
|
|
233
268
|
if (isJsonValue(value)) {
|
|
269
|
+
if (schema.const !== undefined && !deepEqual(value, schema.const)) {
|
|
270
|
+
addExpectedIssue(state, path, "declared JSON constant", value);
|
|
271
|
+
}
|
|
272
|
+
if (schema.enum !== undefined && !schema.enum.some((candidate) => deepEqual(value, candidate))) {
|
|
273
|
+
addExpectedIssue(state, path, "declared JSON enum value", value);
|
|
274
|
+
}
|
|
234
275
|
return { present: true, value };
|
|
235
276
|
}
|
|
236
277
|
addExpectedIssue(state, path, "JSON value", value);
|
|
@@ -245,43 +286,13 @@ function getDefault(schema) {
|
|
|
245
286
|
}
|
|
246
287
|
return { present: false };
|
|
247
288
|
}
|
|
248
|
-
function isPlainRecord(value) {
|
|
289
|
+
export function isPlainRecord(value) {
|
|
249
290
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
250
291
|
return false;
|
|
251
292
|
}
|
|
252
293
|
const prototype = Object.getPrototypeOf(value);
|
|
253
294
|
return prototype === Object.prototype || prototype === null;
|
|
254
295
|
}
|
|
255
|
-
function isJsonValue(value, ancestors = new Set()) {
|
|
256
|
-
if (value === null ||
|
|
257
|
-
typeof value === "string" ||
|
|
258
|
-
typeof value === "number" ||
|
|
259
|
-
typeof value === "boolean") {
|
|
260
|
-
return typeof value !== "number" || Number.isFinite(value);
|
|
261
|
-
}
|
|
262
|
-
if (Array.isArray(value)) {
|
|
263
|
-
if (ancestors.has(value)) {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
ancestors.add(value);
|
|
267
|
-
const result = value.every((item) => isJsonValue(item, ancestors));
|
|
268
|
-
ancestors.delete(value);
|
|
269
|
-
return result;
|
|
270
|
-
}
|
|
271
|
-
if (isPlainRecord(value)) {
|
|
272
|
-
if (ancestors.has(value)) {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
ancestors.add(value);
|
|
276
|
-
const result = Object.values(value).every((item) => isJsonValue(item, ancestors));
|
|
277
|
-
ancestors.delete(value);
|
|
278
|
-
return result;
|
|
279
|
-
}
|
|
280
|
-
return false;
|
|
281
|
-
}
|
|
282
|
-
function cloneDefault(value) {
|
|
283
|
-
return structuredClone(value);
|
|
284
|
-
}
|
|
285
296
|
function setOwnValue(target, key, value) {
|
|
286
297
|
Object.defineProperty(target, key, {
|
|
287
298
|
configurable: true,
|
|
@@ -357,6 +368,9 @@ function receivedType(value) {
|
|
|
357
368
|
if (Array.isArray(value)) {
|
|
358
369
|
return "array";
|
|
359
370
|
}
|
|
371
|
+
if (typeof value === "object" && !isPlainRecord(value)) {
|
|
372
|
+
return "non-plain object";
|
|
373
|
+
}
|
|
360
374
|
if (typeof value === "number" && Number.isInteger(value)) {
|
|
361
375
|
return "integer";
|
|
362
376
|
}
|