@amritk/validation 0.5.0 → 0.6.0

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.
@@ -1,157 +1,263 @@
1
1
  import { regexFlagsFor } from "@amritk/helpers/escape-regex-pattern";
2
2
  import { getDefaultValue } from "@amritk/helpers/get-default-value";
3
- import { readKey } from "@amritk/helpers/read-key";
3
+ import { declaresKey, readKey } from "@amritk/helpers/read-key";
4
4
  import { refToName } from "@amritk/helpers/ref-to-name";
5
5
  import { isSchemaObject } from "@amritk/helpers/schema-guards";
6
6
  import { NO_FORMATS } from "./enforced-keywords.js";
7
+ import { emitRepairWalk, repairWalkNameFor } from "./generate-repair-walk.js";
7
8
  import { createSubschemaMatcher } from "./generate-validator-function.js";
9
+ import { namedTest, schemaMap, subschema } from "./repair-context.js";
8
10
  const repairLookupNameFor = (ref, typeSuffix) => `repair${refToName(ref, typeSuffix)}At`;
9
- const keyTest = (nameSchema, ctx) => {
10
- const named = ctx.matcher.named(nameSchema);
11
- if (named !== null)
12
- return named;
13
- const body = ctx.matcher.test(nameSchema);
14
- if (typeof body !== "string")
15
- return body;
16
- const name = `_repairKey${ctx.next()}`;
17
- const parameter = /\binput\b/.test(body) ? "input" : "_input";
18
- const path = /\b_path\b/.test(body) ? [` const _path = ''`] : [];
19
- ctx.helpers.push([`const ${name} = (${parameter}: unknown): boolean => {`, ...path, body, `}`].join("\n"));
20
- return name;
11
+ const isObjectish = (node) => {
12
+ const type = readKey(node, "type");
13
+ if (type === "object")
14
+ return true;
15
+ return type === void 0 && (declaresKey(node, "properties") || declaresKey(node, "required"));
16
+ };
17
+ const namesItsValue = (node) => declaresKey(node, "default") || declaresKey(node, "const") || declaresKey(node, "enum");
18
+ const requiredKeys = (node) => {
19
+ const required = readKey(node, "required");
20
+ return Array.isArray(required) ? required.filter((key) => typeof key === "string") : [];
21
+ };
22
+ const dropLookup = (ctx) => {
23
+ if (ctx.drop !== null)
24
+ return ctx.drop;
25
+ ctx.drop = "_repairDrop";
26
+ ctx.helpers.push(`const _repairDrop: RepairLookup = (segments, i) => (i === segments.length ? REPAIR_DROP : null)`);
27
+ return ctx.drop;
28
+ };
29
+ const conditionalBranches = (node, ctx) => {
30
+ const ifSchema = subschema(node, "if");
31
+ if (ifSchema === void 0)
32
+ return null;
33
+ const thenSchema = subschema(node, "then");
34
+ const elseSchema = subschema(node, "else");
35
+ if (thenSchema === void 0 && elseSchema === void 0)
36
+ return null;
37
+ const test = namedTest(ifSchema, ctx);
38
+ const thenLookup = test !== false && thenSchema !== void 0 ? emitLookup(thenSchema, ctx) : null;
39
+ const elseLookup = test !== true && elseSchema !== void 0 ? emitLookup(elseSchema, ctx) : null;
40
+ return thenLookup === null && elseLookup === null ? null : { test, thenLookup, elseLookup };
41
+ };
42
+ const conditionalAnswer = (node, ctx) => {
43
+ const parts = [];
44
+ const call = (lookup) => lookup === null ? "null" : `${lookup}(segments, i, value)`;
45
+ const branches = conditionalBranches(node, ctx);
46
+ if (branches !== null) {
47
+ const { test, thenLookup, elseLookup } = branches;
48
+ if (test === true) {
49
+ if (thenLookup !== null)
50
+ parts.push(call(thenLookup));
51
+ } else if (test === false) {
52
+ if (elseLookup !== null)
53
+ parts.push(call(elseLookup));
54
+ } else {
55
+ parts.push(`(${test}(value) ? ${call(thenLookup)} : ${call(elseLookup)})`);
56
+ }
57
+ }
58
+ const allOf = readKey(node, "allOf");
59
+ if (Array.isArray(allOf)) {
60
+ for (const entry of allOf) {
61
+ const lookup = emitLookup(entry, ctx);
62
+ if (lookup !== null)
63
+ parts.push(call(lookup));
64
+ }
65
+ }
66
+ return parts.length === 0 ? null : parts.join(" ?? ");
21
67
  };
22
- const fallbackLiteral = (schema) => {
23
- const literal = getDefaultValue(schema);
24
- return literal === "undefined" ? null : literal;
68
+ const emitFill = (node, self, id, ctx) => {
69
+ const literal = namesItsValue(node) ? getDefaultValue(node) : "{}";
70
+ const base = literal.startsWith("{") ? literal : "{}";
71
+ const steps = requiredKeys(node).map((key) => `fillKey(o, ${JSON.stringify(key)}, ${self}, depth)`);
72
+ const branches = conditionalBranches(node, ctx);
73
+ if (branches !== null) {
74
+ const { test, thenLookup, elseLookup } = branches;
75
+ const answer = (lookup) => lookup === null ? "null" : `${lookup}(NO_SEGMENTS, 0, o)`;
76
+ if (test === true && thenLookup !== null)
77
+ steps.push(`fillInto(o, ${answer(thenLookup)}, depth)`);
78
+ else if (test === false && elseLookup !== null)
79
+ steps.push(`fillInto(o, ${answer(elseLookup)}, depth)`);
80
+ else if (typeof test === "string") {
81
+ steps.push(`fillInto(o, ${test}(o) ? ${answer(thenLookup)} : ${answer(elseLookup)}, depth)`);
82
+ }
83
+ }
84
+ const allOf = readKey(node, "allOf");
85
+ if (Array.isArray(allOf)) {
86
+ for (const entry of allOf) {
87
+ const lookup = emitLookup(entry, ctx);
88
+ if (lookup !== null)
89
+ steps.push(`fillInto(o, ${lookup}(NO_SEGMENTS, 0, o), depth)`);
90
+ }
91
+ }
92
+ const name = `_repairFill${id}`;
93
+ const lines = steps.length === 0 ? [`const ${name}: RepairThunk = (_depth, into) => into ?? ${base}`] : [
94
+ `const ${name}: RepairThunk = (depth, into) => {`,
95
+ // The outermost fill starts the budget every level of it shares.
96
+ ` if (into === undefined && depth === 0) fillBegin()`,
97
+ ` const o: Record<string, unknown> = into ?? ${base}`,
98
+ ` if (depth < MAX_FILL_DEPTH) {`,
99
+ ...steps.map((step) => ` ${step}`),
100
+ ` }`,
101
+ ` return o`,
102
+ `}`
103
+ ];
104
+ ctx.helpers.push(lines.join("\n"));
105
+ return name;
25
106
  };
26
- const schemaMap = (schema, key) => {
27
- const value = readKey(schema, key);
28
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
107
+ const ownAnswer = (node, self, id, ctx) => {
108
+ const marker = readKey(node, "x-repair");
109
+ if (marker === "drop")
110
+ return "REPAIR_DROP";
111
+ if (isObjectish(node))
112
+ return emitFill(node, self, id, ctx);
113
+ if (readKey(node, "type") === void 0 && !namesItsValue(node)) {
114
+ const conditional = conditionalAnswer(node, ctx);
115
+ if (conditional !== null)
116
+ return conditional;
117
+ }
118
+ const literal = getDefaultValue(node);
119
+ if (literal === "undefined")
120
+ return null;
121
+ const name = `_repairValue${id}`;
122
+ ctx.helpers.push(`const ${name}: RepairThunk = () => (${literal})`);
123
+ return name;
29
124
  };
30
- const emitWalker = (schema, ctx) => {
125
+ const emitLookup = (schema, ctx) => {
126
+ if (schema === false)
127
+ return dropLookup(ctx);
31
128
  if (!isSchemaObject(schema))
32
129
  return null;
130
+ if (ctx.lookups.has(schema))
131
+ return ctx.lookups.get(schema) ?? null;
33
132
  const node = schema;
34
133
  const ref = readKey(node, "$ref");
35
- if (typeof ref === "string")
36
- return repairLookupNameFor(ref, ctx.typeSuffix);
37
- const own = fallbackLiteral(schema);
134
+ if (typeof ref === "string") {
135
+ const name2 = repairLookupNameFor(ref, ctx.typeSuffix);
136
+ ctx.lookups.set(schema, name2);
137
+ return name2;
138
+ }
139
+ const id = ctx.next();
140
+ const name = `_repairAt${id}`;
141
+ ctx.lookups.set(schema, name);
142
+ const own = ownAnswer(node, name, id, ctx);
143
+ const descent = emitDescent(node, id, ctx);
144
+ const conditional = conditionalAnswer(node, ctx);
145
+ if (own === null && descent === null && conditional === null) {
146
+ ctx.lookups.set(schema, null);
147
+ return null;
148
+ }
149
+ const deeper = descent !== null && conditional !== null ? `${conditional} ?? ${descent}(segments, i, value)` : descent !== null ? `${descent}(segments, i, value)` : conditional ?? "null";
150
+ const body = [` if (i === segments.length) return ${own ?? "null"}`, ` return ${deeper}`];
151
+ const parameter = body.some((line) => /\bvalue\b/.test(line)) ? "value" : "_value";
152
+ ctx.helpers.push([`const ${name}: RepairLookup = (segments, i, ${parameter}) => {`, ...body, `}`].join("\n"));
153
+ return name;
154
+ };
155
+ const emitDescent = (node, id, ctx) => {
38
156
  const properties = schemaMap(node, "properties") ?? {};
39
- const children = Object.entries(properties).map(([key, child]) => [key, emitWalker(child, ctx)]).filter((entry) => entry[1] !== null);
157
+ const children = Object.entries(properties).map(([key, child2]) => [key, emitLookup(child2, ctx)]).filter((entry) => entry[1] !== null);
40
158
  const patternProperties = schemaMap(node, "patternProperties") ?? {};
41
- const patterns = Object.entries(patternProperties).map(([source, child]) => ({
159
+ const patterns = Object.entries(patternProperties).map(([source, child2]) => ({
42
160
  source,
43
- walker: emitWalker(child, ctx)
161
+ lookup: emitLookup(child2, ctx)
44
162
  }));
45
- const additional = readKey(node, "additionalProperties");
46
- const additionalWalker = typeof additional === "object" && additional !== null ? emitWalker(additional, ctx) : null;
47
- const mapped = additionalWalker !== null || patterns.some((pattern) => pattern.walker !== null);
163
+ const additional = subschema(node, "additionalProperties");
164
+ const additionalLookup = additional === void 0 ? null : emitLookup(additional, ctx);
165
+ const mapped = additionalLookup !== null || patterns.some((pattern) => pattern.lookup !== null);
48
166
  const prefixItems = readKey(node, "prefixItems");
49
167
  const tuple = Array.isArray(prefixItems) ? prefixItems : [];
50
- const tupleWalkers = tuple.map((item) => emitWalker(item, ctx));
168
+ const tupleLookups = tuple.map((item) => emitLookup(item, ctx));
51
169
  const items = readKey(node, "items");
52
- const itemsWalker = typeof items === "object" && items !== null && !Array.isArray(items) ? emitWalker(items, ctx) : null;
53
- const hasChildren = children.length > 0 || mapped || itemsWalker !== null || tupleWalkers.some((walker) => walker !== null);
54
- if (own === null && !hasChildren)
170
+ const itemsSchema = typeof items === "boolean" || typeof items === "object" && items !== null && !Array.isArray(items);
171
+ const itemsLookup = itemsSchema ? emitLookup(items, ctx) : null;
172
+ const indexed = tupleLookups.some((lookup) => lookup !== null) || itemsLookup !== null;
173
+ if (children.length === 0 && !mapped && !indexed)
55
174
  return null;
56
- const id = ctx.next();
57
- const name = `_repairAt${id}`;
58
- const lines = [
59
- `const ${name}: RepairLookup = (segments) => {`,
60
- ` if (segments.length === 0) return ${own === null ? "null" : `() => (${own})`}`
61
- ];
62
- if (hasChildren) {
63
- const declared = mapped ? Object.keys(properties) : children.map(([key]) => key);
64
- const indexed = tupleWalkers.some((walker) => walker !== null) || itemsWalker !== null;
65
- const arrayKeywords = tuple.length > 0 || typeof items === "object" && items !== null && !Array.isArray(items);
66
- const indexBlock = indexed || mapped && arrayKeywords;
67
- const nameSchema = readKey(node, "propertyNames");
68
- const keyCheck = nameSchema === void 0 ? true : keyTest(nameSchema, ctx);
69
- const readsHead = declared.length > 0 || indexBlock || mapped && patterns.length > 0 || typeof keyCheck === "string";
70
- lines.push(` const [${readsHead ? "head" : ""}, ...rest] = segments as [string, ...string[]]`);
71
- if (keyCheck === false)
72
- lines.push(` if (rest.length === 0) return null`);
73
- if (typeof keyCheck === "string")
74
- lines.push(` if (rest.length === 0 && !${keyCheck}(head)) return null`);
75
- if (declared.length > 0) {
76
- const walkers = new Map(children);
77
- lines.push(` switch (head) {`);
78
- for (const key of declared) {
79
- const walker = walkers.get(key);
80
- lines.push(` case ${JSON.stringify(key)}: return ${walker === void 0 ? "null" : `${walker}(rest)`}`);
81
- }
82
- lines.push(` }`);
83
- }
84
- if (indexBlock) {
85
- lines.push(` const index = Number(head)`);
86
- lines.push(` if (Number.isInteger(index) && index >= 0) {`);
87
- tupleWalkers.forEach((walker, position) => {
88
- if (walker === null)
89
- return;
90
- lines.push(` if (index === ${position}) return ${walker}(rest)`);
91
- });
92
- if (itemsWalker !== null) {
93
- lines.push(` if (index >= ${tuple.length}) return ${itemsWalker}(rest)`);
94
- }
95
- if (mapped && arrayKeywords)
96
- lines.push(` return null`);
97
- lines.push(` }`);
175
+ const name = `_repairChild${id}`;
176
+ const lines = [`const ${name}: RepairLookup = (segments, i, value) => {`];
177
+ lines.push(` const head = segments[i] as string`);
178
+ lines.push(` const next = i + 1`);
179
+ const declared = mapped ? Object.keys(properties) : children.map(([key]) => key);
180
+ const arrayKeywords = tuple.length > 0 || itemsSchema;
181
+ const indexBlock = indexed || mapped && arrayKeywords;
182
+ const nameSchema = readKey(node, "propertyNames");
183
+ const keyCheck = nameSchema === void 0 ? true : namedTest(nameSchema, ctx);
184
+ if (keyCheck === false)
185
+ lines.push(` if (next === segments.length) return null`);
186
+ if (typeof keyCheck === "string")
187
+ lines.push(` if (next === segments.length && !${keyCheck}(head)) return null`);
188
+ const child = `childAt(value, head)`;
189
+ if (declared.length > 0) {
190
+ const lookups = new Map(children);
191
+ lines.push(` switch (head) {`);
192
+ for (const key of declared) {
193
+ const lookup = lookups.get(key);
194
+ lines.push(` case ${JSON.stringify(key)}: return ${lookup === void 0 ? "null" : `${lookup}(segments, next, ${child})`}`);
98
195
  }
99
- if (mapped && patterns.length > 0) {
100
- const regexNames = patterns.map((pattern, index) => {
101
- const regexName = `_repairPattern${id}_${index}`;
102
- ctx.helpers.push(`const ${regexName} = new RegExp(${JSON.stringify(pattern.source)}, ${JSON.stringify(regexFlagsFor(pattern.source))})`);
103
- return regexName;
196
+ lines.push(` }`);
197
+ }
198
+ if (indexBlock) {
199
+ lines.push(` const index = Number(head)`);
200
+ lines.push(` if (Number.isInteger(index) && index >= 0) {`);
201
+ tupleLookups.forEach((lookup, position) => {
202
+ if (lookup === null)
203
+ return;
204
+ lines.push(` if (index === ${position}) return ${lookup}(segments, next, ${child})`);
205
+ });
206
+ if (itemsLookup !== null)
207
+ lines.push(` if (index >= ${tuple.length}) return ${itemsLookup}(segments, next, ${child})`);
208
+ if (mapped && arrayKeywords)
209
+ lines.push(` return null`);
210
+ lines.push(` }`);
211
+ }
212
+ if (mapped && patterns.length > 0) {
213
+ const regexNames = patterns.map((pattern, index) => {
214
+ const regexName = `_repairPattern${id}_${index}`;
215
+ ctx.helpers.push(`const ${regexName} = new RegExp(${JSON.stringify(pattern.source)}, ${JSON.stringify(regexFlagsFor(pattern.source))})`);
216
+ return regexName;
217
+ });
218
+ const answer = (lookup) => lookup === null ? "null" : `${lookup}(segments, next, ${child})`;
219
+ const only = patterns[0];
220
+ if (patterns.length === 1 && only !== void 0) {
221
+ lines.push(` if (${regexNames[0]}.test(head)) return ${answer(only.lookup)}`);
222
+ } else {
223
+ lines.push(` const claims = [${regexNames.join(", ")}].filter((pattern) => pattern.test(head))`);
224
+ lines.push(` if (claims.length > 1) return null`);
225
+ patterns.forEach((pattern, index) => {
226
+ lines.push(` if (claims[0] === ${regexNames[index]}) return ${answer(pattern.lookup)}`);
104
227
  });
105
- const answer = (walker) => walker === null ? "null" : `${walker}(rest)`;
106
- const only = patterns[0];
107
- if (patterns.length === 1 && only !== void 0) {
108
- lines.push(` if (${regexNames[0]}.test(head)) return ${answer(only.walker)}`);
109
- } else {
110
- lines.push(` const claims = [${regexNames.join(", ")}].filter((pattern) => pattern.test(head))`);
111
- lines.push(` if (claims.length > 1) return null`);
112
- patterns.forEach((pattern, index) => {
113
- lines.push(` if (claims[0] === ${regexNames[index]}) return ${answer(pattern.walker)}`);
114
- });
115
- }
116
228
  }
117
229
  }
118
- lines.push(additionalWalker === null ? ` return null` : ` return ${additionalWalker}(rest)`, `}`);
119
- ctx.helpers.push(lines.join("\n"));
230
+ lines.push(additionalLookup === null ? ` return null` : ` return ${additionalLookup}(segments, next, ${child})`, `}`);
231
+ const text = lines.join("\n");
232
+ ctx.helpers.push(/\bvalue\b/.test(text.slice(text.indexOf("\n"))) ? text : text.replace("i, value) => {", "i, _value) => {"));
120
233
  return name;
121
234
  };
122
235
  const generateRepairFunction = (schema, typeName, typeSuffix = "", options = {}) => {
123
236
  const helpers = [];
124
237
  let counter = 0;
125
- const matcher = createSubschemaMatcher(typeSuffix, options.rootSchema ?? (isSchemaObject(schema) ? schema : void 0), options.formats ?? NO_FORMATS, "_repair");
126
- const ctx = { typeSuffix, helpers, next: () => counter++, matcher };
127
- const walker = emitWalker(schema, ctx);
238
+ const rootSchema = options.rootSchema ?? (isSchemaObject(schema) ? schema : void 0);
239
+ const matcher = createSubschemaMatcher(typeSuffix, rootSchema, options.formats ?? NO_FORMATS, "_repair");
240
+ const ctx = {
241
+ typeSuffix,
242
+ helpers,
243
+ next: () => counter++,
244
+ matcher,
245
+ lookups: /* @__PURE__ */ new Map(),
246
+ tests: /* @__PURE__ */ new Map(),
247
+ drop: null
248
+ };
249
+ const lookup = emitLookup(schema, ctx);
128
250
  const lookupName = `repair${typeName}At`;
251
+ const walkName = repairWalkNameFor(typeName);
252
+ const walker = options.walk === true ? emitRepairWalk(schema, { repair: ctx, typeName, rootSchema, lookupFor: (node) => emitLookup(node, ctx) }) : null;
129
253
  const declarations = [...matcher.declarations(helpers.join("\n")), ...helpers];
130
254
  const parts = [
131
255
  ...declarations.length > 0 ? [declarations.join("\n\n"), ""] : [],
132
- `export const ${lookupName}: RepairLookup = ${walker === null ? "() => null" : walker}`,
256
+ `export const ${lookupName}: RepairLookup = ${lookup === null ? "() => null" : lookup}`,
133
257
  "",
134
- `export const repair${typeName} = (input: unknown, _path = ''): RepairResult<${typeName}> => {`,
135
- ` let value = coerce${typeName}Value(input)`,
136
- ` const repairs: ValidationError[] = []`,
137
- ` const done = new Set<string>()`,
138
- ``,
139
- ` for (let pass = 0; pass < MAX_REPAIR_PASSES; pass++) {`,
140
- ` const result = validate${typeName}(value, _path)`,
141
- ` if (result === true) return { valid: true, value: value as ${typeName}, repairs }`,
142
- ``,
143
- ` const applied = applyRepairs(value, result.errors, ${lookupName}, done)`,
144
- ` // Nothing moved, so nothing on a further pass would either.`,
145
- ` if (applied.repaired.length === 0) return { valid: false, value, errors: result.errors, repairs }`,
146
- ` value = applied.value`,
147
- ` repairs.push(...applied.repaired)`,
148
- ` }`,
149
- ``,
150
- ` const final = validate${typeName}(value, _path)`,
151
- ` return final === true`,
152
- ` ? { valid: true, value: value as ${typeName}, repairs }`,
153
- ` : { valid: false, value, errors: final.errors, repairs }`,
154
- `}`
258
+ ...options.walk === true ? [`export const ${walkName}: RepairWalk = ${walker ?? "(value) => value"}`, ""] : [],
259
+ `export const repair${typeName} = (input: unknown, options?: RepairOptions | string): RepairResult<${typeName}> =>`,
260
+ options.walk === true ? ` runRepairWalk<${typeName}>(input, options, ${walkName})` : ` runRepairLoop<${typeName}>(input, options, validate${typeName}, ${lookupName}, coerce${typeName}Value)`
155
261
  ];
156
262
  return { code: parts.join("\n") };
157
263
  };
@@ -0,0 +1,24 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ import { type RepairContext } from './repair-context.js';
3
+ /**
4
+ * The name of the walk a file exports for its definition. Not `repairXWalk`:
5
+ * that is the `repairX` of a definition named `XWalk`, and the barrel would
6
+ * export one name twice.
7
+ */
8
+ export declare const repairWalkNameFor: (typeName: string) => string;
9
+ type WalkContext = {
10
+ /** The lookup's context: declarations, numbering and the shared tests. */
11
+ readonly repair: RepairContext;
12
+ /** The definition this file is for, which names the type of its root position. */
13
+ readonly typeName: string;
14
+ /** The whole document, to see through a `$ref` to a Reference schema. */
15
+ readonly rootSchema: Record<string, unknown> | undefined;
16
+ /** The lookup for a node — the one `repairXAt` reaches it through. */
17
+ readonly lookupFor: (schema: JSONSchema) => string | null;
18
+ };
19
+ /**
20
+ * Emits the single-pass repair walk for a definition and answers the name of its
21
+ * root walker, or `null` when nothing in it is checked.
22
+ */
23
+ export declare const emitRepairWalk: (schema: JSONSchema, ctx: WalkContext) => string | null;
24
+ export {};