@amritk/generate-parsers 0.16.0 → 0.16.2

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.
@@ -13,9 +13,32 @@ export declare const scalarItemTypeCheck: (itemSchema: JSONSchema, accessor: str
13
13
  * Anything richer (objects, unions, `$ref`s) needs a real item parser instead.
14
14
  */
15
15
  export declare const isCoercibleItemSchema: (itemSchema: JSONSchema) => boolean;
16
+ /**
17
+ * The tuple `prefixItems` array when the schema declares one, else `null`.
18
+ * `prefixItems` (JSON Schema 2020-12) positions each element against its own
19
+ * subschema; the draft array-form `items` is handled by the existing item
20
+ * paths, not here.
21
+ */
22
+ export declare const getPrefixItems: (schema: JSONSchema) => readonly JSONSchema[] | null;
23
+ /**
24
+ * True when a sibling `items: false` (2020-12) or `additionalItems: false`
25
+ * (draft) caps a tuple's length — any element past the declared prefix is
26
+ * disallowed, so the coercer drops it and strict mode rejects it.
27
+ */
28
+ export declare const prefixItemsCapsLength: (schema: JSONSchema) => boolean;
16
29
  /**
17
30
  * Generates a validation expression that checks if a value matches the expected type.
18
31
  * Returns the value if valid, coerces to the correct type if invalid, or uses default if required and missing.
19
32
  * Supports type checking, pattern validation, enum validation, union types, and $ref resolution.
20
33
  */
21
34
  export declare const generateValidationExpression: (key: string, schema: JSONSchema, defaultValue: string, isRequired: boolean, rootSchema?: Record<string, unknown>, visitedRefs?: Set<string>, accessorOverride?: string, knownNotUndefined?: boolean, caseInsensitive?: boolean) => string;
35
+ /**
36
+ * Builds the coercion transform for a tuple `prefixItems` array: an index-matched
37
+ * `.map` where each declared position runs through its own subschema's coercion
38
+ * (present positions only — a shorter input keeps its absent trailing positions),
39
+ * and every element past the tuple passes through unchanged. When `capLength` is
40
+ * set (a sibling `items: false`), a trailing `.slice(0, N)` drops those extras.
41
+ * `accessor` must already be known to be an array (the caller guards with
42
+ * `Array.isArray`). `$ref` positions resolve against `rootSchema` when provided.
43
+ */
44
+ export declare const generatePrefixItemsMap: (accessor: string, prefix: readonly JSONSchema[], capLength: boolean, rootSchema?: Record<string, unknown>, visitedRefs?: Set<string>, caseInsensitive?: boolean) => string;
@@ -16,9 +16,14 @@ import { generateSchemaChecks } from './generate-schema-checks.js';
16
16
  * epoch number); other classes have no safe generic coercion, so we return null
17
17
  * and the caller falls back to the default.
18
18
  */
19
- const getInstanceCoercion = (accessor, instanceOf) => {
19
+ const getInstanceCoercion = (accessor, instanceOf, defaultValue) => {
20
+ // A bare `new Date(bad)` yields an `Invalid Date` — an object that is
21
+ // `instanceof Date` yet whose every operation is `NaN`, silently poisoning
22
+ // downstream code. Construct once, then fall back to the default when the
23
+ // result is invalid. This sits on the coercion (failure) branch only, so the
24
+ // hot path never pays for it.
20
25
  if (instanceOf === 'Date')
21
- return `new Date(${accessor} as string | number | Date)`;
26
+ return `((_d) => Number.isNaN(_d.getTime()) ? ${defaultValue} : _d)(new Date(${accessor} as string | number | Date))`;
22
27
  return null;
23
28
  };
24
29
  /**
@@ -57,6 +62,29 @@ export const isCoercibleItemSchema = (itemSchema) => {
57
62
  return true;
58
63
  return isSchemaObject(itemSchema) && hasEnum(itemSchema) && itemSchema.enum.length > 0;
59
64
  };
65
+ /**
66
+ * The tuple `prefixItems` array when the schema declares one, else `null`.
67
+ * `prefixItems` (JSON Schema 2020-12) positions each element against its own
68
+ * subschema; the draft array-form `items` is handled by the existing item
69
+ * paths, not here.
70
+ */
71
+ export const getPrefixItems = (schema) => {
72
+ if (!isSchemaObject(schema))
73
+ return null;
74
+ const prefix = schema['prefixItems'];
75
+ return Array.isArray(prefix) ? prefix : null;
76
+ };
77
+ /**
78
+ * True when a sibling `items: false` (2020-12) or `additionalItems: false`
79
+ * (draft) caps a tuple's length — any element past the declared prefix is
80
+ * disallowed, so the coercer drops it and strict mode rejects it.
81
+ */
82
+ export const prefixItemsCapsLength = (schema) => {
83
+ if (!isSchemaObject(schema))
84
+ return false;
85
+ const s = schema;
86
+ return s['items'] === false || s['additionalItems'] === false;
87
+ };
60
88
  /**
61
89
  * A boolean type check for a single JSON Schema primitive type name. Unlike
62
90
  * {@link scalarItemTypeCheck} this also covers `array`/`object` (as inline shape
@@ -104,13 +132,20 @@ const getTypeCoercion = (accessor, schema, defaultValue) => {
104
132
  switch (schema.type) {
105
133
  case 'string':
106
134
  return `String(${accessor})`;
107
- case 'number':
108
- case 'integer': {
135
+ case 'number': {
109
136
  // Number() of a non-numeric string produces NaN, which silently poisons
110
137
  // arithmetic. Guard with Number.isFinite and fall back to the default.
111
138
  const n = `Number(${accessor})`;
112
139
  return `(Number.isFinite(${n}) ? ${n} : ${defaultValue})`;
113
140
  }
141
+ case 'integer': {
142
+ // An `integer` must coerce to a whole number, or the "repaired" value still
143
+ // fails validation (a bare `Number.isFinite` accepts `1.5`). Require
144
+ // integrality and fall back to the default otherwise — matching the
145
+ // root-level integer parser, which yields the default for a non-integer.
146
+ const n = `Number(${accessor})`;
147
+ return `(Number.isInteger(${n}) ? ${n} : ${defaultValue})`;
148
+ }
114
149
  case 'boolean':
115
150
  return `Boolean(${accessor})`;
116
151
  case 'array':
@@ -140,12 +175,15 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
140
175
  const instanceOf = getMjstInstanceOf(schema);
141
176
  if (instanceOf) {
142
177
  const valid = `${accessor} instanceof ${instanceOf}`;
143
- const coercion = getInstanceCoercion(accessor, instanceOf);
178
+ // The coercer's own invalid-value fallback matches the branch it sits on:
179
+ // the default for a required property, `undefined` for an optional one.
144
180
  if (isRequired) {
181
+ const coercion = getInstanceCoercion(accessor, instanceOf, defaultValue);
145
182
  return coercion
146
183
  ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : ${defaultValue})`
147
184
  : `${valid} ? ${accessor} : ${defaultValue}`;
148
185
  }
186
+ const coercion = getInstanceCoercion(accessor, instanceOf, 'undefined');
149
187
  return coercion
150
188
  ? `${valid} ? ${accessor} : (${accessor} !== undefined ? ${coercion} : undefined)`
151
189
  : `${valid} ? ${accessor} : undefined`;
@@ -239,6 +277,17 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
239
277
  if (!hasType(schema) && !hasEnum(schema)) {
240
278
  return `${accessor} ?? ${defaultValue}`;
241
279
  }
280
+ // Tuple `prefixItems`: coerce each declared position through its own subschema
281
+ // and, with a sibling `items: false`/`additionalItems: false`, drop any element
282
+ // past the tuple length. A shorter input keeps its absent trailing positions.
283
+ // The fast path only takes a tuple whose present positions are already
284
+ // well-typed, so a mistyped position reaches here.
285
+ const prefixItems = getPrefixItems(schema);
286
+ if (hasType(schema) && schema.type === 'array' && prefixItems) {
287
+ const mapped = generatePrefixItemsMap(accessor, prefixItems, prefixItemsCapsLength(schema), rootSchema, visitedRefs, caseInsensitive);
288
+ const built = `(Array.isArray(${accessor}) ? ${mapped} : ${defaultValue})`;
289
+ return isRequired ? built : `(${accessor} !== undefined ? ${built} : undefined)`;
290
+ }
242
291
  // An array of scalar or enum items: coerce each element so e.g. `number[]`
243
292
  // actually contains numbers and an enum member set holds. The fast path only
244
293
  // takes a well-typed array, so a mistyped element reaches here. ($ref and
@@ -393,3 +442,30 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
393
442
  return `${combinedCheck} ? ${accessor} : ${enumFallback('undefined')}`;
394
443
  }
395
444
  };
445
+ /**
446
+ * Builds the coercion transform for a tuple `prefixItems` array: an index-matched
447
+ * `.map` where each declared position runs through its own subschema's coercion
448
+ * (present positions only — a shorter input keeps its absent trailing positions),
449
+ * and every element past the tuple passes through unchanged. When `capLength` is
450
+ * set (a sibling `items: false`), a trailing `.slice(0, N)` drops those extras.
451
+ * `accessor` must already be known to be an array (the caller guards with
452
+ * `Array.isArray`). `$ref` positions resolve against `rootSchema` when provided.
453
+ */
454
+ export const generatePrefixItemsMap = (accessor, prefix, capLength, rootSchema, visitedRefs, caseInsensitive) => {
455
+ const branches = prefix.map((rawPos, i) => {
456
+ // Resolve a position `$ref` to its target here: generateValidationExpression's
457
+ // own ref path rebuilds the accessor from the property key, which would
458
+ // discard the `_el` element accessor an override needs — so hand it the
459
+ // concrete schema directly instead.
460
+ let pos = rawPos;
461
+ if (isSchemaObject(pos) && hasRef(pos) && rootSchema) {
462
+ const resolved = resolveRef(pos.$ref, rootSchema);
463
+ if (resolved)
464
+ pos = resolved;
465
+ }
466
+ const posExpr = generateValidationExpression('', pos, getDefaultValue(pos), true, rootSchema, visitedRefs, '_el', true, caseInsensitive);
467
+ return `_i === ${i} ? ${posExpr}`;
468
+ });
469
+ const mapped = `(${accessor} as unknown[]).map((_el, _i) => ${[...branches, '_el'].join(' : ')})`;
470
+ return capLength ? `${mapped}.slice(0, ${prefix.length})` : mapped;
471
+ };
@@ -0,0 +1,29 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Builds a boolean expression that is `true` *exactly* when `accessor` matches
4
+ * `schema` — sound in **both** directions (true ⇒ valid, false ⇒ invalid) — or
5
+ * `null` when the schema uses a form this generator cannot prove inline.
6
+ *
7
+ * This is deliberately self-contained: it never emits a `$ref` shape-validator
8
+ * call (those can be conservative `=> false` stubs, which would break false-
9
+ * soundness) and never touches combinators. That keeps the expression usable
10
+ * both for *counting* matches (`contains`) and for *rejecting* non-matches
11
+ * (`propertyNames`, `dependentSchemas`) in strict mode, where a wrong verdict
12
+ * would silently accept or reject a document. Anything richer returns `null`,
13
+ * and `assertNoUnsupportedKeywords` turns that into a generation-time error.
14
+ *
15
+ * Constraint keywords apply per JSON Schema semantics: a `pattern`/`maxLength`
16
+ * with no `type` still constrains string instances (and is a no-op for other
17
+ * types), so a type-less schema guards each family by its runtime type rather
18
+ * than ignoring it — the subtlety `propertyNames: { maxLength: 3 }` depends on.
19
+ *
20
+ * `depth` seeds the `.every` element variable so nested array matches never
21
+ * shadow one another.
22
+ */
23
+ export declare const subschemaMatchExpr: (accessor: string, schema: JSONSchema, depth?: number) => string | null;
24
+ /**
25
+ * Whether {@link subschemaMatchExpr} can build an exact matcher for `schema`.
26
+ * The generation-time guard uses this to decide whether a `contains`,
27
+ * `propertyNames`, or `dependentSchemas` subschema is enforceable.
28
+ */
29
+ export declare const canMatchSubschema: (schema: JSONSchema) => boolean;
@@ -0,0 +1,286 @@
1
+ import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
+ import { multipleOfPassExpr } from '@amritk/helpers/multiple-of-check';
3
+ import { safeAccessor } from '@amritk/helpers/safe-accessor';
4
+ import { hasAdditionalProperties, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasPattern, hasProperties, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
5
+ import { generateEnumCheck } from './generate-enum-check.js';
6
+ /**
7
+ * Keywords `subschemaMatchExpr` reads and enforces. A subschema carrying any
8
+ * keyword outside this set (and the annotation set below) cannot be matched
9
+ * *exactly*, so the matcher bails to `null` and the generation-time guard turns
10
+ * that into a hard error rather than a permissive parser.
11
+ */
12
+ const HANDLED_KEYWORDS = new Set([
13
+ 'type',
14
+ 'enum',
15
+ 'const',
16
+ 'pattern',
17
+ 'minLength',
18
+ 'maxLength',
19
+ 'minimum',
20
+ 'maximum',
21
+ 'exclusiveMinimum',
22
+ 'exclusiveMaximum',
23
+ 'multipleOf',
24
+ 'minItems',
25
+ 'maxItems',
26
+ 'uniqueItems',
27
+ 'items',
28
+ 'required',
29
+ 'properties',
30
+ 'minProperties',
31
+ 'maxProperties',
32
+ 'additionalProperties',
33
+ ]);
34
+ /**
35
+ * Keywords that only annotate (Ajv, our differential oracle, does not assert on
36
+ * them under `strict: false`), so their presence never blocks an exact match.
37
+ * `format` is annotation-only in 2020-12 by default, matching the oracle.
38
+ */
39
+ const ANNOTATION_KEYWORDS = new Set([
40
+ 'title',
41
+ 'description',
42
+ '$comment',
43
+ '$id',
44
+ '$schema',
45
+ '$anchor',
46
+ '$vocabulary',
47
+ '$defs',
48
+ 'definitions',
49
+ 'examples',
50
+ 'default',
51
+ 'deprecated',
52
+ 'readOnly',
53
+ 'writeOnly',
54
+ 'format',
55
+ 'contentMediaType',
56
+ 'contentEncoding',
57
+ 'contentSchema',
58
+ ]);
59
+ const isObjectExpr = (acc) => `typeof ${acc} === "object" && ${acc} !== null && !Array.isArray(${acc})`;
60
+ /** True when `additionalProperties` is a constraining (non-boolean) schema. */
61
+ const hasSchemaAdditionalProperties = (schema) => hasAdditionalProperties(schema) &&
62
+ typeof schema.additionalProperties !== 'boolean';
63
+ /**
64
+ * String-family constraint checks (no type assertion). Every JSON Schema string
65
+ * keyword only asserts on string instances, so these are combined with a
66
+ * `typeof === "string"` — either as the assertion (a `type: 'string'` schema) or
67
+ * as a guard (`typeof !== "string" || …` for a type-less schema).
68
+ */
69
+ const stringConstraints = (acc, schema) => {
70
+ const c = [];
71
+ if (hasPattern(schema))
72
+ c.push(`/${escapeRegexPattern(schema.pattern)}/.test(${acc})`);
73
+ if (hasMinLength(schema))
74
+ c.push(`${acc}.length >= ${schema.minLength}`);
75
+ if (hasMaxLength(schema))
76
+ c.push(`${acc}.length <= ${schema.maxLength}`);
77
+ return c;
78
+ };
79
+ /** Number-family constraint checks (no type assertion). `integer` requires an explicit type. */
80
+ const numberConstraints = (acc, schema, integer) => {
81
+ const c = [];
82
+ if (integer)
83
+ c.push(`Number.isInteger(${acc})`);
84
+ if (hasMinimum(schema))
85
+ c.push(`${acc} >= ${schema.minimum}`);
86
+ if (hasMaximum(schema))
87
+ c.push(`${acc} <= ${schema.maximum}`);
88
+ if (hasExclusiveMinimum(schema))
89
+ c.push(`${acc} > ${schema.exclusiveMinimum}`);
90
+ if (hasExclusiveMaximum(schema))
91
+ c.push(`${acc} < ${schema.exclusiveMaximum}`);
92
+ if (hasMultipleOf(schema))
93
+ c.push(multipleOfPassExpr(acc, schema.multipleOf));
94
+ return c;
95
+ };
96
+ /** Array-family constraint checks (no `Array.isArray` assertion). `null` when an item schema is unprovable. */
97
+ const arrayConstraints = (acc, schema, depth) => {
98
+ const c = [];
99
+ if (hasMinItems(schema))
100
+ c.push(`${acc}.length >= ${schema.minItems}`);
101
+ if (hasMaxItems(schema))
102
+ c.push(`${acc}.length <= ${schema.maxItems}`);
103
+ if (hasUniqueItems(schema) && schema.uniqueItems === true) {
104
+ c.push(`new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${acc}.length`);
105
+ }
106
+ if (hasItems(schema)) {
107
+ if (Array.isArray(schema.items))
108
+ return null;
109
+ const el = `_i${depth}`;
110
+ const itemMatch = subschemaMatchExpr(el, schema.items, depth + 1);
111
+ if (itemMatch === null)
112
+ return null;
113
+ if (itemMatch !== 'true')
114
+ c.push(`(${acc} as unknown[]).every((${el}) => ${itemMatch})`);
115
+ }
116
+ return c;
117
+ };
118
+ /** Object-family constraint checks (no object-type assertion). `null` when a property schema is unprovable. */
119
+ const objectConstraints = (acc, schema, depth) => {
120
+ // A schema-valued additionalProperties record would leave record values unproven.
121
+ if (hasSchemaAdditionalProperties(schema))
122
+ return null;
123
+ const c = [];
124
+ const required = new Set(hasRequired(schema) ? schema.required : []);
125
+ if (hasProperties(schema)) {
126
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
127
+ const propAcc = safeAccessor(acc, key);
128
+ const propMatch = subschemaMatchExpr(propAcc, propSchema, depth + 1);
129
+ if (propMatch === null)
130
+ return null;
131
+ if (required.has(key)) {
132
+ c.push(propMatch === 'true'
133
+ ? `${JSON.stringify(key)} in ${acc}`
134
+ : `(${JSON.stringify(key)} in ${acc} && ${propMatch})`);
135
+ }
136
+ else if (propMatch !== 'true') {
137
+ c.push(`(${propAcc} === undefined || ${propMatch})`);
138
+ }
139
+ required.delete(key);
140
+ }
141
+ }
142
+ for (const key of required)
143
+ c.push(`${JSON.stringify(key)} in ${acc}`);
144
+ if (hasMinProperties(schema))
145
+ c.push(`Object.keys(${acc}).length >= ${schema.minProperties}`);
146
+ if (hasMaxProperties(schema))
147
+ c.push(`Object.keys(${acc}).length <= ${schema.maxProperties}`);
148
+ if (hasAdditionalProperties(schema) && schema.additionalProperties === false) {
149
+ const allowed = JSON.stringify(hasProperties(schema) ? Object.keys(schema.properties) : []);
150
+ c.push(`Object.keys(${acc}).every((_k) => ${allowed}.includes(_k))`);
151
+ }
152
+ return c;
153
+ };
154
+ /**
155
+ * Builds a boolean expression that is `true` *exactly* when `accessor` matches
156
+ * `schema` — sound in **both** directions (true ⇒ valid, false ⇒ invalid) — or
157
+ * `null` when the schema uses a form this generator cannot prove inline.
158
+ *
159
+ * This is deliberately self-contained: it never emits a `$ref` shape-validator
160
+ * call (those can be conservative `=> false` stubs, which would break false-
161
+ * soundness) and never touches combinators. That keeps the expression usable
162
+ * both for *counting* matches (`contains`) and for *rejecting* non-matches
163
+ * (`propertyNames`, `dependentSchemas`) in strict mode, where a wrong verdict
164
+ * would silently accept or reject a document. Anything richer returns `null`,
165
+ * and `assertNoUnsupportedKeywords` turns that into a generation-time error.
166
+ *
167
+ * Constraint keywords apply per JSON Schema semantics: a `pattern`/`maxLength`
168
+ * with no `type` still constrains string instances (and is a no-op for other
169
+ * types), so a type-less schema guards each family by its runtime type rather
170
+ * than ignoring it — the subtlety `propertyNames: { maxLength: 3 }` depends on.
171
+ *
172
+ * `depth` seeds the `.every` element variable so nested array matches never
173
+ * shadow one another.
174
+ */
175
+ export const subschemaMatchExpr = (accessor, schema, depth = 0) => {
176
+ // Boolean schemas: `true` matches everything, `false` matches nothing.
177
+ if (schema === true)
178
+ return 'true';
179
+ if (schema === false)
180
+ return 'false';
181
+ if (!isSchemaObject(schema))
182
+ return null;
183
+ // Any keyword we neither handle nor know to be a pure annotation could narrow
184
+ // the value set; matching would then be unsound, so bail.
185
+ for (const key of Object.keys(schema)) {
186
+ if (!HANDLED_KEYWORDS.has(key) && !ANNOTATION_KEYWORDS.has(key))
187
+ return null;
188
+ }
189
+ // An array-form `type` (multi-type / nullable) is a disjunction we do not
190
+ // prove inline here; `hasType` is false for it, so without this it would fall
191
+ // through to a permissive result.
192
+ if ('type' in schema && typeof schema.type !== 'string')
193
+ return null;
194
+ const checks = [];
195
+ // `const` — a single exact value. Primitives compare with `===`; a structural
196
+ // const would need deep equality, which this flat matcher does not attempt.
197
+ if (hasConst(schema)) {
198
+ const c = schema.const;
199
+ if (c !== null && typeof c === 'object')
200
+ return null;
201
+ checks.push(`${accessor} === ${JSON.stringify(c)}`);
202
+ }
203
+ // `enum` — membership in a fixed set (generateEnumCheck is exact).
204
+ if (hasEnum(schema)) {
205
+ if (schema.enum.length === 0)
206
+ return null;
207
+ checks.push(generateEnumCheck(accessor, schema.enum));
208
+ }
209
+ if (hasType(schema)) {
210
+ const typeChecks = buildTypedChecks(accessor, schema, depth);
211
+ if (typeChecks === null)
212
+ return null;
213
+ checks.push(...typeChecks);
214
+ }
215
+ else {
216
+ const typeless = buildTypelessChecks(accessor, schema, depth);
217
+ if (typeless === null)
218
+ return null;
219
+ checks.push(...typeless);
220
+ }
221
+ // A schema with no proven constraint matches every value.
222
+ if (checks.length === 0)
223
+ return 'true';
224
+ return checks.length === 1 ? checks[0] : `(${checks.join(' && ')})`;
225
+ };
226
+ /** Type assertion plus that type's constraints, for a single-`type` schema. */
227
+ const buildTypedChecks = (accessor, schema, depth) => {
228
+ switch (schema.type) {
229
+ case 'string':
230
+ return [`typeof ${accessor} === "string"`, ...stringConstraints(accessor, schema)];
231
+ case 'number':
232
+ return [`typeof ${accessor} === "number"`, ...numberConstraints(accessor, schema, false)];
233
+ case 'integer':
234
+ return [`typeof ${accessor} === "number"`, ...numberConstraints(accessor, schema, true)];
235
+ case 'boolean':
236
+ return [`typeof ${accessor} === "boolean"`];
237
+ case 'null':
238
+ return [`${accessor} === null`];
239
+ case 'array': {
240
+ const constraints = arrayConstraints(accessor, schema, depth);
241
+ if (constraints === null)
242
+ return null;
243
+ return [`Array.isArray(${accessor})`, ...constraints];
244
+ }
245
+ case 'object': {
246
+ const constraints = objectConstraints(accessor, schema, depth);
247
+ if (constraints === null)
248
+ return null;
249
+ return [isObjectExpr(accessor), ...constraints];
250
+ }
251
+ default:
252
+ return null;
253
+ }
254
+ };
255
+ /**
256
+ * Constraints on a schema with no `type`: each family only asserts on instances
257
+ * of its type, so guard each present family by its runtime type. A value of a
258
+ * different type satisfies the family vacuously.
259
+ */
260
+ const buildTypelessChecks = (accessor, schema, depth) => {
261
+ const checks = [];
262
+ const strChecks = stringConstraints(accessor, schema);
263
+ if (strChecks.length > 0)
264
+ checks.push(`(typeof ${accessor} !== "string" || ${joinAnd(strChecks)})`);
265
+ const numChecks = numberConstraints(accessor, schema, false);
266
+ if (numChecks.length > 0)
267
+ checks.push(`(typeof ${accessor} !== "number" || ${joinAnd(numChecks)})`);
268
+ const arrChecks = arrayConstraints(accessor, schema, depth);
269
+ if (arrChecks === null)
270
+ return null;
271
+ if (arrChecks.length > 0)
272
+ checks.push(`(!Array.isArray(${accessor}) || ${joinAnd(arrChecks)})`);
273
+ const objChecks = objectConstraints(accessor, schema, depth);
274
+ if (objChecks === null)
275
+ return null;
276
+ if (objChecks.length > 0)
277
+ checks.push(`(!(${isObjectExpr(accessor)}) || ${joinAnd(objChecks)})`);
278
+ return checks;
279
+ };
280
+ const joinAnd = (checks) => (checks.length === 1 ? checks[0] : `(${checks.join(' && ')})`);
281
+ /**
282
+ * Whether {@link subschemaMatchExpr} can build an exact matcher for `schema`.
283
+ * The generation-time guard uses this to decide whether a `contains`,
284
+ * `propertyNames`, or `dependentSchemas` subschema is enforceable.
285
+ */
286
+ export const canMatchSubschema = (schema) => subschemaMatchExpr('_x', schema) !== null;
@@ -50,6 +50,14 @@ export const getDefaultValue = (schema) => {
50
50
  if (getMjstPrimitive(schema) === 'bigint') {
51
51
  return '0n';
52
52
  }
53
+ // Array-form `type` (e.g. `["string","null"]`): `hasType` is false for it, so
54
+ // without this a required-but-missing value would default to `undefined` —
55
+ // violating both `required` and the declared type. Derive the default from the
56
+ // first listed type so the fallback is a valid member.
57
+ const rawType = schema['type'];
58
+ if (Array.isArray(rawType) && rawType.length > 0 && typeof rawType[0] === 'string') {
59
+ return getDefaultValue({ ...schema, type: rawType[0] });
60
+ }
53
61
  if (!hasType(schema)) {
54
62
  return 'undefined';
55
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-parsers",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "json-schema-typed": "^8.0.1",
51
- "@amritk/helpers": "0.12.0",
51
+ "@amritk/helpers": "0.13.1",
52
52
  "@amritk/generate-markdown": "0.4.1"
53
53
  },
54
54
  "devDependencies": {