typescript-runtime-schemas
Version:
A TypeScript schema generation tool that extracts Zod schemas from TypeScript source files with runtime validation support. Generate validation schemas directly from your existing TypeScript types with support for computed types and constraint-based valid
391 lines (390 loc) • 16.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const zod_1 = require("zod");
const zod_schema_generator_1 = require("./zod-schema-generator");
describe("ZodSchemaGenerator", () => {
describe("generateSchemas", () => {
it("should generate Zod schemas from extracted schemas", () => {
const extractedSchemas = [
{
typeName: "TestUser",
schema: {
id: {
type: "number",
required: true,
constraints: { min: 1 },
},
name: {
type: "string",
required: true,
constraints: { minLength: 2, maxLength: 50 },
},
email: {
type: "string",
required: true,
constraints: { email: true },
},
age: {
type: "number",
required: false,
constraints: { min: 18, max: 120 },
},
},
},
];
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSchemas(extractedSchemas);
expect(result).toHaveLength(1);
expect(result[0].typeName).toBe("TestUser");
expect(result[0].zodSchema).toBeInstanceOf(zod_1.z.ZodObject);
// Test the generated schema by parsing valid data
const validData = {
id: 123,
name: "John Doe",
email: "john@example.com",
age: 25,
};
const parsed = result[0].zodSchema.parse(validData);
expect(parsed).toEqual(validData);
});
it("should include source info when requested", () => {
const extractedSchemas = [
{
typeName: "TestType",
schema: {
value: {
type: "string",
required: true,
constraints: {},
},
},
sourceInfo: {
filePath: "/test/file.ts",
line: 10,
},
},
];
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSchemas(extractedSchemas, true);
expect(result[0].sourceInfo).toEqual({
filePath: "/test/file.ts",
line: 10,
});
});
it("should exclude source info when not requested", () => {
const extractedSchemas = [
{
typeName: "TestType",
schema: {
value: {
type: "string",
required: true,
constraints: {},
},
},
sourceInfo: {
filePath: "/test/file.ts",
line: 10,
},
},
];
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSchemas(extractedSchemas, false);
expect(result[0].sourceInfo).toBeUndefined();
});
});
describe("constraint mapping", () => {
it("should handle numeric constraints", () => {
const schema = {
type: "number",
required: true,
constraints: {
min: 10,
max: 100,
integer: true,
positive: true,
},
};
const extractedSchema = {
typeName: "NumericTest",
schema: { value: schema },
};
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSingle(extractedSchema);
const zodSchema = result.zodSchema;
// Test valid values
expect(() => zodSchema.parse({ value: 50 })).not.toThrow();
expect(() => zodSchema.parse({ value: 10 })).not.toThrow();
expect(() => zodSchema.parse({ value: 100 })).not.toThrow();
// Test invalid values
expect(() => zodSchema.parse({ value: 5 })).toThrow();
expect(() => zodSchema.parse({ value: 150 })).toThrow();
expect(() => zodSchema.parse({ value: -10 })).toThrow();
});
it("should handle string constraints", () => {
const schema = {
type: "string",
required: true,
constraints: {
minLength: 3,
maxLength: 20,
email: true,
},
};
const extractedSchema = {
typeName: "StringTest",
schema: { email: schema },
};
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSingle(extractedSchema);
const zodSchema = result.zodSchema;
// Test valid email
expect(() => zodSchema.parse({ email: "test@example.com" })).not.toThrow();
// Test invalid emails
expect(() => zodSchema.parse({ email: "ab" })).toThrow(); // Too short
expect(() => zodSchema.parse({ email: "invalid-email" })).toThrow(); // Not email format
});
it("should handle array constraints", () => {
const schema = {
type: "array",
required: true,
constraints: {
minLength: 1,
maxLength: 5,
},
arrayElementType: {
type: "string",
required: true,
constraints: {},
},
};
const extractedSchema = {
typeName: "ArrayTest",
schema: { tags: schema },
};
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSingle(extractedSchema);
const zodSchema = result.zodSchema;
// Test valid arrays
expect(() => zodSchema.parse({ tags: ["tag1"] })).not.toThrow();
expect(() => zodSchema.parse({ tags: ["tag1", "tag2", "tag3"] })).not.toThrow();
// Test invalid arrays
expect(() => zodSchema.parse({ tags: [] })).toThrow(); // Too short
expect(() => zodSchema.parse({ tags: ["1", "2", "3", "4", "5", "6"] })).toThrow(); // Too long
});
it("should handle nested objects", () => {
const schema = {
type: "object",
required: true,
constraints: {},
properties: {
street: {
type: "string",
required: true,
constraints: { minLength: 5 },
},
city: {
type: "string",
required: true,
constraints: { minLength: 2 },
},
zipCode: {
type: "string",
required: false,
constraints: { regex: "^[0-9]{5}$" },
},
},
};
const extractedSchema = {
typeName: "NestedTest",
schema: { address: schema },
};
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSingle(extractedSchema);
const zodSchema = result.zodSchema;
// Test valid nested object
const validData = {
address: {
street: "123 Main Street",
city: "New York",
zipCode: "12345",
},
};
expect(() => zodSchema.parse(validData)).not.toThrow();
// Test invalid nested object
const invalidData = {
address: {
street: "123", // Too short
city: "NY",
zipCode: "invalid", // Invalid format
},
};
expect(() => zodSchema.parse(invalidData)).toThrow();
});
it("should handle optional properties", () => {
const schema = {
type: "string",
required: false,
constraints: { minLength: 3 },
};
const extractedSchema = {
typeName: "OptionalTest",
schema: { optionalField: schema },
};
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSingle(extractedSchema);
const zodSchema = result.zodSchema;
// Test with optional field present
expect(() => zodSchema.parse({ optionalField: "test" })).not.toThrow();
// Test with optional field missing
expect(() => zodSchema.parse({})).not.toThrow();
// Test with invalid optional field
expect(() => zodSchema.parse({ optionalField: "ab" })).toThrow();
});
it("should handle custom constraints with refinements", () => {
const schema = {
type: "string",
required: true,
constraints: {
phone: true,
},
};
const extractedSchema = {
typeName: "PhoneTest",
schema: { phoneNumber: schema },
};
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSingle(extractedSchema);
const zodSchema = result.zodSchema;
// Test valid phone numbers
expect(() => zodSchema.parse({ phoneNumber: "+1234567890" })).not.toThrow();
expect(() => zodSchema.parse({ phoneNumber: "1234567890" })).not.toThrow();
// Test invalid phone numbers
expect(() => zodSchema.parse({ phoneNumber: "invalid" })).toThrow();
expect(() => zodSchema.parse({ phoneNumber: "0123" })).toThrow();
});
it("should handle regex constraints", () => {
const schema = {
type: "string",
required: true,
constraints: {
regex: "^[A-Z]{2}[0-9]{4}$",
},
};
const extractedSchema = {
typeName: "RegexTest",
schema: { code: schema },
};
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSingle(extractedSchema);
const zodSchema = result.zodSchema;
// Test valid codes
expect(() => zodSchema.parse({ code: "AB1234" })).not.toThrow();
expect(() => zodSchema.parse({ code: "XY9876" })).not.toThrow();
// Test invalid codes
expect(() => zodSchema.parse({ code: "ab1234" })).toThrow(); // Lowercase
expect(() => zodSchema.parse({ code: "ABC123" })).toThrow(); // Wrong format
});
it("should handle oneOf constraints", () => {
const schema = {
type: "string",
required: true,
constraints: {
oneOf: ["red", "green", "blue"],
},
};
const extractedSchema = {
typeName: "EnumTest",
schema: { color: schema },
};
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSingle(extractedSchema);
const zodSchema = result.zodSchema;
// Test valid enum values
expect(() => zodSchema.parse({ color: "red" })).not.toThrow();
expect(() => zodSchema.parse({ color: "green" })).not.toThrow();
expect(() => zodSchema.parse({ color: "blue" })).not.toThrow();
// Test invalid enum values
expect(() => zodSchema.parse({ color: "yellow" })).toThrow();
expect(() => zodSchema.parse({ color: "Red" })).toThrow(); // Case sensitive
});
});
describe("integration with pipeline", () => {
it("should work with the full pipeline", async () => {
const sourceCode = `
// Constraint definitions
type SupportsRuntimeValidation = {};
type Constraint<K extends any> = {};
type Min<N extends number> = Constraint<N>;
type Max<N extends number> = Constraint<N>;
type MinLength<N extends number> = Constraint<N>;
type MaxLength<N extends number> = Constraint<N>;
type Email = Constraint<string>;
// Validated type
type ValidatedUser = {
id: number & Min<1>;
username: string & MinLength<3> & MaxLength<20>;
email: string & Email;
tags: string[] & MinLength<1> & MaxLength<5>;
profile?: {
firstName: string & MinLength<1>;
lastName: string & MinLength<1>;
};
} & SupportsRuntimeValidation;
`;
const zodSchemas = await (0, zod_schema_generator_1.generateZodSchemasFromSource)(sourceCode);
expect(zodSchemas).toHaveLength(1);
expect(zodSchemas[0].typeName).toBe("ValidatedUser");
const schema = zodSchemas[0].zodSchema;
// Test valid data
const validUser = {
id: 123,
username: "johndoe",
email: "john@example.com",
tags: ["developer", "typescript"],
profile: {
firstName: "John",
lastName: "Doe",
},
};
expect(() => schema.parse(validUser)).not.toThrow();
// Test invalid data
const invalidUser = {
id: 0, // Below minimum
username: "jo", // Too short
email: "invalid-email", // Invalid format
tags: [], // Empty array
};
expect(() => schema.parse(invalidUser)).toThrow();
});
});
describe("error handling", () => {
it("should handle unknown constraints gracefully", () => {
const schema = {
type: "string",
required: true,
constraints: {
unknownConstraint: "someValue",
},
};
const extractedSchema = {
typeName: "UnknownConstraintTest",
schema: { value: schema },
};
// Should not throw, but log a warning
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSingle(extractedSchema);
expect(result.zodSchema).toBeInstanceOf(zod_1.z.ZodObject);
expect(consoleSpy).toHaveBeenCalledWith("Unknown constraint: unknownConstraint with value:", "someValue");
consoleSpy.mockRestore();
});
it("should handle invalid regex patterns gracefully", () => {
const schema = {
type: "string",
required: true,
constraints: {
regex: "[invalid regex",
},
};
const extractedSchema = {
typeName: "InvalidRegexTest",
schema: { value: schema },
};
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
const result = zod_schema_generator_1.ZodSchemaGenerator.generateSingle(extractedSchema);
expect(result.zodSchema).toBeInstanceOf(zod_1.z.ZodObject);
expect(consoleSpy).toHaveBeenCalledWith("Invalid regex pattern: [invalid regex", expect.any(Error));
consoleSpy.mockRestore();
});
});
});
//# sourceMappingURL=zod-schema-generator.test.js.map