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.
@@ -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
- type?: JsonSchemaType;
103
+ allOf?: JsonSchema[];
104
+ anyOf?: JsonSchema[];
105
+ type?: JsonSchemaType | JsonSchemaType[];
105
106
  description?: string;
106
107
  default?: unknown;
107
- enum?: ReadonlyArray<JsonSchemaEnumValue>;
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.nullable = true;
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 = toJsonSchema(schema);
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) => toJsonSchema(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
- return withMetadata(unwrappedSchema, {});
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";
@@ -1,5 +1,6 @@
1
1
  import type { CompileJsonSchemaOptions, SchemaNode } from "./types.js";
2
2
  export interface CompiledGraph {
3
+ formats: ReadonlyMap<string, (value: string) => boolean>;
3
4
  root: SchemaNode;
4
5
  locations: Map<string, SchemaNode>;
5
6
  resources: Map<string, SchemaNode>;
@@ -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
+ }
@@ -14,4 +14,8 @@ export type ValidationResult<T> = {
14
14
  ok: false;
15
15
  issues: readonly ValidationIssue[];
16
16
  };
17
- export declare function validate<S extends SchemaDescriptor>(schema: S, value: unknown): ValidationResult<Static<S>>;
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
- return walkSchema(schema.inner, cloneDefault(defaultValue.value), path, state);
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
- if (schema.minLength !== undefined && value.length < schema.minLength) {
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 ${value.length}`, `Expected ${expected} at ${formatPath(path)}`);
94
+ addIssue(state, path, expected, `string with length ${length}`, `Expected ${expected} at ${formatPath(path)}`);
63
95
  }
64
- if (schema.maxLength !== undefined && value.length > schema.maxLength) {
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 ${value.length}`, `Expected ${expected} at ${formatPath(path)}`);
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.map((item, index) => {
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
- const candidateBranches = schema.branches.filter((branch) => hasRequiredKeys(branch, value));
187
- if (candidateBranches.length === 1) {
188
- return walkObject(candidateBranches[0], value, path, state);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolcraft-schema",
3
- "version": "0.0.232",
3
+ "version": "0.0.233",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",