@amritk/generate-parsers 0.12.2 → 0.13.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.
@@ -0,0 +1,370 @@
1
+ import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
+ import { multipleOfPassExpr } from '@amritk/helpers/multiple-of-check';
3
+ import { refToName } from '@amritk/helpers/ref-to-name';
4
+ import { resolveRef } from '@amritk/helpers/resolve-ref';
5
+ import { safeAccessor } from '@amritk/helpers/safe-accessor';
6
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
7
+ import { generateEnumCheck } from './generate-enum-check.js';
8
+ import { scalarItemTypeCheck } from './generate-validation-expression.js';
9
+ /**
10
+ * Boolean type-check expression builders shared by the shape validators, the
11
+ * parsers' fast paths, and the strict-mode assertions. They live in their own
12
+ * module (rather than generate-parser-function) so generate-strict-assertion
13
+ * can use them without an import cycle.
14
+ *
15
+ * Every expression built here is *true-sound*: when it evaluates to true the
16
+ * value definitely matches the schema fragment. That is all a fast path needs.
17
+ * The reverse direction — false definitely means invalid — only holds when
18
+ * {@link canEnforceUnion} approves the schema, because a check may call an
19
+ * imported `validate{X}Shape` that was generated as a conservative `=> false`
20
+ * stub. Strict-mode code that *throws* on a false check must gate on it.
21
+ */
22
+ /**
23
+ * Returns the shape-predicate function name for a given type name.
24
+ * The predicate is generated alongside each parser and tells callers
25
+ * whether `input` is already in the shape produced by the parser's fast path.
26
+ */
27
+ export const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
28
+ /**
29
+ * Extracts the branch list of a `oneOf`/`anyOf` union, or `null` when the
30
+ * schema is not a union (or mixes in other composition keywords we cannot
31
+ * turn into a membership check).
32
+ */
33
+ export const getUnionBranches = (schema) => {
34
+ if (!isSchemaObject(schema))
35
+ return null;
36
+ if (hasAllOf(schema) || 'not' in schema)
37
+ return null;
38
+ if (hasOneOf(schema) && schema.oneOf.length > 0)
39
+ return schema.oneOf;
40
+ if (hasAnyOf(schema) && schema.anyOf.length > 0)
41
+ return schema.anyOf;
42
+ return null;
43
+ };
44
+ /**
45
+ * Generates a fast-path type check expression for a property.
46
+ * Returns null if the schema is too complex for a simple type check.
47
+ *
48
+ * When `useRefImports` is true, $ref properties (and arrays of $refs) are
49
+ * checked by calling the imported shape predicate, allowing parent parsers
50
+ * to fast-path through nested ref types without recursing into their parsers.
51
+ */
52
+ export const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
53
+ if (!isSchemaObject(schema))
54
+ return null;
55
+ // $ref via shape predicate (deep fast-path through nested types).
56
+ if (useRefImports && hasRef(schema)) {
57
+ const refName = refToName(schema.$ref, suffix);
58
+ return `${shapeValidatorName(refName)}(${varName})`;
59
+ }
60
+ if (hasRef(schema))
61
+ return null;
62
+ // A fixed member set is directly predicable regardless of `type` — this is
63
+ // what lets a nested object carrying an enum property (e.g. `axiom.kind`)
64
+ // keep a real shape validator instead of the `=> false` stub.
65
+ if (hasEnum(schema)) {
66
+ return schema.enum.length > 0 ? generateEnumCheck(varName, schema.enum) : null;
67
+ }
68
+ // A union property is predicable when every branch is: membership is the
69
+ // disjunction of the branch checks.
70
+ const branches = getUnionBranches(schema);
71
+ if (branches) {
72
+ return generateUnionCheck(varName, branches, useRefImports, suffix);
73
+ }
74
+ // Remaining composition (allOf, not, or a union mixed with them) cannot be
75
+ // expressed as a simple boolean check.
76
+ if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || 'not' in schema) {
77
+ return null;
78
+ }
79
+ // A `const` property (the usual discriminated-union tag, e.g. `kind: "lit"`) has
80
+ // no `type`, but is fast-path predicable via strict equality. Handling it here is
81
+ // what lets a discriminated branch's shape validator be a real predicate instead
82
+ // of the `=> false` stub. Structural consts need deep equality, so skip those.
83
+ if (hasConst(schema)) {
84
+ const c = schema.const;
85
+ return c === null || typeof c !== 'object' ? `${varName} === ${JSON.stringify(c)}` : null;
86
+ }
87
+ if (!hasType(schema))
88
+ return null;
89
+ // Array of $refs: each item must satisfy the imported shape predicate.
90
+ if (useRefImports &&
91
+ schema.type === 'array' &&
92
+ hasItems(schema) &&
93
+ isSchemaObject(schema.items) &&
94
+ hasRef(schema.items)) {
95
+ const refName = refToName(schema.items.$ref, suffix);
96
+ return `Array.isArray(${varName}) && ${varName}.every(${shapeValidatorName(refName)})`;
97
+ }
98
+ const checks = [];
99
+ switch (schema.type) {
100
+ case 'string': {
101
+ checks.push(`typeof ${varName} === "string"`);
102
+ if (hasPattern(schema))
103
+ checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${varName})`);
104
+ if (hasMinLength(schema))
105
+ checks.push(`${varName}.length >= ${schema.minLength}`);
106
+ if (hasMaxLength(schema))
107
+ checks.push(`${varName}.length <= ${schema.maxLength}`);
108
+ break;
109
+ }
110
+ case 'number':
111
+ case 'integer': {
112
+ checks.push(`typeof ${varName} === "number"`);
113
+ // `integer` must reject non-integral numbers on the fast path too, otherwise
114
+ // `1.5` would pass through uncoerced.
115
+ if (schema.type === 'integer')
116
+ checks.push(`Number.isInteger(${varName})`);
117
+ if (hasMinimum(schema))
118
+ checks.push(`${varName} >= ${schema.minimum}`);
119
+ if (hasMaximum(schema))
120
+ checks.push(`${varName} <= ${schema.maximum}`);
121
+ if (hasExclusiveMinimum(schema))
122
+ checks.push(`${varName} > ${schema.exclusiveMinimum}`);
123
+ if (hasExclusiveMaximum(schema))
124
+ checks.push(`${varName} < ${schema.exclusiveMaximum}`);
125
+ if (hasMultipleOf(schema))
126
+ checks.push(multipleOfPassExpr(varName, schema.multipleOf));
127
+ break;
128
+ }
129
+ case 'boolean':
130
+ checks.push(`typeof ${varName} === "boolean"`);
131
+ break;
132
+ case 'array': {
133
+ checks.push(`Array.isArray(${varName})`);
134
+ if (hasMinItems(schema))
135
+ checks.push(`${varName}.length >= ${schema.minItems}`);
136
+ if (hasMaxItems(schema))
137
+ checks.push(`${varName}.length <= ${schema.maxItems}`);
138
+ if (hasUniqueItems(schema) && schema.uniqueItems === true) {
139
+ checks.push(`new Set(${varName}).size === ${varName}.length`);
140
+ }
141
+ // For a scalar or enum item type, every element must already be well-typed
142
+ // to take the fast path; a mismatched element routes the array to the slow
143
+ // path where each element is coerced (or, in strict mode, throws).
144
+ if (hasItems(schema) && !Array.isArray(schema.items)) {
145
+ const items = schema.items;
146
+ const itemCheck = scalarItemTypeCheck(items, '_it') ??
147
+ (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0
148
+ ? generateEnumCheck('_it', items.enum)
149
+ : null);
150
+ if (itemCheck)
151
+ checks.push(`${varName}.every((_it) => ${itemCheck})`);
152
+ }
153
+ break;
154
+ }
155
+ case 'object':
156
+ checks.push(`isObject(${varName})`);
157
+ break;
158
+ default:
159
+ return null;
160
+ }
161
+ if (checks.length === 0)
162
+ return null;
163
+ // checks[0] is safe: we guard with checks.length === 0 above
164
+ let result = checks[0];
165
+ for (let i = 1; i < checks.length; i++) {
166
+ result += ' && ' + checks[i];
167
+ }
168
+ return result;
169
+ };
170
+ /**
171
+ * Generates a boolean check that `varName` matches an inline object schema —
172
+ * `isObject` plus a per-property check for every declared property (and a
173
+ * presence check for `required` keys without a declared schema). Used for
174
+ * union branches written inline, where no named shape validator exists to
175
+ * call. Returns null when any property is too complex to check.
176
+ */
177
+ export const generateInlineObjectCheck = (varName, schema, useRefImports, suffix) => {
178
+ if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
179
+ return null;
180
+ if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema) || 'not' in schema)
181
+ return null;
182
+ if ('patternProperties' in schema || 'if' in schema)
183
+ return null;
184
+ // additionalProperties-as-schema (records) would leave the record values
185
+ // unchecked, breaking the "true means valid" guarantee.
186
+ if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== 'boolean')
187
+ return null;
188
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
189
+ const checks = [`isObject(${varName})`];
190
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
191
+ const acc = safeAccessor(varName, key);
192
+ const check = generatePropertyTypeCheck(acc, propSchema, useRefImports, suffix);
193
+ if (check === null)
194
+ return null;
195
+ checks.push(required.has(key) ? check : `(${acc} === undefined || ${check})`);
196
+ required.delete(key);
197
+ }
198
+ // `required` keys without a declared property schema still need a presence check.
199
+ for (const key of required) {
200
+ checks.push(`${JSON.stringify(key)} in ${varName}`);
201
+ }
202
+ // additionalProperties: false — extras make the value invalid for this branch.
203
+ if (hasAdditionalProperties(schema) && schema.additionalProperties === false) {
204
+ const allowedKeys = JSON.stringify(Object.keys(schema.properties));
205
+ checks.push(`Object.keys(${varName}).every((_k) => ${allowedKeys}.includes(_k))`);
206
+ }
207
+ return checks.join(' && ');
208
+ };
209
+ /**
210
+ * Generates a union membership check: true when `varName` matches at least one
211
+ * branch of a `oneOf`/`anyOf`. Inline object branches get a per-property check;
212
+ * everything else goes through {@link generatePropertyTypeCheck} ($refs call
213
+ * the imported shape validator, consts/enums/scalars check directly). Returns
214
+ * null when any branch cannot be checked.
215
+ */
216
+ export const generateUnionCheck = (varName, branches, useRefImports, suffix) => {
217
+ if (branches.length === 0)
218
+ return null;
219
+ const parts = [];
220
+ for (const branch of branches) {
221
+ if (!isSchemaObject(branch))
222
+ return null;
223
+ const check = isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
224
+ ? generateInlineObjectCheck(varName, branch, useRefImports, suffix)
225
+ : generatePropertyTypeCheck(varName, branch, useRefImports, suffix);
226
+ if (check === null)
227
+ return null;
228
+ parts.push(`(${check})`);
229
+ }
230
+ // Callers conjoin this with other checks, so a multi-branch disjunction must
231
+ // carry its own parentheses — `a || b && c` binds the wrong way without them.
232
+ return parts.length === 1 ? parts[0] : `(${parts.join(' || ')})`;
233
+ };
234
+ /**
235
+ * True when the union membership check emitted by {@link generateUnionCheck}
236
+ * is also *false-sound* — a false result definitely means the value is invalid
237
+ * — so strict mode may throw on it.
238
+ *
239
+ * The risk is `$ref`s: a branch (or a property inside one) whose check calls
240
+ * an imported `validate{X}Shape` is only trustworthy when that validator was
241
+ * generated as a real predicate. This walks every reachable ref (mirroring
242
+ * generateShapeValidator's stub conditions) and rejects the union when any of
243
+ * them would have produced the conservative `=> false` stub, or is itself
244
+ * built on one. Cycles (e.g. a recursive `expr` union) are assumed sound —
245
+ * the recursion bottoms out on the data, not the schema.
246
+ */
247
+ export const canEnforceUnion = (branches, rootSchema) => {
248
+ const visiting = new Set();
249
+ return (branches.length > 0 &&
250
+ branches.every((branch) => {
251
+ if (!isSchemaObject(branch))
252
+ return false;
253
+ return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
254
+ ? canTrustInlineObjectCheck(branch, rootSchema, visiting)
255
+ : canTrustPropertyCheck(branch, rootSchema, visiting);
256
+ }));
257
+ };
258
+ /** Mirrors {@link generatePropertyTypeCheck}: non-null AND every embedded validator call is real. */
259
+ const canTrustPropertyCheck = (schema, rootSchema, visiting) => {
260
+ if (!isSchemaObject(schema))
261
+ return false;
262
+ if (hasRef(schema)) {
263
+ return canTrustReferencedValidator(schema.$ref, rootSchema, visiting);
264
+ }
265
+ if (hasEnum(schema))
266
+ return schema.enum.length > 0;
267
+ const branches = getUnionBranches(schema);
268
+ if (branches) {
269
+ return branches.every((branch) => {
270
+ if (!isSchemaObject(branch))
271
+ return false;
272
+ return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
273
+ ? canTrustInlineObjectCheck(branch, rootSchema, visiting)
274
+ : canTrustPropertyCheck(branch, rootSchema, visiting);
275
+ });
276
+ }
277
+ if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || 'not' in schema)
278
+ return false;
279
+ if (hasConst(schema))
280
+ return schema.const === null || typeof schema.const !== 'object';
281
+ if (!hasType(schema))
282
+ return false;
283
+ if (schema.type === 'array' && hasItems(schema) && isSchemaObject(schema.items) && hasRef(schema.items)) {
284
+ return canTrustReferencedValidator(schema.items.$ref, rootSchema, visiting);
285
+ }
286
+ switch (schema.type) {
287
+ case 'string':
288
+ case 'number':
289
+ case 'integer':
290
+ case 'boolean':
291
+ case 'array':
292
+ case 'object':
293
+ return true;
294
+ default:
295
+ return false;
296
+ }
297
+ };
298
+ /** Mirrors {@link generateInlineObjectCheck}: non-null AND every property check is trustworthy. */
299
+ const canTrustInlineObjectCheck = (schema, rootSchema, visiting) => {
300
+ if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
301
+ return false;
302
+ if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema) || 'not' in schema)
303
+ return false;
304
+ if ('patternProperties' in schema || 'if' in schema)
305
+ return false;
306
+ if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== 'boolean')
307
+ return false;
308
+ return Object.values(schema.properties).every((propSchema) => canTrustPropertyCheck(propSchema, rootSchema, visiting));
309
+ };
310
+ /**
311
+ * True when the file generated for `ref`'s target carries a *real* (non-stub)
312
+ * shape validator whose result can be trusted in both directions. Mirrors the
313
+ * schema classes generateShapeValidator (and its new union/alias paths) turns
314
+ * into real predicates, recursing into every reachable ref.
315
+ */
316
+ const canTrustReferencedValidator = (ref, rootSchema, visiting) => {
317
+ if (visiting.has(ref))
318
+ return true;
319
+ if (!rootSchema)
320
+ return false;
321
+ const resolved = resolveRef(ref, rootSchema);
322
+ if (!resolved || !isSchemaObject(resolved))
323
+ return false;
324
+ visiting.add(ref);
325
+ try {
326
+ // Alias definition (bare $ref): its validator delegates to the target's.
327
+ if (hasRef(resolved) && !hasProperties(resolved)) {
328
+ return canTrustReferencedValidator(resolved.$ref, rootSchema, visiting);
329
+ }
330
+ // Pure union definition: real when the membership check is enforceable.
331
+ if (!hasProperties(resolved)) {
332
+ const branches = getUnionBranches(resolved);
333
+ if (!branches)
334
+ return false;
335
+ if ('patternProperties' in resolved || 'if' in resolved)
336
+ return false;
337
+ return branches.every((branch) => {
338
+ if (!isSchemaObject(branch))
339
+ return false;
340
+ return isObjectSchema(branch) && hasProperties(branch) && !hasRef(branch)
341
+ ? canTrustInlineObjectCheck(branch, rootSchema, visiting)
342
+ : canTrustPropertyCheck(branch, rootSchema, visiting);
343
+ });
344
+ }
345
+ // Object definition: real when generateShapeValidator emits a predicate —
346
+ // no composition/conditional keywords, and every property check resolves.
347
+ if (hasOneOf(resolved) ||
348
+ hasAnyOf(resolved) ||
349
+ hasAllOf(resolved) ||
350
+ 'not' in resolved ||
351
+ 'patternProperties' in resolved ||
352
+ 'if' in resolved ||
353
+ 'then' in resolved ||
354
+ 'else' in resolved) {
355
+ return false;
356
+ }
357
+ return Object.values(resolved.properties).every((propSchema) => {
358
+ // Records of refs make generateShapeValidator bail to the stub.
359
+ if (isSchemaObject(propSchema) &&
360
+ hasAdditionalProperties(propSchema) &&
361
+ typeof propSchema.additionalProperties !== 'boolean') {
362
+ return false;
363
+ }
364
+ return canTrustPropertyCheck(propSchema, rootSchema, visiting);
365
+ });
366
+ }
367
+ finally {
368
+ visiting.delete(ref);
369
+ }
370
+ };
@@ -1,5 +1,6 @@
1
1
  import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
2
  import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
3
+ import { multipleOfPassExpr } from '@amritk/helpers/multiple-of-check';
3
4
  import { resolveRef } from '@amritk/helpers/resolve-ref';
4
5
  import { safeAccessor } from '@amritk/helpers/safe-accessor';
5
6
  import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards';
@@ -33,16 +34,55 @@ export const scalarItemTypeCheck = (itemSchema, accessor) => {
33
34
  case 'string':
34
35
  return `typeof ${accessor} === "string"`;
35
36
  case 'number':
37
+ return `typeof ${accessor} === "number"`;
36
38
  case 'integer':
39
+ // `integer` items reject non-integral numbers; a bare typeof accepts `1.5`.
40
+ return `typeof ${accessor} === "number" && Number.isInteger(${accessor})`;
41
+ case 'boolean':
42
+ return `typeof ${accessor} === "boolean"`;
43
+ case 'null':
44
+ return `${accessor} === null`;
45
+ default:
46
+ return null;
47
+ }
48
+ };
49
+ /**
50
+ * A boolean type check for a single JSON Schema primitive type name. Unlike
51
+ * {@link scalarItemTypeCheck} this also covers `array`/`object` (as inline shape
52
+ * checks, so no `isObject` import is assumed) and enforces `integer` with
53
+ * `Number.isInteger` — a bare `typeof === "number"` accepts `1.5`. Used to build
54
+ * the disjunction for an array-form `type` (e.g. `["string","null"]`).
55
+ */
56
+ const singleTypeCheck = (accessor, type) => {
57
+ switch (type) {
58
+ case 'string':
59
+ return `typeof ${accessor} === "string"`;
60
+ case 'number':
37
61
  return `typeof ${accessor} === "number"`;
62
+ case 'integer':
63
+ return `typeof ${accessor} === "number" && Number.isInteger(${accessor})`;
38
64
  case 'boolean':
39
65
  return `typeof ${accessor} === "boolean"`;
40
66
  case 'null':
41
67
  return `${accessor} === null`;
68
+ case 'array':
69
+ return `Array.isArray(${accessor})`;
70
+ case 'object':
71
+ return `typeof ${accessor} === "object" && ${accessor} !== null && !Array.isArray(${accessor})`;
42
72
  default:
43
73
  return null;
44
74
  }
45
75
  };
76
+ /**
77
+ * Returns the list of type names when `schema.type` is an array (the JSON Schema
78
+ * multi-type / nullable idiom, e.g. `["string","null"]`), or `null` for a single
79
+ * or absent type. Multi-type is validated as a *disjunction* of per-type checks.
80
+ */
81
+ const getTypeArray = (schema) => {
82
+ if (!isSchemaObject(schema) || !('type' in schema) || !Array.isArray(schema.type))
83
+ return null;
84
+ return schema.type;
85
+ };
46
86
  /**
47
87
  * Generates a type coercion expression for converting a value to the expected type.
48
88
  */
@@ -171,6 +211,20 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
171
211
  ? `${accessor} === ${constLiteral} ? ${accessor} : ${constLiteral}`
172
212
  : `${accessor} === ${constLiteral} ? ${accessor} : (${accessor} !== undefined ? ${constLiteral} : undefined)`;
173
213
  }
214
+ // Array-form `type` (multi-type / nullable, e.g. `["string","null"]`). `hasType`
215
+ // is false for an array `type`, so without this the value would fall through
216
+ // every branch below and emit NO validation — not even a required-presence
217
+ // check. Emit a disjunction: the value is accepted when it matches ANY listed
218
+ // type, otherwise it is coerced to the default (required) or dropped (optional).
219
+ // A missing value fails every disjunct, so `required` presence is still enforced.
220
+ const typeArray = getTypeArray(schema);
221
+ if (typeArray && !hasEnum(schema)) {
222
+ const checks = typeArray.map((t) => singleTypeCheck(accessor, t)).filter((c) => c !== null);
223
+ if (checks.length > 0) {
224
+ const combined = checks.map((c) => `(${c})`).join(' || ');
225
+ return isRequired ? `${combined} ? ${accessor} : ${defaultValue}` : `${combined} ? ${accessor} : undefined`;
226
+ }
227
+ }
174
228
  if (!hasType(schema) && !hasEnum(schema)) {
175
229
  return `${accessor} ?? ${defaultValue}`;
176
230
  }
@@ -207,6 +261,11 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
207
261
  case 'number':
208
262
  case 'integer': {
209
263
  checks.push(`typeof ${accessor} === "number"`);
264
+ // `integer` must reject non-integral numbers; a bare `typeof === "number"`
265
+ // accepts `1.5`. Matches the validators package (which uses Number.isInteger).
266
+ if (schema.type === 'integer') {
267
+ checks.push(`Number.isInteger(${accessor})`);
268
+ }
210
269
  if (hasMinimum(schema)) {
211
270
  checks.push(`${accessor} >= ${schema.minimum}`);
212
271
  }
@@ -220,7 +279,7 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
220
279
  checks.push(`${accessor} < ${schema.exclusiveMaximum}`);
221
280
  }
222
281
  if (hasMultipleOf(schema)) {
223
- checks.push(`${accessor} % ${schema.multipleOf} === 0`);
282
+ checks.push(multipleOfPassExpr(accessor, schema.multipleOf));
224
283
  }
225
284
  break;
226
285
  }
@@ -248,7 +307,7 @@ export const generateValidationExpression = (key, schema, defaultValue, isRequir
248
307
  // Check required properties exist
249
308
  if (hasRequired(schema) && schema.required.length > 0) {
250
309
  for (const requiredKey of schema.required) {
251
- checks.push(`"${requiredKey}" in ${accessor}`);
310
+ checks.push(`${JSON.stringify(requiredKey)} in ${accessor}`);
252
311
  }
253
312
  }
254
313
  // Check minProperties
@@ -11,7 +11,9 @@ const EMBEDDED_NAMED_EXPORTS = {
11
11
  'validate-record': 'validateRecord',
12
12
  'has-ref': 'hasRef',
13
13
  };
14
- const embeddedImport = (helper, prefix) => `import { ${EMBEDDED_NAMED_EXPORTS[helper]} } from '${prefix}_helpers/${helper}';`;
14
+ const embeddedImport = (helper, prefix) =>
15
+ // `.js` extension so the embedded-helper import resolves under Node ESM, not only Bun.
16
+ `import { ${EMBEDDED_NAMED_EXPORTS[helper]} } from '${prefix}_helpers/${helper}.js';`;
15
17
  /**
16
18
  * Detects which runtime helpers a generated parser body references.
17
19
  *
@@ -2,7 +2,11 @@ import { refToFilename } from '@amritk/helpers/ref-to-filename';
2
2
  import { refToName } from '@amritk/helpers/ref-to-name';
3
3
  import { resolveRef } from '@amritk/helpers/resolve-ref';
4
4
  import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasRef } from '@amritk/helpers/schema-guards';
5
- const getImportPathForFilename = (filename) => `./${filename}`;
5
+ // Emit an explicit `.js` extension on the relative specifier. Node's ESM resolver
6
+ // requires it (extensionless relative imports only work under a bundler or Bun),
7
+ // and `./x.js` pointing at a sibling `x.ts` is the standard TS NodeNext form —
8
+ // accepted by Bun, esbuild, webpack, and tsc alike.
9
+ const getImportPathForFilename = (filename) => `./${filename}.js`;
6
10
  /**
7
11
  * Collects all import statements needed for a schema by finding $ref references
8
12
  * that are directly used in the type definition (from properties, items, etc.).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-parsers",
3
- "version": "0.12.2",
3
+ "version": "0.13.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -48,8 +48,8 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "json-schema-typed": "^8.0.1",
51
- "@amritk/helpers": "0.10.1",
52
- "@amritk/generate-markdown": "0.4.0"
51
+ "@amritk/helpers": "0.10.3",
52
+ "@amritk/generate-markdown": "0.4.1"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@sinclair/typebox": "^0.34.49",