@amritk/validation 0.4.1 → 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.
@@ -10,20 +10,23 @@ const buildImport = (ref, suffix, reads, importExt) => {
10
10
  const checkerName = `check${typeName}`;
11
11
  const coercerName = `coerce${typeName}Value`;
12
12
  const repairerName = `repair${typeName}At`;
13
- const { type, validator, guard = false, checker = false, coercer = false, repairer = false } = reads({
13
+ const walkerName = `walk${typeName}Repair`;
14
+ const { type, validator, guard = false, checker = false, coercer = false, repairer = false, walker = false } = reads({
14
15
  typeName,
15
16
  validatorName,
16
17
  guardName,
17
18
  checkerName,
18
19
  coercerName,
19
- repairerName
20
+ repairerName,
21
+ walkerName
20
22
  });
21
23
  const values = [
22
24
  ...validator ? [validatorName] : [],
23
25
  ...guard ? [guardName] : [],
24
26
  ...checker ? [checkerName] : [],
25
27
  ...coercer ? [coercerName] : [],
26
- ...repairer ? [repairerName] : []
28
+ ...repairer ? [repairerName] : [],
29
+ ...walker ? [walkerName] : []
27
30
  ];
28
31
  if (type && values.length > 0)
29
32
  return `import { type ${typeName}, ${values.join(", ")} } from './${filename}.${importExt}'`;
@@ -53,6 +53,12 @@ type GenerateValidatorFileOptions = {
53
53
  * validator ever sees it, and never shows up as a repair.
54
54
  */
55
55
  readonly repair?: boolean;
56
+ /**
57
+ * Whether the repairing half also gets the single-pass walk, which `repairX`
58
+ * then runs instead of the validate-and-repair loop. Decided over the whole
59
+ * schema graph by the caller, since one walk calls another across files.
60
+ */
61
+ readonly repairWalk?: boolean;
56
62
  /** Extension on every emitted relative specifier: `'js'` (default) or `'ts'`. */
57
63
  readonly importExt?: 'js' | 'ts';
58
64
  /**
@@ -17,7 +17,7 @@ const generateValidatorFile = (schema, typeName, options) => {
17
17
  const importExt = options?.importExt ?? "js";
18
18
  const formats = options?.formats ?? NO_FORMATS;
19
19
  const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix, options?.rootSchema, unknownKeys, formats, options?.branchErrors === true);
20
- const { code: booleanGuard, standalone: standaloneGuard } = generateBooleanGuardSource(schema, typeName, unknownKeys, formats);
20
+ const { code: booleanGuard, standalone: standaloneGuard } = generateBooleanGuardSource(schema, typeName, unknownKeys, formats, { suffix: typeSuffix, ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {} });
21
21
  const checker = options?.check === true ? generateCheckFunction(schema, typeName, typeSuffix, options?.rootSchema, unknownKeys, formats) : "";
22
22
  const wantsCoerce = options?.coerce === true || options?.repair === true;
23
23
  const coercer = wantsCoerce ? generateCoerceFunction(schema, typeName, typeSuffix, {
@@ -27,7 +27,8 @@ const generateValidatorFile = (schema, typeName, options) => {
27
27
  }).code : "";
28
28
  const repairer = options?.repair === true ? generateRepairFunction(schema, typeName, typeSuffix, {
29
29
  ...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
30
- formats
30
+ formats,
31
+ walk: options.repairWalk === true
31
32
  }).code : "";
32
33
  const appended = [checker, coercer, repairer].filter((part) => part !== "");
33
34
  const body = validatorFunction + booleanGuard + (appended.length === 0 ? "" : "\n\n" + appended.join("\n\n"));
@@ -37,13 +38,14 @@ const generateValidatorFile = (schema, typeName, options) => {
37
38
  rootSchema: options?.rootSchema,
38
39
  typeSuffix,
39
40
  importExt,
40
- reads: ({ typeName: name, validatorName, guardName, checkerName, coercerName, repairerName }) => ({
41
+ reads: ({ typeName: name, validatorName, guardName, checkerName, coercerName, repairerName, walkerName }) => ({
41
42
  type: mentions(name),
42
43
  validator: mentions(validatorName),
43
44
  guard: mentions(guardName),
44
45
  checker: mentions(checkerName),
45
46
  coercer: mentions(coercerName),
46
- repairer: mentions(repairerName)
47
+ repairer: mentions(repairerName),
48
+ walker: mentions(walkerName)
47
49
  })
48
50
  });
49
51
  const resultTypes = [
@@ -51,7 +53,7 @@ const generateValidatorFile = (schema, typeName, options) => {
51
53
  .../\bValidationError\b/.test(body) ? ["ValidationError"] : [],
52
54
  .../\bCoercionResult\b/.test(body) ? ["CoercionResult"] : [],
53
55
  .../\bRepairResult\b/.test(body) ? ["RepairResult"] : [],
54
- .../\bRepairLookup\b/.test(body) ? ["RepairLookup"] : []
56
+ ...["RepairLookup", "RepairOptions", "RepairThunk", "RepairWalk", "RepairRun"].filter((name) => new RegExp(`\\b${name}\\b`).test(body))
55
57
  ];
56
58
  let result = `import type { ${resultTypes.join(", ")} } from './validation-result.${importExt}'
57
59
  `;
@@ -63,10 +65,33 @@ const generateValidatorFile = (schema, typeName, options) => {
63
65
  "selectBranchErrors",
64
66
  "coerceScalar",
65
67
  "coerceUnion",
66
- "applyRepairs"
67
- ].filter((name) => body.includes(`${name}(`));
68
- if (body.includes("MAX_REPAIR_PASSES"))
69
- runtimeHelpers.push("MAX_REPAIR_PASSES");
68
+ "runRepairLoop",
69
+ "runRepairWalk",
70
+ "childAt",
71
+ "fillKey",
72
+ "fillInto",
73
+ "walkError",
74
+ "walkPath",
75
+ "walkRepair",
76
+ "walkVerified",
77
+ "walkSet",
78
+ "walkSetItem",
79
+ "walkCompact",
80
+ "walkMissing",
81
+ "walkScope",
82
+ "walkUnscope",
83
+ "fillBegin",
84
+ "walkExtra",
85
+ "walkProto",
86
+ "walkHalt",
87
+ "walkMemo",
88
+ "walkRef",
89
+ "isPlainObject"
90
+ ].filter((name) => body.includes(`${name}(`) || body.includes(`${name}<`));
91
+ for (const name of ["REPAIR_DROP", "NO_SEGMENTS", "MAX_FILL_DEPTH"]) {
92
+ if (new RegExp(`\\b${name}\\b`).test(body))
93
+ runtimeHelpers.push(name);
94
+ }
70
95
  if (runtimeHelpers.length > 0) {
71
96
  result += `import { ${runtimeHelpers.join(", ")} } from './validation-result.${importExt}'
72
97
  `;
@@ -5,28 +5,24 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
5
5
  */
6
6
  export declare const repairLookupNameFor: (ref: string, typeSuffix: string) => string;
7
7
  /**
8
- * Generates the repairing half of a validator file: the position lookup, and the
9
- * `repairX` that drives validate-repair-revalidate over it.
8
+ * Generates the repairing half of a validator file: the position lookup, the
9
+ * single-pass walk when the schema is one it covers, and the `repairX` that runs
10
+ * one or the other.
10
11
  *
11
- * The loop is the whole design. Coercion runs first, so anything that was only
12
- * written in the wrong type is already right and never counts as a repair. What
13
- * is left is judged by the very same `validateX` the package already emits, and
14
- * *its errors* are what drive the repair: each one names a position and a
15
- * reason, the lookup answers with a value for that position, and the error the
16
- * repair came from is what gets reported. Nothing second-guesses the validator,
17
- * and there is no parallel set of rules about what is wrong that could disagree
18
- * with it.
19
- *
20
- * Re-validating after a round is what catches the cases one pass cannot see: a
21
- * required object that was missing entirely is repaired into existence, and only
22
- * then can its own children be judged. A position is repaired at most once, so
23
- * the loop terminates on progress rather than on the pass cap.
12
+ * The lookup answers what a position repairs to, and both engines ask it, so
13
+ * they cannot land on different values. The walk checks and repairs every
14
+ * position in one pass; the loop — validate, repair the reported positions,
15
+ * validate again — is what every other schema gets. Either way, what is
16
+ * reported is the error the validator would have produced, and nothing
17
+ * second-guesses it.
24
18
  */
25
19
  export declare const generateRepairFunction: (schema: JSONSchema, typeName: string, typeSuffix?: string, options?: {
26
- /** The whole document, for a `propertyNames` test that reads a `$ref`'s target. */
20
+ /** The whole document, for a test that reads a `$ref`'s target. */
27
21
  readonly rootSchema?: Record<string, unknown>;
28
- /** The `format` names the validator enforces, so a key test agrees with it. */
22
+ /** The `format` names the validator enforces, so a test agrees with it. */
29
23
  readonly formats?: ReadonlySet<string>;
24
+ /** Whether to emit the single-pass walk, decided over the whole schema graph. */
25
+ readonly walk?: boolean;
30
26
  }) => {
31
27
  code: string;
32
28
  };
@@ -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 {};