UNPKG

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

480 lines (475 loc) 24.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const type_resolver_1 = require("./type-resolver"); describe('TypeResolver Unit Tests', () => { describe('Basic Utility Types', () => { const basicSource = ` type User = { id: number; name: string; email: string; age?: number; } type UserProfile = Pick<User, 'name' | 'email'> type UserWithoutEmail = Omit<User, 'email'> type PartialUser = Partial<User> type RequiredUser = Required<User> type ReadonlyUser = Readonly<User> `; let resolver; beforeEach(() => { resolver = new type_resolver_1.TypeResolver(basicSource); }); test('should resolve Pick utility type', () => { const result = resolver.getResolvedType('UserProfile'); expect(result).toBe('{ name: string; email: string }'); }); test('should resolve Omit utility type', () => { const result = resolver.getResolvedType('UserWithoutEmail'); expect(result).toBe('{ id: number; name: string; age?: number }'); }); test('should resolve Partial utility type', () => { const result = resolver.getResolvedType('PartialUser'); expect(result).toBe('{ id?: number; name?: string; email?: string; age?: number }'); }); test('should resolve Required utility type', () => { const result = resolver.getResolvedType('RequiredUser'); expect(result).toBe('{ id: number; name: string; email: string; age: number }'); }); test('should resolve Readonly utility type', () => { const result = resolver.getResolvedType('ReadonlyUser'); expect(result).toBe('{ readonly id: number; readonly name: string; readonly email: string; readonly age?: number }'); }); test('should resolve base type without utility', () => { const result = resolver.getResolvedType('User'); expect(result).toBe('{ id: number; name: string; email: string; age?: number }'); }); }); describe('Record and Union Types', () => { const recordSource = ` type Role = 'admin' | 'user' | 'guest' type User = { id: number; name: string; } type RolePermissions = Record<Role, boolean> type UsersByRole = Record<Role, User> type StringMap = Record<string, number> `; let resolver; beforeEach(() => { resolver = new type_resolver_1.TypeResolver(recordSource); }); test('should resolve Record with union keys', () => { const result = resolver.getResolvedType('RolePermissions'); expect(result).toBe("{ [key: 'admin' | 'user' | 'guest']: boolean }"); }); test('should resolve Record with complex value type', () => { const result = resolver.getResolvedType('UsersByRole'); expect(result).toBe("{ [key: 'admin' | 'user' | 'guest']: { id: number; name: string } }"); }); test('should resolve Record with string index', () => { const result = resolver.getResolvedType('StringMap'); expect(result).toBe('{ [key: string]: number }'); }); }); describe('Union Manipulation Types', () => { const unionSource = ` type Status = 'active' | 'inactive' | 'pending' | null | undefined type StringOrNumber = string | number | null type NonNullableStatus = NonNullable<Status> type NonNullableStringOrNumber = NonNullable<StringOrNumber> type ActiveStatuses = Exclude<Status, null | undefined> type NullishStatuses = Extract<Status, null | undefined> type ExcludeString = Exclude<StringOrNumber, string> type ExtractString = Extract<StringOrNumber, string> `; let resolver; beforeEach(() => { resolver = new type_resolver_1.TypeResolver(unionSource); }); test('should resolve NonNullable utility type', () => { const result = resolver.getResolvedType('NonNullableStatus'); expect(result).toBe("'active' | 'inactive' | 'pending'"); }); test('should resolve NonNullable with mixed types', () => { const result = resolver.getResolvedType('NonNullableStringOrNumber'); expect(result).toBe('string | number'); }); test('should resolve Exclude utility type', () => { const result = resolver.getResolvedType('ActiveStatuses'); expect(result).toBe("'active' | 'inactive' | 'pending'"); }); test('should resolve Extract utility type', () => { const result = resolver.getResolvedType('NullishStatuses'); expect(result).toBe('null | undefined'); }); test('should resolve Exclude with single type', () => { const result = resolver.getResolvedType('ExcludeString'); expect(result).toBe('number | null'); }); test('should resolve Extract with single type', () => { const result = resolver.getResolvedType('ExtractString'); expect(result).toBe('string'); }); }); describe('Function Types', () => { const functionSource = ` type SimpleFunction = () => string type ComplexFunction = (a: string, b: number) => boolean type AsyncFunction = (id: number) => Promise<User> type User = { id: number; name: string; } type SimpleFunctionReturn = ReturnType<SimpleFunction> type ComplexFunctionReturn = ReturnType<ComplexFunction> type AsyncFunctionReturn = ReturnType<AsyncFunction> type SimpleFunctionParams = Parameters<SimpleFunction> type ComplexFunctionParams = Parameters<ComplexFunction> type AsyncFunctionParams = Parameters<AsyncFunction> `; let resolver; beforeEach(() => { resolver = new type_resolver_1.TypeResolver(functionSource); }); test('should resolve ReturnType for simple function', () => { const result = resolver.getResolvedType('SimpleFunctionReturn'); expect(result).toBe('string'); }); test('should resolve ReturnType for complex function', () => { const result = resolver.getResolvedType('ComplexFunctionReturn'); expect(result).toBe('boolean'); }); test('should resolve ReturnType for async function', () => { const result = resolver.getResolvedType('AsyncFunctionReturn'); expect(result).toBe('Promise<User>'); }); test('should resolve Parameters for simple function', () => { const result = resolver.getResolvedType('SimpleFunctionParams'); expect(result).toBe('[]'); }); test('should resolve Parameters for complex function', () => { const result = resolver.getResolvedType('ComplexFunctionParams'); expect(result).toBe('[string, number]'); }); test('should resolve Parameters for async function', () => { const result = resolver.getResolvedType('AsyncFunctionParams'); expect(result).toBe('[number]'); }); }); describe('Constructor Types', () => { const constructorSource = ` type User = { id: number; name: string; } type UserConstructor = new (name: string, email: string) => User type SimpleConstructor = new () => string type UserConstructorParams = ConstructorParameters<UserConstructor> type SimpleConstructorParams = ConstructorParameters<SimpleConstructor> type UserInstance = InstanceType<UserConstructor> type SimpleInstance = InstanceType<SimpleConstructor> `; let resolver; beforeEach(() => { resolver = new type_resolver_1.TypeResolver(constructorSource); }); test('should resolve ConstructorParameters with parameters', () => { const result = resolver.getResolvedType('UserConstructorParams'); expect(result).toBe('[string, string]'); }); test('should resolve ConstructorParameters without parameters', () => { const result = resolver.getResolvedType('SimpleConstructorParams'); expect(result).toBe('[]'); }); test('should resolve InstanceType', () => { const result = resolver.getResolvedType('UserInstance'); expect(result).toContain('InstanceType<'); }); }); describe('Generic Types', () => { const genericSource = ` type Container<T> = { value: T; label: string } type ApiResponse<T, E = Error> = { data: T; error?: E; status: number } type User = { id: number; name: string; } type StringContainer = Container<string> type NumberContainer = Container<number> type UserContainer = Container<User> type UserResponse = ApiResponse<User> type UserListResponse = ApiResponse<User[]> `; let resolver; beforeEach(() => { resolver = new type_resolver_1.TypeResolver(genericSource); }); test('should resolve simple generic type', () => { const result = resolver.getResolvedType('StringContainer'); expect(result).toBe('{ value: string; label: string }'); }); test('should resolve generic type with number', () => { const result = resolver.getResolvedType('NumberContainer'); expect(result).toBe('{ value: number; label: string }'); }); test('should resolve generic type with complex type', () => { const result = resolver.getResolvedType('UserContainer'); expect(result).toBe('{ value: { id: number; name: string }; label: string }'); }); test('should resolve generic type with default parameter', () => { const result = resolver.getResolvedType('UserResponse'); expect(result).toBe('{ data: { id: number; name: string }; error?: E; status: number }'); }); test('should resolve generic type with array parameter', () => { const result = resolver.getResolvedType('UserListResponse'); expect(result).toBe('{ data: User[]; error?: E; status: number }'); }); }); describe('Array and Promise Types', () => { const arrayPromiseSource = ` type User = { id: number; name: string; } type UserArray = Array<User> type StringArray = Array<string> type UserPromise = Promise<User> type StringPromise = Promise<string> type NestedPromise = Promise<Promise<User>> `; let resolver; beforeEach(() => { resolver = new type_resolver_1.TypeResolver(arrayPromiseSource); }); test('should resolve Array type with complex element', () => { const result = resolver.getResolvedType('UserArray'); expect(result).toBe('{ id: number; name: string }[]'); }); test('should resolve Array type with primitive element', () => { const result = resolver.getResolvedType('StringArray'); expect(result).toBe('string[]'); }); test('should resolve Promise type with complex value', () => { const result = resolver.getResolvedType('UserPromise'); expect(result).toBe('Promise<{ id: number; name: string }>'); }); test('should resolve Promise type with primitive value', () => { const result = resolver.getResolvedType('StringPromise'); expect(result).toBe('Promise<string>'); }); test('should resolve nested Promise type', () => { const result = resolver.getResolvedType('NestedPromise'); expect(result).toBe('Promise<Promise<{ id: number; name: string }>>'); }); }); describe('Complex Nested Types', () => { const complexSource = ` type Address = { street: string; city: string; country: string; zipCode?: string; } type Person = { id: number; name: string; email: string; address: Address; roles: string[]; } type PersonSummary = Pick<Person, 'name' | 'email'> type PersonWithoutAddress = Omit<Person, 'address'> type PartialPerson = Partial<Person> type ReadonlyPerson = Readonly<Person> type RequiredAddress = Required<Address> type PersonRecord = Record<'primary' | 'secondary', Person> `; let resolver; beforeEach(() => { resolver = new type_resolver_1.TypeResolver(complexSource); }); test('should resolve Pick with nested types', () => { const result = resolver.getResolvedType('PersonSummary'); expect(result).toBe('{ name: string; email: string }'); }); test('should resolve Omit preserving other complex properties', () => { const result = resolver.getResolvedType('PersonWithoutAddress'); expect(result).toBe('{ id: number; name: string; email: string; roles: string[] }'); }); test('should resolve Partial with nested objects', () => { const result = resolver.getResolvedType('PartialPerson'); expect(result).toBe('{ id?: number; name?: string; email?: string; address?: { street: string; city: string; country: string; zipCode?: string }; roles?: string[] }'); }); test('should resolve Readonly with nested objects', () => { const result = resolver.getResolvedType('ReadonlyPerson'); expect(result).toBe('{ readonly id: number; readonly name: string; readonly email: string; readonly address: { street: string; city: string; country: string; zipCode?: string }; readonly roles: string[] }'); }); test('should resolve Required with optional properties', () => { const result = resolver.getResolvedType('RequiredAddress'); expect(result).toBe('{ street: string; city: string; country: string; zipCode: string }'); }); test('should resolve Record with complex nested types', () => { const result = resolver.getResolvedType('PersonRecord'); expect(result).toContain('primary:'); expect(result).toContain('secondary:'); expect(result).toContain('{ id: number; name: string; email: string; address: { street: string; city: string; country: string; zipCode?: string }; roles: string[] }'); }); }); describe('Error Handling', () => { const validSource = ` type User = { id: number; name: string; } type UserProfile = Pick<User, 'name'> `; let resolver; beforeEach(() => { resolver = new type_resolver_1.TypeResolver(validSource); }); test('should throw error for non-existent type', () => { expect(() => { resolver.getResolvedType('NonExistentType'); }).toThrow('Type NonExistentType not found'); }); test('should handle empty source code', () => { const emptyResolver = new type_resolver_1.TypeResolver(''); expect(() => { emptyResolver.getResolvedType('AnyType'); }).toThrow('Type AnyType not found'); }); test('should handle malformed utility types gracefully', () => { const malformedSource = ` type User = { id: number; name: string; } type BadPick = Pick<User> `; const malformedResolver = new type_resolver_1.TypeResolver(malformedSource); const result = malformedResolver.getResolvedType('BadPick'); // Should return something reasonable rather than crashing expect(result).toBeDefined(); }); }); describe('Convenience Functions', () => { const testSource = ` type User = { id: number; name: string; email: string; } type UserProfile = Pick<User, 'name' | 'email'> type PartialUser = Partial<User> `; test('resolveTypesFromSource should resolve all types', () => { const result = (0, type_resolver_1.resolveTypesFromSource)(testSource); expect(result).toContain('type User ='); expect(result).toContain('type UserProfile ='); expect(result).toContain('type PartialUser ='); expect(result).toContain('{ name: string; email: string }'); }); test('getResolvedType should resolve specific type', () => { const result = (0, type_resolver_1.getResolvedType)(testSource, 'UserProfile'); expect(result).toBe('{ name: string; email: string }'); }); test('getResolvedType should throw for non-existent type', () => { expect(() => { (0, type_resolver_1.getResolvedType)(testSource, 'NonExistent'); }).toThrow('Type NonExistent not found'); }); }); describe('Edge Cases', () => { test('should handle types with special characters in names', () => { const specialSource = ` type User = { id: number; name: string; } type User_Profile = Pick<User, 'name'> `; const resolver = new type_resolver_1.TypeResolver(specialSource); const result = resolver.getResolvedType('User_Profile'); expect(result).toBe('{ name: string }'); }); test('should handle deeply nested utility types', () => { const nestedSource = ` type User = { id: number; name: string; email: string; age?: number; } type Step1 = Pick<User, 'name' | 'email' | 'age'> type Step2 = Required<Step1> type Step3 = Readonly<Step2> `; const resolver = new type_resolver_1.TypeResolver(nestedSource); const step1Result = resolver.getResolvedType('Step1'); expect(step1Result).toBe('{ name: string; email: string; age?: number }'); // For deeply nested utility types, the resolver may not fully resolve them // but should handle them gracefully without crashing const step3Result = resolver.getResolvedType('Step3'); expect(step3Result).toBeDefined(); expect(step3Result).toContain('Readonly<'); }); test('should handle union types in Pick/Omit keys', () => { const unionKeysSource = ` type User = { id: number; name: string; email: string; phone: string; } type ContactInfo = 'email' | 'phone' type UserContact = Pick<User, ContactInfo> `; const resolver = new type_resolver_1.TypeResolver(unionKeysSource); const result = resolver.getResolvedType('UserContact'); // This is a complex case that might not resolve perfectly, but shouldn't crash expect(result).toBeDefined(); }); }); describe('Predicate Filtering', () => { const predicateTestSource = ` type User = { id: number; name: string; email: string; age?: number; } type UserProfile = Pick<User, 'name' | 'email'> type UserWithoutEmail = Omit<User, 'email'> type PartialUser = Partial<User> type RequiredUser = Required<User> type ReadonlyUser = Readonly<User> type SimpleType = string type ComplexType = Record<string, User> `; let resolver; beforeEach(() => { resolver = new type_resolver_1.TypeResolver(predicateTestSource); }); test('should resolve all types when no predicate is provided', () => { const result = resolver.resolveTypes(); expect(result).toContain('type User ='); expect(result).toContain('type UserProfile = { name: string; email: string }'); expect(result).toContain('type UserWithoutEmail = { id: number; name: string; age?: number }'); expect(result).toContain('type PartialUser ='); expect(result).toContain('type RequiredUser ='); expect(result).toContain('type ReadonlyUser ='); expect(result).toContain('type SimpleType = string'); expect(result).toContain('type ComplexType ='); }); test('should resolve all types when predicate always returns true', () => { const result = resolver.resolveTypes(() => true); expect(result).toContain('type User ='); expect(result).toContain('type UserProfile = { name: string; email: string }'); expect(result).toContain('type UserWithoutEmail = { id: number; name: string; age?: number }'); expect(result).toContain('type PartialUser ='); expect(result).toContain('type RequiredUser ='); expect(result).toContain('type ReadonlyUser ='); expect(result).toContain('type SimpleType = string'); expect(result).toContain('type ComplexType ='); }); test('should return original types when predicate always returns false', () => { const result = resolver.resolveTypes(() => false); // When predicate returns false, types should remain unresolved expect(result).toContain("Pick<User, 'name' | 'email'>"); expect(result).toContain("Omit<User, 'email'>"); expect(result).toContain('Partial<User>'); expect(result).toContain('Required<User>'); expect(result).toContain('Readonly<User>'); }); test('should filter types based on name pattern', () => { // Only resolve types containing "Profile" in the name const result = resolver.resolveTypes((name) => name.includes('Profile')); // UserProfile should be resolved expect(result).toContain('{ name: string; email: string }'); // Other types should remain unresolved expect(result).toContain('Partial<User>'); expect(result).toContain('Required<User>'); }); test('should filter types based on utility type usage', () => { // Only resolve Pick and Omit utility types const result = resolver.resolveTypes((name, typeAlias) => { const typeText = typeAlias.getText(); return typeText.includes('Pick<') || typeText.includes('Omit<'); }); // Pick and Omit types should be resolved expect(result).toContain('{ name: string; email: string }'); expect(result).toContain('{ id: number; name: string; age?: number }'); // Other utility types should remain unresolved expect(result).toContain('Partial<User>'); expect(result).toContain('Required<User>'); expect(result).toContain('Readonly<User>'); }); test('should filter types by name prefix', () => { // Only resolve types starting with "User" but not "UserProfile" const result = resolver.resolveTypes((name) => name.startsWith('User') && name !== 'UserProfile'); // UserProfile should remain unresolved expect(result).toContain("Pick<User, 'name' | 'email'>"); // Other User types should be resolved expect(result).toContain('{ id: number; name: string; email: string; age?: number }'); }); test('should handle mixed resolution correctly', () => { // Resolve only Partial and Required types const result = resolver.resolveTypes((name, typeAlias) => { const typeText = typeAlias.getText(); return typeText.includes('Partial<') || typeText.includes('Required<'); }); expect(result).toContain('type PartialUser = { id?: number; name?: string; email?: string; age?: number }'); expect(result).toContain('type RequiredUser = { id: number; name: string; email: string; age: number }'); expect(result).toContain('type User = { id: number; name: string; email: string; age?: number; }'); expect(result).toContain("type UserProfile = Pick<User, 'name' | 'email'>"); expect(result).toContain("type UserWithoutEmail = Omit<User, 'email'>"); expect(result).toContain('type ReadonlyUser = Readonly<User>'); expect(result).toContain('type SimpleType = string'); expect(result).toContain('type ComplexType = Record<string, User>'); }); }); }); //# sourceMappingURL=type-resolver.test.js.map