UNPKG

hey-api-builders

Version:

A custom plugin for @hey-api/openapi-ts that generates mock data builders with a lightweight custom runtime, Zod integration, or static mock generation.

872 lines (722 loc) 25.1 kB
import { describe, it, expect } from 'vitest'; import type { IR } from '@hey-api/openapi-ts'; import { irToSchema, collectSchemas, normalizeSchema, sanitizeSchema } from './schema-transformer'; import type { NormalizedSchemaNode } from '../types'; describe('Schema Transformer', () => { describe('irToSchema', () => { it('converts simple string schema', () => { const ir: IR.SchemaObject = { type: 'string' }; const result = irToSchema(ir, {}); expect(result).toEqual({ type: 'string' }); }); it('converts object schema with properties', () => { const ir: IR.SchemaObject = { type: 'object', properties: { name: { type: 'string' }, }, }; const result = irToSchema(ir, {}); expect(result).toHaveProperty('type', 'object'); expect(result).toHaveProperty('properties'); }); it('handles required fields', () => { const ir: IR.SchemaObject = { type: 'object', properties: { name: { type: 'string' }, }, required: ['name'], }; const result = irToSchema(ir, {}); expect(result).toHaveProperty('required'); expect(result.required).toEqual(['name']); }); it('handles $ref references', () => { const all: Record<string, IR.SchemaObject> = { User: { type: 'object', properties: { name: { type: 'string' } } }, }; const ir: IR.SchemaObject = { $ref: '#/components/schemas/User' }; const result = irToSchema(ir, all); expect(result).toHaveProperty('type', 'object'); expect(result).toHaveProperty('properties'); }); it('handles nullable fields', () => { const ir = { type: 'string', nullable: true, } as IR.SchemaObject; const result = irToSchema(ir, {}); expect(result.type).toEqual(['string', 'null']); }); it('infers type from properties', () => { const ir: IR.SchemaObject = { properties: { name: { type: 'string' }, }, }; const result = irToSchema(ir, {}); expect(result).toHaveProperty('type', 'object'); }); it('infers type from items', () => { const ir: IR.SchemaObject = { items: [{ type: 'string' }], }; const result = irToSchema(ir, {}); expect(result).toHaveProperty('type', 'array'); }); it('handles array items', () => { const ir: IR.SchemaObject = { type: 'array', items: [{ type: 'string' }], }; const result = irToSchema(ir, {}); expect(result).toHaveProperty('items'); }); it('handles additionalProperties boolean', () => { const ir: IR.SchemaObject = { type: 'object', additionalProperties: false, }; const result = irToSchema(ir, {}); expect(result).toHaveProperty('additionalProperties', false); }); it('handles additionalProperties schema', () => { const ir: IR.SchemaObject = { type: 'object', additionalProperties: { type: 'string' }, }; const result = irToSchema(ir, {}); expect(result).toHaveProperty('additionalProperties'); expect(result.additionalProperties).toHaveProperty('type', 'string'); }); it('copies format property', () => { const ir: IR.SchemaObject = { type: 'string', format: 'uuid', }; const result = irToSchema(ir, {}); expect(result).toHaveProperty('format', 'uuid'); }); it('copies pattern property', () => { const ir: IR.SchemaObject = { type: 'string', pattern: '^[A-Z]+$', }; const result = irToSchema(ir, {}); expect(result).toHaveProperty('pattern', '^[A-Z]+$'); }); it('copies min/max properties for strings', () => { const ir: IR.SchemaObject = { type: 'string', minLength: 5, maxLength: 100, }; const result = irToSchema(ir, {}); expect(result).toHaveProperty('minLength', 5); expect(result).toHaveProperty('maxLength', 100); }); it('copies min/max properties for numbers', () => { const ir: IR.SchemaObject = { type: 'number', minimum: 0, maximum: 100, }; const result = irToSchema(ir, {}); expect(result).toHaveProperty('minimum', 0); expect(result).toHaveProperty('maximum', 100); }); it('converts example to examples array', () => { const ir = { type: 'string', example: 'test-value', } as IR.SchemaObject; const result = irToSchema(ir, {}); expect(result).toHaveProperty('examples'); expect(result.examples).toEqual(['test-value']); }); it('handles circular references without infinite loop', () => { const ir: IR.SchemaObject = { type: 'object' }; const seen = new Set<IR.SchemaObject>(); seen.add(ir); const result = irToSchema(ir, {}, seen); expect(result).toEqual({}); }); it('returns empty object for null input', () => { const result = irToSchema(null as unknown as IR.SchemaObject, {}); expect(result).toEqual({}); }); it('returns empty object for non-object input', () => { const result = irToSchema('string' as IR.SchemaObject, {}); expect(result).toEqual({}); }); it('handles enum with type enum and items', () => { const ir = { type: 'enum', items: [{ const: 'value1' }, { const: 'value2' }, { const: 'value3' }], } as IR.SchemaObject; const result = irToSchema(ir, {}); expect(result).toHaveProperty('anyOf'); expect(result.anyOf).toHaveLength(3); }); it('handles enum with items but no enum property', () => { const ir = { items: [{ const: 'red' }, { const: 'blue' }, { const: 'green' }], } as IR.SchemaObject; const result = irToSchema(ir, {}); expect(result).toHaveProperty('anyOf'); expect(result.anyOf).toHaveLength(3); }); it('handles nullable arrays with union types', () => { const ir = { type: ['string', 'null'], nullable: true, } as unknown as IR.SchemaObject; const result = irToSchema(ir, {}); expect(result.type).toEqual(['string', 'null']); }); it('handles allOf composition', () => { const ir = { allOf: [ { type: 'object', properties: { a: { type: 'string' } } }, { type: 'object', properties: { b: { type: 'number' } } }, ], } as IR.SchemaObject; const result = irToSchema(ir, {}); expect(result).toHaveProperty('allOf'); expect(result.allOf).toHaveLength(2); }); it('handles anyOf composition', () => { const ir = { anyOf: [{ type: 'string' }, { type: 'number' }], } as IR.SchemaObject; const result = irToSchema(ir, {}); expect(result).toHaveProperty('anyOf'); expect(result.anyOf).toHaveLength(2); }); it('handles oneOf composition', () => { const ir = { oneOf: [{ type: 'boolean' }, { type: 'string' }], } as IR.SchemaObject; const result = irToSchema(ir, {}); expect(result).toHaveProperty('oneOf'); expect(result.oneOf).toHaveLength(2); }); }); describe('normalizeSchema', () => { it('normalizes basic schema', () => { const schema: NormalizedSchemaNode = { type: 'string' }; const result = normalizeSchema(schema); expect(result).toHaveProperty('type', 'string'); }); it('normalizes nested properties', () => { const schema: NormalizedSchemaNode = { type: 'object', properties: { nested: { type: 'string' }, }, }; const result = normalizeSchema(schema); expect(result.properties).toBeDefined(); expect(result.properties?.nested).toHaveProperty('type', 'string'); }); it('normalizes enum type with items', () => { const schema: NormalizedSchemaNode = { type: 'enum', items: [{ const: 'value1' }, { const: 'value2' }], }; const result = normalizeSchema(schema); expect(result.enum).toEqual(['value1', 'value2']); expect(result.type).toBe('string'); }); it('normalizes enum type with number items', () => { const schema: NormalizedSchemaNode = { type: 'enum', items: [{ const: 1 }, { const: 2 }, { const: 3 }], }; const result = normalizeSchema(schema); expect(result.enum).toEqual([1, 2, 3]); expect(result.type).toBe('integer'); }); it('normalizes enum type with mixed types to string', () => { const schema: NormalizedSchemaNode = { type: 'enum', items: [{ const: 'text' }, { const: 123 }], }; const result = normalizeSchema(schema); expect(result.enum).toEqual(['text', 123]); expect(result.type).toBe('string'); }); it('normalizes enum type with no valid items', () => { const schema: NormalizedSchemaNode = { type: 'enum', items: [], }; const result = normalizeSchema(schema); expect(result.type).toBe('string'); }); it('normalizes array with enum items', () => { const schema: NormalizedSchemaNode = { type: 'object', properties: { values: { type: 'array', items: [ { type: 'enum', items: [{ const: 'a' }] }, { type: 'enum', items: [{ const: 'b' }] }, ], }, }, }; const result = normalizeSchema(schema); expect(result.properties?.values).toBeDefined(); }); it('normalizes array with mixed type items to anyOf', () => { const schema: NormalizedSchemaNode = { type: 'array', items: [{ type: 'string' }, { type: 'number' }], }; const result = normalizeSchema(schema); expect(result.anyOf).toBeDefined(); expect(result).not.toHaveProperty('type'); }); it('normalizes nested items in array', () => { const schema: NormalizedSchemaNode = { type: 'object', properties: { data: { type: 'array', items: { type: 'string' }, }, }, }; const result = normalizeSchema(schema); expect(result.properties?.data).toBeDefined(); }); it('normalizes allOf schemas', () => { const schema: NormalizedSchemaNode = { type: 'object', allOf: [{ type: 'string' }], }; const result = normalizeSchema(schema); expect(result.allOf).toBeDefined(); }); it('normalizes anyOf schemas', () => { const schema: NormalizedSchemaNode = { type: 'object', anyOf: [{ type: 'string' }], }; const result = normalizeSchema(schema); expect(result.anyOf).toBeDefined(); }); it('normalizes oneOf schemas', () => { const schema: NormalizedSchemaNode = { type: 'object', oneOf: [{ type: 'string' }], }; const result = normalizeSchema(schema); expect(result.oneOf).toBeDefined(); }); }); describe('sanitizeSchema', () => { it('removes enum type and sets appropriate type', () => { const schema: NormalizedSchemaNode = { type: 'enum', enum: ['value1', 'value2'], }; const result = sanitizeSchema(schema); expect(result.type).toBe('string'); expect(result.enum).toEqual(['value1', 'value2']); }); it('removes IR.SchemaObject type', () => { const schema: NormalizedSchemaNode = { type: 'unknown' } as unknown as NormalizedSchemaNode; const result = sanitizeSchema(schema); expect(result).not.toHaveProperty('type'); }); it('removes logicalOperator property', () => { const schema: NormalizedSchemaNode = { type: 'string', logicalOperator: 'AND', }; const result = sanitizeSchema(schema); expect(result).not.toHaveProperty('logicalOperator'); }); it('sanitizes nested properties', () => { const schema: NormalizedSchemaNode = { type: 'object', properties: { field: { type: 'enum', enum: ['val'] }, }, }; const result = sanitizeSchema(schema); expect(result.properties?.field).not.toHaveProperty('type', 'enum'); }); it('sanitizes arrays', () => { const schema: NormalizedSchemaNode = { type: 'array', items: { type: 'unknown' }, } as unknown as NormalizedSchemaNode; const result = sanitizeSchema(schema); expect(result.items).toBeDefined(); }); it('sets type to string when enum exists without type', () => { const schema: NormalizedSchemaNode = { enum: ['a', 'b', 'c'], }; const result = sanitizeSchema(schema); expect(result.type).toBe('string'); }); it('sanitizes additionalProperties', () => { const schema: NormalizedSchemaNode = { type: 'object', additionalProperties: { type: 'object', }, }; const result = sanitizeSchema(schema); expect(result.additionalProperties).toBeDefined(); }); it('sanitizes array items when items is array', () => { const schema: NormalizedSchemaNode = { type: 'array', items: [{ type: 'enum', enum: ['val1'] }, { type: 'string' }], }; const result = sanitizeSchema(schema); expect(result.items).toBeDefined(); if (Array.isArray(result.items)) { expect(result.items[0]).not.toHaveProperty('type', 'enum'); } }); it('sanitizes allOf schemas', () => { const schema: NormalizedSchemaNode = { type: 'object', allOf: [{ type: 'enum', enum: ['val'] }], }; const result = sanitizeSchema(schema); expect(result.allOf).toBeDefined(); }); it('sanitizes anyOf schemas', () => { const schema: NormalizedSchemaNode = { type: 'object', anyOf: [{ type: 'string' }], }; const result = sanitizeSchema(schema); expect(result.anyOf).toBeDefined(); }); it('sanitizes oneOf schemas', () => { const schema: NormalizedSchemaNode = { type: 'object', oneOf: [{ type: 'enum', enum: ['x'] }], }; const result = sanitizeSchema(schema); expect(result.oneOf).toBeDefined(); }); }); describe('collectSchemas', () => { it('collects and processes multiple schemas', () => { const schemas: Record<string, IR.SchemaObject> = { UserSchema: { type: 'object', properties: { name: { type: 'string' } }, }, ProductSchema: { type: 'object', properties: { price: { type: 'number' } }, }, }; const result = collectSchemas(schemas); expect(result).toHaveLength(2); expect(result[0].typeName).toBe('User'); expect(result[1].typeName).toBe('Product'); }); it('removes Schema suffix from type names', () => { const schemas: Record<string, IR.SchemaObject> = { UserSchema: { type: 'object' }, }; const result = collectSchemas(schemas); expect(result[0].typeName).toBe('User'); }); it('normalizes type names', () => { const schemas: Record<string, IR.SchemaObject> = { UITheme: { type: 'object' }, APIKey: { type: 'object' }, turnaround_data_source: { type: 'string' }, }; const result = collectSchemas(schemas); expect(result[0].typeName).toBe('UiTheme'); expect(result[1].typeName).toBe('ApiKey'); expect(result[2].typeName).toBe('TurnaroundDataSource'); }); it('identifies object types correctly', () => { const schemas: Record<string, IR.SchemaObject> = { ObjectType: { type: 'object' }, StringType: { type: 'string' }, }; const result = collectSchemas(schemas); expect(result[0].isObject).toBe(true); expect(result[1].isObject).toBe(false); }); it('generates safe constant names', () => { const schemas: Record<string, IR.SchemaObject> = { 'My-Schema': { type: 'object' }, }; const result = collectSchemas(schemas); expect(result[0].constName).toMatch(/Schema$/); expect(result[0].constName).not.toContain('-'); }); it('handles schemas with $ref', () => { const schemas: Record<string, IR.SchemaObject> = { User: { type: 'object', properties: { address: { $ref: '#/components/schemas/Address' }, }, }, Address: { type: 'object' }, }; const result = collectSchemas(schemas); expect(result.length).toBe(2); }); it('handles schemas with additionalProperties', () => { const schemas: Record<string, IR.SchemaObject> = { DynamicObject: { type: 'object', additionalProperties: { type: 'string' }, }, }; const result = collectSchemas(schemas); expect(result.length).toBe(1); expect(result[0].schema.additionalProperties).toBeDefined(); }); it('handles schemas with required array', () => { const schemas: Record<string, IR.SchemaObject> = { User: { type: 'object', properties: { id: { type: 'number' }, name: { type: 'string' }, }, required: ['id'], }, }; const result = collectSchemas(schemas); expect(result[0].schema.required).toEqual(['id']); }); it('handles oneOf schemas', () => { const schemas: Record<string, IR.SchemaObject> = { Mixed: { oneOf: [{ type: 'string' }, { type: 'number' }], }, } as Record<string, IR.SchemaObject>; const result = collectSchemas(schemas); expect(result[0].schema.oneOf).toBeDefined(); }); it('handles anyOf schemas', () => { const schemas: Record<string, IR.SchemaObject> = { Flexible: { anyOf: [{ type: 'string' }, { type: 'boolean' }], }, } as Record<string, IR.SchemaObject>; const result = collectSchemas(schemas); expect(result[0].schema.anyOf).toBeDefined(); }); it('handles allOf schemas', () => { const schemas: Record<string, IR.SchemaObject> = { Combined: { allOf: [ { type: 'object', properties: { a: { type: 'string' } } }, { type: 'object', properties: { b: { type: 'number' } } }, ], }, } as Record<string, IR.SchemaObject>; const result = collectSchemas(schemas); expect(result[0].schema.allOf).toBeDefined(); }); it('handles array schemas with min/max items', () => { const schemas: Record<string, IR.SchemaObject> = { LimitedArray: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 10, }, } as unknown as Record<string, IR.SchemaObject>; const result = collectSchemas(schemas); expect(result[0].schema.minItems).toBe(1); expect(result[0].schema.maxItems).toBe(10); }); it('handles number schemas with constraints', () => { const schemas: Record<string, IR.SchemaObject> = { ConstrainedNumber: { type: 'number', minimum: 0, maximum: 100, multipleOf: 5, }, } as Record<string, IR.SchemaObject>; const result = collectSchemas(schemas); expect(result[0]).toBeDefined(); expect(result[0].typeName).toBe('ConstrainedNumber'); }); it('handles string schemas with format and pattern', () => { const schemas: Record<string, IR.SchemaObject> = { FormattedString: { type: 'string', format: 'email', pattern: '^[a-z]+@[a-z]+\\.[a-z]+$', minLength: 5, maxLength: 50, }, }; const result = collectSchemas(schemas); expect(result[0]).toBeDefined(); expect(result[0].typeName).toBe('FormattedString'); }); it('handles nested object schemas', () => { const schemas: Record<string, IR.SchemaObject> = { Nested: { type: 'object', properties: { inner: { type: 'object', properties: { value: { type: 'string' }, }, }, }, }, }; const result = collectSchemas(schemas); expect(result[0].schema.properties?.inner).toBeDefined(); }); it('handles schemas with description and title', () => { const schemas: Record<string, IR.SchemaObject> = { Documented: { type: 'object', title: 'Documented Schema', description: 'A well-documented schema', }, }; const result = collectSchemas(schemas); expect(result[0].schema.title).toBe('Documented Schema'); expect(result[0].schema.description).toBe('A well-documented schema'); }); it('handles schemas with nullable', () => { const schemas: Record<string, IR.SchemaObject> = { Nullable: { type: 'string', nullable: true, }, } as Record<string, IR.SchemaObject>; const result = collectSchemas(schemas); expect(result[0]).toBeDefined(); expect(result[0].typeName).toBe('Nullable'); }); it('handles schemas with deprecated flag', () => { const schemas: Record<string, IR.SchemaObject> = { Old: { type: 'string', deprecated: true, }, } as Record<string, IR.SchemaObject>; const result = collectSchemas(schemas); expect(result[0].schema.deprecated).toBe(true); }); it('handles schemas with readOnly flag', () => { const schemas: Record<string, IR.SchemaObject> = { ReadOnly: { type: 'object', properties: { id: { type: 'number', readOnly: true }, }, }, } as Record<string, IR.SchemaObject>; const result = collectSchemas(schemas); expect(result[0].schema.properties?.id.readOnly).toBe(true); }); it('handles schemas with writeOnly flag', () => { const schemas: Record<string, IR.SchemaObject> = { WriteOnly: { type: 'object', properties: { password: { type: 'string', writeOnly: true }, }, }, } as Record<string, IR.SchemaObject>; const result = collectSchemas(schemas); expect(result[0].schema.properties?.password.writeOnly).toBe(true); }); it('handles schemas with examples', () => { const schemas: Record<string, IR.SchemaObject> = { WithExamples: { type: 'string', examples: ['example1', 'example2'], }, } as Record<string, IR.SchemaObject>; const result = collectSchemas(schemas); expect(result[0]).toBeDefined(); expect(result[0].typeName).toBe('WithExamples'); }); it('handles schemas with default values', () => { const schemas: Record<string, IR.SchemaObject> = { WithDefault: { type: 'string', default: 'default-value', }, }; const result = collectSchemas(schemas); expect(result[0]).toBeDefined(); expect(result[0].typeName).toBe('WithDefault'); }); it('handles schemas with const values', () => { const schemas: Record<string, IR.SchemaObject> = { Constant: { type: 'string', const: 'fixed', }, }; const result = collectSchemas(schemas); expect(result[0]).toBeDefined(); expect(result[0].typeName).toBe('Constant'); }); it('handles complex nested structures', () => { const schemas: Record<string, IR.SchemaObject> = { Complex: { type: 'object', properties: { users: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, roles: { type: 'array', items: { type: 'string', enum: ['admin', 'user', 'guest'], }, }, }, }, }, }, }, } as unknown as Record<string, IR.SchemaObject>; const result = collectSchemas(schemas); expect(result[0].schema.properties?.users).toBeDefined(); }); it('handles schemas with exclusive minimum and maximum', () => { const schemas: Record<string, IR.SchemaObject> = { Exclusive: { type: 'number', exclusiveMinimum: 0, exclusiveMaximum: 100, }, }; const result = collectSchemas(schemas); expect(result[0].schema.exclusiveMinimum).toBe(0); expect(result[0].schema.exclusiveMaximum).toBe(100); }); }); });