@amritk/generate-parsers 0.7.2 → 0.9.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.
Files changed (73) hide show
  1. package/README.md +1 -1
  2. package/dist/generators/build-schema.d.ts +0 -1
  3. package/dist/generators/generate-default-from-pattern.d.ts +0 -1
  4. package/dist/generators/generate-discriminated-union-validation.d.ts +0 -1
  5. package/dist/generators/generate-enum-check.d.ts +0 -1
  6. package/dist/generators/generate-files.d.ts +0 -1
  7. package/dist/generators/generate-non-discriminated-union-validation.d.ts +0 -1
  8. package/dist/generators/generate-parser-function.d.ts +3 -3
  9. package/dist/generators/generate-parser-function.js +267 -32
  10. package/dist/generators/generate-schema-checks.d.ts +0 -1
  11. package/dist/generators/generate-strict-assertion.d.ts +0 -1
  12. package/dist/generators/generate-validation-expression.d.ts +0 -1
  13. package/dist/helpers/apply-schema-extensions.d.ts +0 -1
  14. package/dist/helpers/collect-helpers.d.ts +0 -1
  15. package/dist/helpers/collect-imports.d.ts +0 -1
  16. package/dist/helpers/find-discriminator.d.ts +0 -1
  17. package/dist/helpers/get-default-value.d.ts +0 -1
  18. package/dist/helpers/get-discriminator-value.d.ts +0 -1
  19. package/dist/index.d.ts +0 -1
  20. package/dist/types/schema-extensions.d.ts +0 -1
  21. package/package.json +3 -5
  22. package/dist/generators/build-schema.d.ts.map +0 -1
  23. package/dist/generators/generate-default-from-pattern.d.ts.map +0 -1
  24. package/dist/generators/generate-discriminated-union-validation.d.ts.map +0 -1
  25. package/dist/generators/generate-enum-check.d.ts.map +0 -1
  26. package/dist/generators/generate-files.d.ts.map +0 -1
  27. package/dist/generators/generate-non-discriminated-union-validation.d.ts.map +0 -1
  28. package/dist/generators/generate-parser-function.d.ts.map +0 -1
  29. package/dist/generators/generate-schema-checks.d.ts.map +0 -1
  30. package/dist/generators/generate-strict-assertion.d.ts.map +0 -1
  31. package/dist/generators/generate-validation-expression.d.ts.map +0 -1
  32. package/dist/helpers/apply-schema-extensions.d.ts.map +0 -1
  33. package/dist/helpers/collect-helpers.d.ts.map +0 -1
  34. package/dist/helpers/collect-imports.d.ts.map +0 -1
  35. package/dist/helpers/find-discriminator.d.ts.map +0 -1
  36. package/dist/helpers/get-default-value.d.ts.map +0 -1
  37. package/dist/helpers/get-discriminator-value.d.ts.map +0 -1
  38. package/dist/index.d.ts.map +0 -1
  39. package/dist/types/schema-extensions.d.ts.map +0 -1
  40. package/src/generators/build-schema.test.ts +0 -854
  41. package/src/generators/build-schema.ts +0 -147
  42. package/src/generators/generate-default-from-pattern.test.ts +0 -81
  43. package/src/generators/generate-default-from-pattern.ts +0 -39
  44. package/src/generators/generate-discriminated-union-validation.test.ts +0 -84
  45. package/src/generators/generate-discriminated-union-validation.ts +0 -54
  46. package/src/generators/generate-enum-check.test.ts +0 -45
  47. package/src/generators/generate-enum-check.ts +0 -8
  48. package/src/generators/generate-files.test.ts +0 -448
  49. package/src/generators/generate-files.ts +0 -155
  50. package/src/generators/generate-non-discriminated-union-validation.test.ts +0 -74
  51. package/src/generators/generate-non-discriminated-union-validation.ts +0 -40
  52. package/src/generators/generate-parser-function.test.ts +0 -1794
  53. package/src/generators/generate-parser-function.ts +0 -1340
  54. package/src/generators/generate-schema-checks.test.ts +0 -330
  55. package/src/generators/generate-schema-checks.ts +0 -128
  56. package/src/generators/generate-strict-assertion.test.ts +0 -67
  57. package/src/generators/generate-strict-assertion.ts +0 -243
  58. package/src/generators/generate-validation-expression.test.ts +0 -831
  59. package/src/generators/generate-validation-expression.ts +0 -327
  60. package/src/helpers/apply-schema-extensions.test.ts +0 -194
  61. package/src/helpers/apply-schema-extensions.ts +0 -63
  62. package/src/helpers/collect-helpers.test.ts +0 -115
  63. package/src/helpers/collect-helpers.ts +0 -79
  64. package/src/helpers/collect-imports.test.ts +0 -547
  65. package/src/helpers/collect-imports.ts +0 -275
  66. package/src/helpers/find-discriminator.test.ts +0 -88
  67. package/src/helpers/find-discriminator.ts +0 -39
  68. package/src/helpers/get-default-value.test.ts +0 -139
  69. package/src/helpers/get-default-value.ts +0 -95
  70. package/src/helpers/get-discriminator-value.test.ts +0 -83
  71. package/src/helpers/get-discriminator-value.ts +0 -21
  72. package/src/index.ts +0 -1
  73. package/src/types/schema-extensions.ts +0 -35
package/README.md CHANGED
@@ -132,7 +132,7 @@ Returns: `Promise<GeneratedFile[]>`.
132
132
  <td align="center"><code>false</code></td>
133
133
  </tr>
134
134
  <tr>
135
- <td colspan="5">Generate parsers that throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values. Unknown extra keys are still allowed.</td>
135
+ <td colspan="5">Generate parsers that throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values. When a schema sets additionalProperties: false, undeclared keys throw too; otherwise they are still allowed.</td>
136
136
  </tr>
137
137
  </tbody>
138
138
  </table>
@@ -74,4 +74,3 @@ export type GeneratedFile = {
74
74
  * ```
75
75
  */
76
76
  export declare const buildSchema: (rootSchema: JSONSchema, rootTypeName: string, extensions?: SchemaExtensions, typesOnly?: boolean, logWarnings?: boolean, strict?: boolean, helpersMode?: HelpersMode, helpersImportPrefix?: string, readonly?: boolean, typeSuffix?: string) => Promise<GeneratedFile[]>;
77
- //# sourceMappingURL=build-schema.d.ts.map
@@ -3,4 +3,3 @@
3
3
  * Recognizes common patterns and returns appropriate defaults.
4
4
  */
5
5
  export declare const generateDefaultFromPattern: (pattern: string) => string | null;
6
- //# sourceMappingURL=generate-default-from-pattern.d.ts.map
@@ -4,4 +4,3 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
4
4
  * Uses a discriminator property to efficiently choose the correct schema.
5
5
  */
6
6
  export declare const generateDiscriminatedUnionValidation: (accessor: string, schemas: readonly JSONSchema[], discriminatorKey: string, defaultValue: string, isRequired: boolean) => string;
7
- //# sourceMappingURL=generate-discriminated-union-validation.d.ts.map
@@ -3,4 +3,3 @@
3
3
  * Returns a string that checks if the value is in the allowed enum values.
4
4
  */
5
5
  export declare const generateEnumCheck: (accessor: string, enumValues: readonly unknown[]) => string;
6
- //# sourceMappingURL=generate-enum-check.d.ts.map
@@ -91,4 +91,3 @@ export type GeneratedFileContent = {
91
91
  */
92
92
  export declare const generateFile: (schema: JSONSchema, typeName: string, options?: GenerateFileOptions) => GeneratedFileContent;
93
93
  export {};
94
- //# sourceMappingURL=generate-files.d.ts.map
@@ -4,4 +4,3 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
4
4
  * Tries each schema in order until one matches.
5
5
  */
6
6
  export declare const generateNonDiscriminatedUnionValidation: (accessor: string, schemas: readonly JSONSchema[], defaultValue: string, isRequired: boolean) => string;
7
- //# sourceMappingURL=generate-non-discriminated-union-validation.d.ts.map
@@ -17,7 +17,8 @@ type GenerateParserOptions = {
17
17
  /**
18
18
  * When true, the generated parser throws on type/shape mismatches instead
19
19
  * of coercing invalid input to default values. Throws on the first violation
20
- * with a path-aware Error. Unknown extra keys are still allowed.
20
+ * with a path-aware Error. When the schema sets `additionalProperties: false`,
21
+ * undeclared keys throw too; otherwise they are still allowed.
21
22
  */
22
23
  readonly strict?: boolean;
23
24
  /**
@@ -54,6 +55,5 @@ export declare const shapeValidatorFunctionName: (typeName: string) => string;
54
55
  * in additionalProperties). The stub is safe: parents calling it will fall
55
56
  * through to the slow path, matching the pre-deep-fast-path behavior.
56
57
  */
57
- export declare const generateShapeValidator: (schema: JSONSchema, typeName: string, useRefImports: boolean, suffix?: string) => string;
58
+ export declare const generateShapeValidator: (schema: JSONSchema, typeName: string, useRefImports: boolean, suffix?: string, exported?: boolean) => string;
58
59
  export {};
59
- //# sourceMappingURL=generate-parser-function.d.ts.map
@@ -1,7 +1,8 @@
1
1
  import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
2
  import { refToName } from '@amritk/helpers/ref-to-name';
3
3
  import { safeAccessor, safeKey } from '@amritk/helpers/safe-accessor';
4
- import { hasAllOf, hasAnyOf, hasConst, hasDefault, hasEnum, hasExamples, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
4
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDefault, hasEnum, hasExamples, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
5
+ import { unknownKeyCheck } from '@amritk/helpers/unknown-key-check';
5
6
  import { getDefaultValue } from '../helpers/get-default-value.js';
6
7
  import { generateObjectStrictAssertion, generateScalarStrictAssertion } from './generate-strict-assertion.js';
7
8
  import { generateValidationExpression } from './generate-validation-expression.js';
@@ -252,7 +253,7 @@ const generateFallbackValue = (_, propSchema, useRefImports, suffix) => {
252
253
  * Generates a fallback object with required properties filled with default values.
253
254
  * This is used when input is not an object (undefined, null, etc.).
254
255
  */
255
- const generateFallbackObject = (schema, useRefImports, typeName, suffix) => {
256
+ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subTypeNames) => {
256
257
  if (!hasProperties(schema)) {
257
258
  return `{} as ${typeName}`;
258
259
  }
@@ -266,7 +267,12 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix) => {
266
267
  for (const [key, propSchema] of Object.entries(schema.properties)) {
267
268
  const isRequired = isPropertyRequired(key, schema);
268
269
  if (isRequired) {
269
- const fallbackValue = generateFallbackValue(key, propSchema, useRefImports, suffix);
270
+ // Inline nested objects delegate to their sub-parser so the fallback
271
+ // carries proper deep defaults instead of an empty object literal.
272
+ const subName = subTypeNames?.get(key);
273
+ const fallbackValue = subName
274
+ ? `${generateParserName(subName)}(undefined)`
275
+ : generateFallbackValue(key, propSchema, useRefImports, suffix);
270
276
  if (fallbackValue === 'undefined') {
271
277
  return `{} as ${typeName}`;
272
278
  }
@@ -344,6 +350,73 @@ const toVarName = (key) => {
344
350
  * whether `input` is already in the shape produced by the parser's fast path.
345
351
  */
346
352
  const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
353
+ /**
354
+ * Matches an inline nested object property — an object schema written directly
355
+ * under `properties` with its own `properties`, rather than referenced via
356
+ * `$ref`. These get a private sub-parser (and shape predicate) in the same
357
+ * generated file so their fields are actually parsed; anything involving
358
+ * composition, conditionals, enums, or record semantics keeps the existing
359
+ * code paths.
360
+ */
361
+ const isInlineObjectProperty = (propSchema) => {
362
+ if (!isSchemaObject(propSchema))
363
+ return false;
364
+ if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema))
365
+ return false;
366
+ if (hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema))
367
+ return false;
368
+ if ('patternProperties' in propSchema || 'not' in propSchema || 'if' in propSchema)
369
+ return false;
370
+ // additionalProperties-as-schema records go through the record paths instead
371
+ if (hasAdditionalProperties(propSchema) && typeof propSchema.additionalProperties !== 'boolean')
372
+ return false;
373
+ return isObjectSchema(propSchema) && hasProperties(propSchema);
374
+ };
375
+ /**
376
+ * Builds the property-key → synthesized-type-name map for every inline nested
377
+ * object property of a schema, e.g. parent `Order` + key `shipTo` →
378
+ * `OrderShipTo`. Both the parser and the shape validator derive the map
379
+ * independently, so the naming (including collision suffixes) is a pure
380
+ * function of the schema and parent type name.
381
+ */
382
+ const collectInlineObjectProperties = (schema, typeName) => {
383
+ const subTypeNames = new Map();
384
+ if (!hasProperties(schema))
385
+ return subTypeNames;
386
+ const used = new Set();
387
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
388
+ if (!isInlineObjectProperty(propSchema))
389
+ continue;
390
+ const pascal = key
391
+ .replace(/[^a-zA-Z0-9_$]/g, '_')
392
+ .split('_')
393
+ .filter((part) => part.length > 0)
394
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
395
+ .join('');
396
+ let subName = `${typeName}${pascal || 'Value'}`;
397
+ while (used.has(subName))
398
+ subName = `${subName}_`;
399
+ used.add(subName);
400
+ subTypeNames.set(key, subName);
401
+ }
402
+ return subTypeNames;
403
+ };
404
+ /**
405
+ * True when the parser must reject (strict mode) or strip (coerce mode) every
406
+ * undeclared key: the schema sets `additionalProperties: false` and nothing
407
+ * else (key patterns, composition) can legitimately introduce extra keys.
408
+ */
409
+ const hasStrictKeys = (schema) => {
410
+ if (!isSchemaObject(schema) || !hasProperties(schema))
411
+ return false;
412
+ if (!hasAdditionalProperties(schema) || schema.additionalProperties !== false)
413
+ return false;
414
+ if ('patternProperties' in schema)
415
+ return false;
416
+ if (hasAllOf(schema) || hasOneOf(schema) || hasAnyOf(schema))
417
+ return false;
418
+ return true;
419
+ };
347
420
  /**
348
421
  * Generates a fast-path type check expression for a property.
349
422
  * Returns null if the schema is too complex for a simple type check
@@ -457,22 +530,52 @@ const shouldCacheVariable = (propSchema, _canFastPath, _useRefImports) => {
457
530
  * - Fast path that returns input directly when all properties are already valid
458
531
  * - Slow path with optimized expressions
459
532
  */
460
- const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict) => {
533
+ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnings, strict, exported = true) => {
461
534
  const functionName = generateParserName(typeName);
535
+ const exportPrefix = exported ? 'export ' : '';
462
536
  if (!hasProperties(schema)) {
463
537
  if (strict) {
464
- return `export const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return input as ${typeName};\n};`;
538
+ return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {\n if (!isObject(input)) throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);\n return input as ${typeName};\n};`;
465
539
  }
466
- return `export const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? input as ${typeName} : {} as ${typeName};`;
540
+ return `${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => isObject(input) ? input as ${typeName} : {} as ${typeName};`;
467
541
  }
468
- const fallbackObject = generateFallbackObject(schema, useRefImports, typeName, suffix);
469
542
  const properties = Object.entries(schema.properties);
543
+ // Inline nested object properties get a private sub-parser (plus shape
544
+ // predicate and type alias) in the same file, so their fields are parsed for
545
+ // real instead of only passing an isObject check. The sub-parser recurses
546
+ // through this same generator, so nesting works to any depth.
547
+ const subTypeNames = collectInlineObjectProperties(schema, typeName);
548
+ const preamble = [];
549
+ for (const [key, subName] of subTypeNames) {
550
+ const propSchema = schema.properties[key];
551
+ preamble.push(`type ${subName} = ${typeName}[${JSON.stringify(key)}];`);
552
+ preamble.push(generateShapeValidator(propSchema, subName, useRefImports, suffix, false));
553
+ preamble.push(generateObjectParser(propSchema, subName, useRefImports, suffix, logWarnings, strict, false));
554
+ }
555
+ // additionalProperties: false — the per-key "is this undeclared" test inlines
556
+ // `!==` comparisons for a short key list and hoists a Set only for a long one
557
+ // (see unknownKeyCheck). The predicate form is shared by the fast path and the
558
+ // shape validator; strict mode throws with the offending key.
559
+ const strictKeys = hasStrictKeys(schema);
560
+ const strictKeyCheck = unknownKeyCheck(properties.map(([key]) => key), `_knownKeys${typeName}`);
561
+ if (strictKeys) {
562
+ for (const declaration of strictKeyCheck.declarations) {
563
+ preamble.push(`${declaration};`);
564
+ }
565
+ preamble.push(`const _hasOnlyKnownKeys${typeName} = (input: Record<string, unknown>): boolean => {\n for (const _k in input) if (${strictKeyCheck.isUnknown('_k')}) return false;\n return true;\n};`);
566
+ }
567
+ const fallbackObject = generateFallbackObject(schema, useRefImports, typeName, suffix, subTypeNames);
470
568
  const lines = [];
471
- lines.push(`export const ${functionName} = (input: unknown): ${typeName} => {`);
569
+ lines.push(`${exportPrefix}const ${functionName} = (input: unknown): ${typeName} => {`);
472
570
  if (strict) {
473
571
  for (const assertionLine of generateObjectStrictAssertion(schema, typeName)) {
474
572
  lines.push(assertionLine);
475
573
  }
574
+ if (strictKeys) {
575
+ lines.push(` for (const _k in input) {`);
576
+ lines.push(` if (${strictKeyCheck.isUnknown('_k')}) throw new Error(\`[${typeName}] unknown property "\${_k}"\`);`);
577
+ lines.push(` }`);
578
+ }
476
579
  }
477
580
  else {
478
581
  lines.push(` if (!isObject(input)) return ${fallbackObject};`);
@@ -499,7 +602,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
499
602
  canFastPath = false;
500
603
  }
501
604
  else {
502
- const check = generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix);
605
+ const subName = subTypeNames.get(key);
606
+ // Inline nested objects fast-path through their private shape
607
+ // predicate, the same way $ref properties use the imported one.
608
+ const check = subName
609
+ ? `${shapeValidatorName(subName)}(${varName})`
610
+ : generatePropertyTypeCheck(varName, propSchema, useRefImports, suffix);
503
611
  if (check === null) {
504
612
  canFastPath = false;
505
613
  }
@@ -514,6 +622,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
514
622
  }
515
623
  }
516
624
  }
625
+ // The fast path returns `{ ...input }`, so with additionalProperties: false
626
+ // it must only fire when no undeclared keys are present. Strict mode already
627
+ // threw on them above.
628
+ if (strictKeys && !strict) {
629
+ fastPathChecks.push(`_hasOnlyKnownKeys${typeName}(input)`);
630
+ }
517
631
  // Second pass: generate variable declarations only for properties that need them
518
632
  for (const { key, varName, propSchema } of propInfo) {
519
633
  if (shouldCacheVariable(propSchema, canFastPath, useRefImports)) {
@@ -522,10 +636,12 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
522
636
  }
523
637
  // Emit a warning for any input key not declared in the schema's properties
524
638
  if (logWarnings && propInfo.length > 0) {
525
- const keysList = propInfo.map(({ key }) => JSON.stringify(key)).join(', ');
526
- lines.push(` const _knownKeys = new Set([${keysList}]);`);
639
+ const warnKeyCheck = unknownKeyCheck(propInfo.map(({ key }) => key), '_knownKeys');
640
+ for (const declaration of warnKeyCheck.declarations) {
641
+ lines.push(` ${declaration};`);
642
+ }
527
643
  lines.push(` for (const _k in input) {`);
528
- lines.push(` if (!_knownKeys.has(_k)) {`);
644
+ lines.push(` if (${warnKeyCheck.isUnknown('_k')}) {`);
529
645
  lines.push(` console.warn(\`[${typeName}] Unknown property "\${_k}"\`);`);
530
646
  lines.push(` }`);
531
647
  lines.push(` }`);
@@ -538,9 +654,13 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
538
654
  }
539
655
  lines.push(` if (${fastPathExpr}) return { ...input } as ${typeName};`);
540
656
  }
541
- // Generate slow-path object construction
657
+ // Generate slow-path object construction. The input spread preserves
658
+ // undeclared keys, so it is dropped when additionalProperties: false —
659
+ // building from the declared properties alone strips them from the result.
542
660
  const objectLines = [];
543
- objectLines.push(' ...input,');
661
+ if (!strictKeys) {
662
+ objectLines.push(' ...input,');
663
+ }
544
664
  // Spread allOf $ref parsers so their field coercions are applied before
545
665
  // the explicit property validations below.
546
666
  if (useRefImports && isSchemaObject(schema) && hasAllOf(schema)) {
@@ -554,6 +674,19 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
554
674
  for (const { key, varName, isRequired, propSchema } of propInfo) {
555
675
  const shouldCache = shouldCacheVariable(propSchema, canFastPath, useRefImports);
556
676
  const accessor = shouldCache ? varName : safeAccessor('input', key);
677
+ // Handle inline nested objects via their private sub-parser, mirroring
678
+ // how $ref properties delegate to the imported parser.
679
+ const subName = subTypeNames.get(key);
680
+ if (subName) {
681
+ const subParserName = generateParserName(subName);
682
+ if (isRequired) {
683
+ objectLines.push(` ${safeKey(key)}: ${subParserName}(${accessor}),`);
684
+ }
685
+ else {
686
+ objectLines.push(` ...(${accessor} !== undefined && { ${safeKey(key)}: ${subParserName}(${accessor}) }),`);
687
+ }
688
+ continue;
689
+ }
557
690
  // Handle direct $ref properties via imported parsers
558
691
  if (shouldUseRefImport(propSchema, useRefImports)) {
559
692
  const ref = propSchema.$ref;
@@ -613,20 +746,66 @@ const generateObjectParser = (schema, typeName, useRefImports, suffix, logWarnin
613
746
  }
614
747
  }
615
748
  lines.push(` return {`);
616
- // objectLines[0] is safe: objectLines is always non-empty (has at least the closing brace line)
617
- let objectBody = objectLines[0];
618
- for (let i = 1; i < objectLines.length; i++) {
619
- objectBody += '\n' + objectLines[i];
620
- }
621
- lines.push(objectBody);
749
+ lines.push(objectLines.join('\n'));
622
750
  lines.push(` } as unknown as ${typeName};`);
623
751
  lines.push(`}`);
624
- // lines[0] is safe: lines is always non-empty (it always has the function declaration)
625
- let result = lines[0];
626
- for (let i = 1; i < lines.length; i++) {
627
- result += '\n' + lines[i];
628
- }
629
- return result;
752
+ const fn = lines.join('\n');
753
+ if (preamble.length === 0)
754
+ return fn;
755
+ return `${preamble.join('\n\n')}\n\n${fn}`;
756
+ };
757
+ /**
758
+ * Combined-parser variant for `properties` + `patternProperties` schemas that
759
+ * set `additionalProperties: false`. Declared properties are coerced as usual,
760
+ * keys matching any pattern are kept (the first `$ref` pattern is coerced via
761
+ * its imported parser when ref imports are on), and every other key is rejected
762
+ * in strict mode or stripped in coerce mode — matching the interpreter.
763
+ */
764
+ const generateStrictCombinedParser = (schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict) => {
765
+ const declaredKeys = hasProperties(schema) ? Object.keys(schema.properties) : [];
766
+ // Below the inline threshold a key === "a" || key === "b" chain skips declared
767
+ // keys without the per-call Set allocation; a wider list hoists the Set.
768
+ const knownKeyCheck = unknownKeyCheck(declaredKeys, '_knownKeys');
769
+ // The first $ref pattern (when ref imports are on) coerces its matching values
770
+ // through the imported parser; the remaining patterns keep the raw value.
771
+ const refPattern = useRefImports ? patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps)) : undefined;
772
+ const loopLines = [` if (${knownKeyCheck.isKnown('key')}) continue;`];
773
+ if (refPattern) {
774
+ const parserName = generateParserName(refToName(refPattern[1].$ref, suffix));
775
+ loopLines.push(` if (/${escapeRegexPattern(refPattern[0])}/.test(key)) {`);
776
+ loopLines.push(` (result as Record<string, unknown>)[key] = ${parserName}(input[key]);`);
777
+ loopLines.push(` continue;`);
778
+ loopLines.push(` }`);
779
+ }
780
+ const keepConditions = patterns
781
+ .filter(([p]) => !(refPattern && p === refPattern[0]))
782
+ .map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
783
+ if (keepConditions.length > 0) {
784
+ loopLines.push(` if (${keepConditions.join(' || ')}) {`);
785
+ loopLines.push(` (result as Record<string, unknown>)[key] = input[key];`);
786
+ loopLines.push(` continue;`);
787
+ loopLines.push(` }`);
788
+ }
789
+ // Unknown key: throw in strict mode, otherwise let it fall through (dropped,
790
+ // since it was never copied into `result`).
791
+ if (strict)
792
+ loopLines.push(` throw new Error(\`[${typeName}] unknown property "\${key}"\`);`);
793
+ const notObjectBranch = strict
794
+ ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
795
+ : ` return {} as unknown as ${typeName};`;
796
+ const resultBody = propertyLines.length > 0 ? `{\n${propertyLines.join('\n')}\n }` : '{}';
797
+ // Only the Set form needs a declaration; the inline === chain is stateless.
798
+ const knownKeysDeclaration = knownKeyCheck.declarations.map((decl) => ` ${decl};\n`).join('');
799
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
800
+ if (!isObject(input)) {
801
+ ${notObjectBranch}
802
+ }
803
+ ${knownKeysDeclaration} const result = ${resultBody} as unknown as ${typeName};
804
+ for (const key in input) {
805
+ ${loopLines.join('\n')}
806
+ }
807
+ return result;
808
+ };`;
630
809
  };
631
810
  /**
632
811
  * Generates a parser for schemas that have both properties AND patternProperties.
@@ -649,6 +828,14 @@ const generateCombinedObjectParser = (schema, typeName, useRefImports, suffix, l
649
828
  const patternProps = schema.patternProperties;
650
829
  const patterns = Object.entries(patternProps);
651
830
  const refPattern = patterns.find(([, ps]) => isSchemaObject(ps) && hasRef(ps));
831
+ const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
832
+ // With `additionalProperties: false`, keys matching neither a declared
833
+ // property nor any pattern must be rejected (strict) or stripped (coerce).
834
+ // The blanket `...input` spread used below cannot do that, so build a
835
+ // selective copy that mirrors the interpreter's undeclared-key handling.
836
+ if (strictKeys) {
837
+ return generateStrictCombinedParser(schema, typeName, functionName, propertyLines, patterns, useRefImports, suffix, strict);
838
+ }
652
839
  if (!refPattern || !useRefImports) {
653
840
  return generateObjectParser(schema, typeName, useRefImports, suffix, logWarnings, strict);
654
841
  }
@@ -775,6 +962,38 @@ const generatePatternPropertiesParser = (schema, typeName, useRefImports, suffix
775
962
  const notObjectBranch = strict
776
963
  ? ` throw new Error(\`[${typeName}] expected object, got \${input === null ? "null" : typeof input}\`);`
777
964
  : ` return {} as unknown as ${typeName};`;
965
+ // With `additionalProperties: false`, only keys matching a pattern survive:
966
+ // others are rejected (strict) or stripped (coerce). Start from an empty
967
+ // object rather than spreading every input key.
968
+ const strictKeys = hasAdditionalProperties(schema) && schema.additionalProperties === false;
969
+ if (strictKeys) {
970
+ const loopLines = [` if (/${escapedPattern}/.test(key)) {`];
971
+ if (patternAssignment) {
972
+ loopLines.push(` const value = input[key];`);
973
+ loopLines.push(` ${patternAssignment}`);
974
+ }
975
+ loopLines.push(` continue;`);
976
+ loopLines.push(` }`);
977
+ const keepConditions = patterns.slice(1).map(([p]) => `/${escapeRegexPattern(p)}/.test(key)`);
978
+ if (keepConditions.length > 0) {
979
+ loopLines.push(` if (${keepConditions.join(' || ')}) {`);
980
+ loopLines.push(` (result as Record<string, unknown>)[key] = input[key];`);
981
+ loopLines.push(` continue;`);
982
+ loopLines.push(` }`);
983
+ }
984
+ if (strict)
985
+ loopLines.push(` throw new Error(\`[${typeName}] unknown property "\${key}"\`);`);
986
+ return `export const ${functionName} = (input: unknown): ${typeName} => {
987
+ if (!isObject(input)) {
988
+ ${notObjectBranch}
989
+ }
990
+ const result = {} as unknown as ${typeName};
991
+ for (const key in input) {
992
+ ${loopLines.join('\n')}
993
+ }
994
+ return result;
995
+ };`;
996
+ }
778
997
  // Generate a parser that handles both pattern matching and x- extensions
779
998
  return `export const ${functionName} = (input: unknown): ${typeName} => {
780
999
  if (!isObject(input)) {
@@ -1011,9 +1230,10 @@ export const shapeValidatorFunctionName = (typeName) => shapeValidatorName(typeN
1011
1230
  * in additionalProperties). The stub is safe: parents calling it will fall
1012
1231
  * through to the slow path, matching the pre-deep-fast-path behavior.
1013
1232
  */
1014
- export const generateShapeValidator = (schema, typeName, useRefImports, suffix = '') => {
1233
+ export const generateShapeValidator = (schema, typeName, useRefImports, suffix = '', exported = true) => {
1015
1234
  const fnName = shapeValidatorName(typeName);
1016
- const stub = `export const ${fnName} = (_input: unknown): boolean => false;`;
1235
+ const exportPrefix = exported ? 'export ' : '';
1236
+ const stub = `${exportPrefix}const ${fnName} = (_input: unknown): boolean => false;`;
1017
1237
  if (!isSchemaObject(schema))
1018
1238
  return stub;
1019
1239
  if (!hasProperties(schema))
@@ -1030,6 +1250,12 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
1030
1250
  return stub;
1031
1251
  }
1032
1252
  const properties = Object.entries(schema.properties);
1253
+ // Same deterministic naming as the parser's sub-parser generation, so the
1254
+ // referenced private shape predicates exist in the same file.
1255
+ const subTypeNames = collectInlineObjectProperties(schema, typeName);
1256
+ // With additionalProperties: false the shape only matches when every key is
1257
+ // declared, so the parser's fast path will not smuggle extras through.
1258
+ const strictKeysGuard = hasStrictKeys(schema) ? `\n if (!_hasOnlyKnownKeys${typeName}(input)) return false;` : '';
1033
1259
  const checks = [];
1034
1260
  for (const [key, propSchema] of properties) {
1035
1261
  // Records of refs require iterating every value — too expensive for the fast path.
@@ -1038,7 +1264,10 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
1038
1264
  }
1039
1265
  const accessor = safeAccessor('input', key);
1040
1266
  const isRequired = isPropertyRequired(key, schema);
1041
- const check = generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
1267
+ const subName = subTypeNames.get(key);
1268
+ const check = subName
1269
+ ? `${shapeValidatorName(subName)}(${accessor})`
1270
+ : generatePropertyTypeCheck(accessor, propSchema, useRefImports, suffix);
1042
1271
  if (check === null) {
1043
1272
  return stub;
1044
1273
  }
@@ -1050,14 +1279,20 @@ export const generateShapeValidator = (schema, typeName, useRefImports, suffix =
1050
1279
  }
1051
1280
  }
1052
1281
  if (checks.length === 0) {
1053
- return `export const ${fnName} = (input: unknown): boolean => isObject(input);`;
1282
+ if (strictKeysGuard) {
1283
+ return `${exportPrefix}const ${fnName} = (input: unknown): boolean => {
1284
+ if (!isObject(input)) return false;${strictKeysGuard}
1285
+ return true;
1286
+ };`;
1287
+ }
1288
+ return `${exportPrefix}const ${fnName} = (input: unknown): boolean => isObject(input);`;
1054
1289
  }
1055
1290
  let body = checks[0];
1056
1291
  for (let i = 1; i < checks.length; i++) {
1057
1292
  body += '\n && ' + checks[i];
1058
1293
  }
1059
- return `export const ${fnName} = (input: unknown): boolean => {
1060
- if (!isObject(input)) return false;
1294
+ return `${exportPrefix}const ${fnName} = (input: unknown): boolean => {
1295
+ if (!isObject(input)) return false;${strictKeysGuard}
1061
1296
  return ${body};
1062
1297
  };`;
1063
1298
  };
@@ -3,4 +3,3 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
3
3
  * Generates validation checks for a single schema (used in union validation).
4
4
  */
5
5
  export declare const generateSchemaChecks: (accessor: string, schema: JSONSchema) => string[];
6
- //# sourceMappingURL=generate-schema-checks.d.ts.map
@@ -16,4 +16,3 @@ export declare const generateObjectStrictAssertion: (schema: JSONSchema, typeNam
16
16
  * Returns null when the schema has no type information to assert on.
17
17
  */
18
18
  export declare const generateScalarStrictAssertion: (schema: JSONSchema, typeName: string) => string | null;
19
- //# sourceMappingURL=generate-strict-assertion.d.ts.map
@@ -5,4 +5,3 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
5
5
  * Supports type checking, pattern validation, enum validation, union types, and $ref resolution.
6
6
  */
7
7
  export declare const generateValidationExpression: (key: string, schema: JSONSchema, defaultValue: string, isRequired: boolean, rootSchema?: Record<string, unknown>, visitedRefs?: Set<string>, accessorOverride?: string, knownNotUndefined?: boolean) => string;
8
- //# sourceMappingURL=generate-validation-expression.d.ts.map
@@ -33,4 +33,3 @@ import type { SchemaExtensions } from '../types/schema-extensions.js';
33
33
  * ```
34
34
  */
35
35
  export declare const applySchemaExtensions: (schema: JSONSchema, definitionName: string, extensions: SchemaExtensions) => JSONSchema;
36
- //# sourceMappingURL=apply-schema-extensions.d.ts.map
@@ -21,4 +21,3 @@ export type CollectedHelpers = {
21
21
  * `_helpers/` directory at the output root.
22
22
  */
23
23
  export declare const collectHelpers: (parserFunction: string, mode: HelpersMode, helpersImportPrefix?: string) => CollectedHelpers;
24
- //# sourceMappingURL=collect-helpers.d.ts.map
@@ -68,4 +68,3 @@ type CollectImportsOptions = {
68
68
  */
69
69
  export declare const collectImports: (schema: JSONSchema, options?: CollectImportsOptions) => string[];
70
70
  export {};
71
- //# sourceMappingURL=collect-imports.d.ts.map
@@ -4,4 +4,3 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
4
4
  * Returns the property name if a suitable discriminator is found, otherwise null.
5
5
  */
6
6
  export declare const findDiscriminator: (schemas: readonly JSONSchema[]) => string | null;
7
- //# sourceMappingURL=find-discriminator.d.ts.map
@@ -5,4 +5,3 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
5
5
  * These defaults ensure that parsing never fails, even with missing data.
6
6
  */
7
7
  export declare const getDefaultValue: (schema: JSONSchema) => string;
8
- //# sourceMappingURL=get-default-value.d.ts.map
@@ -4,4 +4,3 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
4
4
  * Returns the const value or single enum value if available.
5
5
  */
6
6
  export declare const getDiscriminatorValue: (schema: JSONSchema, discriminatorKey: string) => unknown | null;
7
- //# sourceMappingURL=get-discriminator-value.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,2 +1 @@
1
1
  export { buildSchema, type GeneratedFile } from './generators/build-schema.js';
2
- //# sourceMappingURL=index.d.ts.map
@@ -32,4 +32,3 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
32
32
  * ```
33
33
  */
34
34
  export type SchemaExtensions = Record<string, Record<string, JSONSchema>>;
35
- //# sourceMappingURL=schema-extensions.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-parsers",
3
- "version": "0.7.2",
3
+ "version": "0.9.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -23,8 +23,7 @@
23
23
  "url": "https://github.com/amritk/mjst/issues"
24
24
  },
25
25
  "files": [
26
- "dist",
27
- "src"
26
+ "dist"
28
27
  ],
29
28
  "publishConfig": {
30
29
  "access": "public"
@@ -42,14 +41,13 @@
42
41
  },
43
42
  "exports": {
44
43
  ".": {
45
- "development": "./src/index.ts",
46
44
  "default": "./dist/index.js",
47
45
  "types": "./dist/index.d.ts"
48
46
  }
49
47
  },
50
48
  "dependencies": {
51
49
  "json-schema-typed": "^8.0.1",
52
- "@amritk/helpers": "0.8.0",
50
+ "@amritk/helpers": "0.10.0",
53
51
  "@amritk/generate-markdown": "0.2.4"
54
52
  }
55
53
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"build-schema.d.ts","sourceRoot":"","sources":["../../src/generators/build-schema.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAEjE,OAAO,KAAK,EAAE,WAAW,EAAqB,MAAM,0BAA0B,CAAA;AAC9E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAahE;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,eAAO,MAAM,WAAW,eACV,UAAU,gBACR,MAAM,eACP,gBAAgB,cACjB,OAAO,gBACL,OAAO,WACZ,OAAO,gBACH,WAAW,4EAIvB,OAAO,CAAC,aAAa,EAAE,CAyCzB,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-default-from-pattern.d.ts","sourceRoot":"","sources":["../../src/generators/generate-default-from-pattern.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,0BAA0B,YAAa,MAAM,KAAG,MAAM,GAAG,IAkCrE,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-discriminated-union-validation.d.ts","sourceRoot":"","sources":["../../src/generators/generate-discriminated-union-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAMjE;;;GAGG;AACH,eAAO,MAAM,oCAAoC,aACrC,MAAM,WACP,SAAS,UAAU,EAAE,oBACZ,MAAM,gBACV,MAAM,cACR,OAAO,KAClB,MAqCF,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-enum-check.d.ts","sourceRoot":"","sources":["../../src/generators/generate-enum-check.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,iBAAiB,aAAc,MAAM,cAAc,SAAS,OAAO,EAAE,KAAG,MAGpF,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-files.d.ts","sourceRoot":"","sources":["../../src/generators/generate-files.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AACjE,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAA;AAKjC;;GAEG;AACH,KAAK,mBAAmB,GAAG;IACzB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7C;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAA;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;IACrC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED,iDAAiD;AACjD,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,iBAAiB,CAAC,CAAA;CACrD,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,YAAY,WACf,UAAU,YACR,MAAM,YACN,mBAAmB,KAC5B,oBA+CF,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-non-discriminated-union-validation.d.ts","sourceRoot":"","sources":["../../src/generators/generate-non-discriminated-union-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAIjE;;;GAGG;AACH,eAAO,MAAM,uCAAuC,aACxC,MAAM,WACP,SAAS,UAAU,EAAE,gBAChB,MAAM,cACR,OAAO,KAClB,MA0BF,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-parser-function.d.ts","sourceRoot":"","sources":["../../src/generators/generate-parser-function.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAMjE;;GAEG;AACH,KAAK,qBAAqB,GAAG;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAA;IAChC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AA4pCD;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,WACzB,UAAU,YACR,MAAM,YACN,qBAAqB,KAC9B,MAEF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,0BAA0B,aAAc,MAAM,KAAG,MAAsC,CAAA;AAEpG;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB,WACzB,UAAU,YACR,MAAM,iBACD,OAAO,sBAErB,MAyDF,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-schema-checks.d.ts","sourceRoot":"","sources":["../../src/generators/generate-schema-checks.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAIjE;;GAEG;AACH,eAAO,MAAM,oBAAoB,aAAc,MAAM,UAAU,UAAU,KAAG,MAAM,EAkGjF,CAAA"}