@amritk/generate-validators 0.9.0 → 0.10.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.
@@ -25,7 +25,8 @@ type GenerateValidatorFileOptions = {
25
25
  * - Imports for the ValidationResult/ValidationError types
26
26
  * - Imports for any $ref types and their validator functions
27
27
  * - The exported TypeScript type definition
28
- * - The exported validator function
28
+ * - The exported validator function (`validateX`, rich `ValidationResult`)
29
+ * - The exported boolean type-guard (`isX`, a flat `input is X` predicate)
29
30
  *
30
31
  * @example
31
32
  * ```typescript
@@ -38,6 +39,7 @@ type GenerateValidatorFileOptions = {
38
39
  * // import type { ValidationResult, ValidationError } from './validation-result'
39
40
  * // export type Info = { title: string }
40
41
  * // export const validateInfo = (input: unknown, _path = ''): ValidationResult => { ... }
42
+ * // export const isInfo = (input: unknown): input is Info => { ... }
41
43
  * ```
42
44
  */
43
45
  export declare const generateValidatorFile: (schema: JSONSchema, typeName: string, options?: GenerateValidatorFileOptions) => string;
@@ -1,6 +1,6 @@
1
1
  import { generateTypeDefinition } from '@amritk/helpers/generate-type-definition';
2
2
  import { collectValidatorImports } from './collect-validator-imports.js';
3
- import { generateValidatorFunction } from './generate-validator-function.js';
3
+ import { generateBooleanGuard, generateValidatorFunction } from './generate-validator-function.js';
4
4
  /**
5
5
  * Generates a complete TypeScript validator file from a JSON Schema.
6
6
  *
@@ -8,7 +8,8 @@ import { generateValidatorFunction } from './generate-validator-function.js';
8
8
  * - Imports for the ValidationResult/ValidationError types
9
9
  * - Imports for any $ref types and their validator functions
10
10
  * - The exported TypeScript type definition
11
- * - The exported validator function
11
+ * - The exported validator function (`validateX`, rich `ValidationResult`)
12
+ * - The exported boolean type-guard (`isX`, a flat `input is X` predicate)
12
13
  *
13
14
  * @example
14
15
  * ```typescript
@@ -21,6 +22,7 @@ import { generateValidatorFunction } from './generate-validator-function.js';
21
22
  * // import type { ValidationResult, ValidationError } from './validation-result'
22
23
  * // export type Info = { title: string }
23
24
  * // export const validateInfo = (input: unknown, _path = ''): ValidationResult => { ... }
25
+ * // export const isInfo = (input: unknown): input is Info => { ... }
24
26
  * ```
25
27
  */
26
28
  export const generateValidatorFile = (schema, typeName, options) => {
@@ -32,6 +34,7 @@ export const generateValidatorFile = (schema, typeName, options) => {
32
34
  });
33
35
  const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix });
34
36
  const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix);
37
+ const booleanGuard = generateBooleanGuard(schema, typeName, typeSuffix);
35
38
  let result = `import type { ValidationResult, ValidationError } from './validation-result'\n`;
36
39
  // `const` checks on object/array values call the runtime `valuesEqual` helper.
37
40
  // Only import it when the generated body actually uses it, so files without a
@@ -48,6 +51,6 @@ export const generateValidatorFile = (schema, typeName, options) => {
48
51
  else {
49
52
  result += '\n';
50
53
  }
51
- result += typeDefinition + '\n\n' + validatorFunction;
54
+ result += typeDefinition + '\n\n' + validatorFunction + '\n\n' + booleanGuard;
52
55
  return result;
53
56
  };
@@ -1,4 +1,15 @@
1
1
  import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Generates the exported boolean type-guard `isTypeName(input): input is TypeName`.
4
+ *
5
+ * Unlike `validateTypeName` (which returns rich `ValidationResult` errors), this
6
+ * is a single flat boolean predicate — no error array, no cold-path call — so V8
7
+ * inlines it like a hand-written `check`, matching the shape of TypeBox's
8
+ * compiled checker. It returns the *same verdict* as the validator. When the
9
+ * schema carries anything the flat form can't mirror exactly, it falls back to
10
+ * `validateTypeName(input) === true`, which is always correct.
11
+ */
12
+ export declare const generateBooleanGuard: (schema: JSONSchema, typeName: string, _suffix?: string) => string;
2
13
  /**
3
14
  * Generates a TypeScript validator function from a JSON Schema.
4
15
  *
@@ -633,6 +633,219 @@ const generateObjectValidator = (schema, typeName, suffix) => {
633
633
  `}`,
634
634
  ].join('\n');
635
635
  };
636
+ /**
637
+ * Derives the boolean type-guard name from a type name.
638
+ * e.g. "InfoObject" → "isInfoObject"
639
+ */
640
+ const guardName = (typeName) => `is${typeName}`;
641
+ /**
642
+ * Positive type check for a value — the negation of {@link wrongTypeCondition}.
643
+ * Used by the boolean type-guard, which proves validity with `&&` conditions
644
+ * rather than collecting errors. Object is a shape-only check (matching the
645
+ * validator, which never recurses into array items or untyped object values).
646
+ */
647
+ const rightTypeCondition = (accessor, type) => {
648
+ switch (type) {
649
+ case 'string':
650
+ return `typeof ${accessor} === 'string'`;
651
+ case 'number':
652
+ case 'integer':
653
+ return `typeof ${accessor} === 'number'`;
654
+ case 'boolean':
655
+ return `typeof ${accessor} === 'boolean'`;
656
+ case 'array':
657
+ return `Array.isArray(${accessor})`;
658
+ case 'object':
659
+ return `typeof ${accessor} === 'object' && ${accessor} !== null && !Array.isArray(${accessor})`;
660
+ default:
661
+ return null;
662
+ }
663
+ };
664
+ /**
665
+ * Builds a boolean expression that is TRUE iff `acc` satisfies `schema`, with the
666
+ * *exact same verdict* as the error-collecting validator — or `null` when the
667
+ * schema carries something the flat form can't faithfully mirror ($ref, unions,
668
+ * `const`, x-mjst, etc.), in which case the whole guard falls back to calling the
669
+ * validator. Used for a property value or an array item; `acc` is the expression
670
+ * yielding the value.
671
+ */
672
+ const booleanLeafExpr = (schema, acc) => {
673
+ if (!isSchemaObject(schema))
674
+ return null;
675
+ // Anything whose verdict the flat form can't mirror exactly: defer to the
676
+ // validator (the caller turns a single `null` into a full fallback guard).
677
+ if (hasRef(schema) ||
678
+ hasConst(schema) ||
679
+ hasOneOf(schema) ||
680
+ 'anyOf' in schema ||
681
+ 'allOf' in schema ||
682
+ 'not' in schema ||
683
+ getMjstInstanceOf(schema) !== undefined ||
684
+ getMjstPrimitive(schema) !== undefined) {
685
+ return null;
686
+ }
687
+ // enum — same membership test the validator uses.
688
+ if (hasEnum(schema)) {
689
+ return `(${JSON.stringify(schema.enum)} as unknown[]).includes(${acc})`;
690
+ }
691
+ if (!hasType(schema))
692
+ return null;
693
+ const t = schema.type;
694
+ switch (t) {
695
+ // Each constraint is the exact negation of the validator's error condition
696
+ // (`!(len < min)`, not `len >= min`) so edge values — most importantly `NaN`,
697
+ // which the validator accepts for a constrained number since `NaN < min` is
698
+ // false — get the identical verdict.
699
+ case 'string': {
700
+ const parts = [`typeof ${acc} === 'string'`];
701
+ if (hasPattern(schema))
702
+ parts.push(`/${escapeRegexPattern(schema.pattern)}/.test(${acc})`);
703
+ if (hasMinLength(schema))
704
+ parts.push(`!(${acc}.length < ${schema.minLength})`);
705
+ if (hasMaxLength(schema))
706
+ parts.push(`!(${acc}.length > ${schema.maxLength})`);
707
+ return parts.join(' && ');
708
+ }
709
+ case 'number':
710
+ case 'integer': {
711
+ const parts = [`typeof ${acc} === 'number'`];
712
+ if (hasMinimum(schema))
713
+ parts.push(`!(${acc} ${hasStrictExclusiveMinimum(schema) ? '<=' : '<'} ${schema.minimum})`);
714
+ if (hasMaximum(schema))
715
+ parts.push(`!(${acc} ${hasStrictExclusiveMaximum(schema) ? '>=' : '>'} ${schema.maximum})`);
716
+ if (hasExclusiveMinimum(schema))
717
+ parts.push(`!(${acc} <= ${schema.exclusiveMinimum})`);
718
+ if (hasExclusiveMaximum(schema))
719
+ parts.push(`!(${acc} >= ${schema.exclusiveMaximum})`);
720
+ if (hasMultipleOf(schema))
721
+ parts.push(`${acc} % ${schema.multipleOf} === 0`);
722
+ return parts.join(' && ');
723
+ }
724
+ case 'boolean':
725
+ return `typeof ${acc} === 'boolean'`;
726
+ case 'object': {
727
+ const parts = booleanObjectParts(schema, acc, `(${acc} as Record<string, unknown>)`);
728
+ return parts === null ? null : parts.join(' && ');
729
+ }
730
+ case 'array':
731
+ return booleanArrayExpr(schema, acc);
732
+ default:
733
+ return null;
734
+ }
735
+ };
736
+ /**
737
+ * Boolean expression for an array value. Mirrors the validator, which checks the
738
+ * array shape and — for typed items — only each item's *type* (objects are shape-
739
+ * checked, not recursed into); it never enforces `minItems`/`maxItems` or item
740
+ * constraints. Returns `null` for `$ref` items (those defer to the validator).
741
+ */
742
+ const booleanArrayExpr = (schema, acc) => {
743
+ const base = `Array.isArray(${acc})`;
744
+ if (!hasItems(schema))
745
+ return base;
746
+ const items = schema.items;
747
+ if (!isSchemaObject(items))
748
+ return base;
749
+ if (hasRef(items))
750
+ return null;
751
+ if (!hasType(items))
752
+ return base;
753
+ const itemCheck = rightTypeCondition('_it', items.type);
754
+ if (itemCheck === null)
755
+ return base;
756
+ return `${base} && (${acc} as unknown[]).every((_it) => ${itemCheck})`;
757
+ };
758
+ /**
759
+ * Builds the `&&` conditions proving an object value is valid (same verdict as
760
+ * the error-collecting validator), or `null` when any property or object-level
761
+ * keyword can't be mirrored flat. `raw` yields the value (for the shape check);
762
+ * `objAcc` is the same value narrowed to a record (for member access).
763
+ */
764
+ const booleanObjectParts = (schema, raw, objAcc) => {
765
+ if (!isObjectSchema(schema))
766
+ return null;
767
+ // These need per-key loops or cross-references the flat form can't express.
768
+ if (hasDependentRequired(schema) || hasPropertyNames(schema))
769
+ return null;
770
+ if (isSchemaObject(schema) && 'patternProperties' in schema)
771
+ return null;
772
+ let strict = false;
773
+ if (hasAdditionalProperties(schema)) {
774
+ // Only `additionalProperties: false` is expressible; a schema needs per-key
775
+ // validation, so defer to the validator.
776
+ if (schema.additionalProperties === false)
777
+ strict = true;
778
+ else
779
+ return null;
780
+ }
781
+ const required = new Set(hasRequired(schema) ? schema.required : []);
782
+ const properties = hasProperties(schema) ? schema.properties : {};
783
+ const keys = Object.keys(properties);
784
+ // Drop the `!Array.isArray` term when a required, typeof-guarded property
785
+ // already rejects arrays (an array's normal key is `undefined`, which no
786
+ // `typeof` accepts) — the same sound optimisation the validator's hot guard
787
+ // uses. Kept when no such property exists.
788
+ const arrayCheck = arrayRejectedByRequiredProp(keys, required, properties) ? '' : ` && !Array.isArray(${raw})`;
789
+ const parts = [`typeof ${raw} === 'object' && ${raw} !== null${arrayCheck}`];
790
+ for (const key of keys) {
791
+ const propSchema = properties[key];
792
+ if (propSchema === undefined || !isSchemaObject(propSchema))
793
+ return null;
794
+ const member = safeAccessor(objAcc, key);
795
+ const expr = booleanLeafExpr(propSchema, member);
796
+ if (expr === null)
797
+ return null;
798
+ parts.push(required.has(key) ? expr : `(${member} === undefined || (${expr}))`);
799
+ }
800
+ if (strict) {
801
+ // `additionalProperties: false`: with every property required, an exact key
802
+ // count proves no extras (the typeof checks above already proved presence);
803
+ // otherwise sweep the keys against the declared set.
804
+ if (keys.length === 0) {
805
+ parts.push(`Object.keys(${objAcc}).length === 0`);
806
+ }
807
+ else if (keys.every((key) => required.has(key))) {
808
+ parts.push(`Object.keys(${objAcc}).length === ${keys.length}`);
809
+ }
810
+ else {
811
+ const known = keys.map((key) => `_k === ${JSON.stringify(key)}`).join(' || ');
812
+ parts.push(`Object.keys(${objAcc}).every((_k) => ${known})`);
813
+ }
814
+ }
815
+ return parts;
816
+ };
817
+ /**
818
+ * Generates the exported boolean type-guard `isTypeName(input): input is TypeName`.
819
+ *
820
+ * Unlike `validateTypeName` (which returns rich `ValidationResult` errors), this
821
+ * is a single flat boolean predicate — no error array, no cold-path call — so V8
822
+ * inlines it like a hand-written `check`, matching the shape of TypeBox's
823
+ * compiled checker. It returns the *same verdict* as the validator. When the
824
+ * schema carries anything the flat form can't mirror exactly, it falls back to
825
+ * `validateTypeName(input) === true`, which is always correct.
826
+ */
827
+ export const generateBooleanGuard = (schema, typeName, _suffix = '') => {
828
+ const name = guardName(typeName);
829
+ const fallback = `export const ${name} = (input: unknown): input is ${typeName} => ${validatorName(typeName)}(input) === true`;
830
+ if (isObjectSchema(schema)) {
831
+ const parts = booleanObjectParts(schema, 'input', 'obj');
832
+ if (parts === null)
833
+ return fallback;
834
+ return [
835
+ `export const ${name} = (input: unknown): input is ${typeName} => {`,
836
+ ` const obj = input as Record<string, unknown>`,
837
+ ` return (`,
838
+ parts.map((part) => ` ${part}`).join(' &&\n'),
839
+ ` )`,
840
+ `}`,
841
+ ].join('\n');
842
+ }
843
+ // Non-object roots (scalar, enum, array) can often be expressed inline too.
844
+ const expr = booleanLeafExpr(schema, 'input');
845
+ if (expr === null)
846
+ return fallback;
847
+ return `export const ${name} = (input: unknown): input is ${typeName} => ${expr}`;
848
+ };
636
849
  /**
637
850
  * Generates a validator function for a non-object schema (primitive, array, enum, $ref).
638
851
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-validators",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Generate TypeScript validation functions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",