import { describe, it, expect } from 'vitest'; import { defineSchema, parseSchema, parseValue, createValidator, ParseError } from './index'; import type { Schema, StringLiteralSchema, UnionSchema } from './types'; describe('defineSchema', () => { it('should return a ParsedSchema with schema, validator, and parse', () => { const parsed = defineSchema('string'); expect(parsed).toHaveProperty('schema'); expect(parsed).toHaveProperty('validator'); expect(parsed).toHaveProperty('parse'); }); }); describe('Primitive types', () => { describe('string', () => { it('should parse and validate string', () => { const schema = defineSchema('string'); expect(schema.parse('hello')).toBe('hello'); expect(schema.validator('hello')).toBe(true); expect(schema.validator(42)).toBe(false); }); it('should handle identifiers with hyphens', () => { const schema = defineSchema('word-smith'); expect(schema.parse('word-smith')).toBe('word-smith'); expect(schema.validator('word-smith')).toBe(true); }); }); describe('number', () => { it('should parse and validate number', () => { const schema = defineSchema('number'); expect(schema.parse('42')).toBe(42); expect(schema.parse('3.14')).toBe(3.14); expect(schema.validator(42)).toBe(true); expect(schema.validator('42')).toBe(false); }); }); describe('int', () => { it('should parse and validate int', () => { const schema = defineSchema('int'); expect(schema.parse('42')).toBe(42); expect(schema.validator(42)).toBe(true); expect(schema.validator(3.14)).toBe(false); }); it('should reject floats', () => { const schema = defineSchema('int'); expect(() => schema.parse('3.14')).toThrow(ParseError); }); }); describe('float', () => { it('should parse and validate float', () => { const schema = defineSchema('float'); expect(schema.parse('3.14')).toBe(3.14); expect(schema.parse('42')).toBe(42); expect(schema.validator(3.14)).toBe(true); expect(schema.validator(42)).toBe(true); }); }); describe('boolean', () => { it('should parse and validate boolean', () => { const schema = defineSchema('boolean'); expect(schema.parse('true')).toBe(true); expect(schema.parse('false')).toBe(false); expect(schema.validator(true)).toBe(true); expect(schema.validator('true')).toBe(false); }); it('should reject non-boolean values', () => { const schema = defineSchema('boolean'); expect(() => schema.parse('yes')).toThrow(ParseError); }); }); }); describe('String literals', () => { it('should parse double-quoted string literal', () => { const schema = defineSchema('"hello"'); expect(schema.parse('"hello"')).toBe('hello'); expect(schema.validator('hello')).toBe(true); expect(schema.validator('world')).toBe(false); }); it('should parse single-quoted string literal', () => { const schema = defineSchema("'world'"); expect(schema.parse("'world'")).toBe('world'); expect(schema.validator('world')).toBe(true); expect(schema.validator('hello')).toBe(false); }); it('should reject mismatched string literal values', () => { const schema = defineSchema('"on"'); expect(() => schema.parse('"off"')).toThrow(ParseError); }); it('should handle string literals with semicolons', () => { const schema = defineSchema('"hello;world"'); expect(schema.parse('"hello;world"')).toBe('hello;world'); expect(schema.validator('hello;world')).toBe(true); }); it('should handle escaped quotes in string literals', () => { const schema = defineSchema('"hello\\"world"'); expect(schema.parse('"hello\\"world"')).toBe('hello"world'); expect(schema.validator('hello"world')).toBe(true); }); }); describe('Union types', () => { it('should parse union of string literals', () => { const schema = defineSchema('"on" | "off"'); expect(schema.parse('"on"')).toBe('on'); expect(schema.parse('"off"')).toBe('off'); expect(schema.validator('on')).toBe(true); expect(schema.validator('off')).toBe(true); expect(schema.validator('maybe')).toBe(false); }); it('should parse union with three members', () => { const schema = defineSchema('"pending" | "approved" | "rejected"'); expect(schema.parse('"approved"')).toBe('approved'); expect(schema.validator('pending')).toBe(true); expect(schema.validator('approved')).toBe(true); expect(schema.validator('rejected')).toBe(true); expect(schema.validator('unknown')).toBe(false); }); it('should support parentheses for grouping', () => { const schema = defineSchema('( "active" | "inactive" )'); expect(schema.parse('"active"')).toBe('active'); expect(schema.validator('active')).toBe(true); }); it('should reject invalid union values', () => { const schema = defineSchema('"a" | "b"'); expect(() => schema.parse('"c"')).toThrow(ParseError); }); it('should support mixed type unions', () => { const schema = defineSchema('string | number'); // string is tried first, so '42' is parsed as string "42" expect(schema.parse('hello')).toBe('hello'); expect(schema.parse('42')).toBe('42'); expect(schema.validator('hello')).toBe(true); expect(schema.validator(42)).toBe(true); expect(schema.validator(true)).toBe(false); }); it('should support string type and string literal union', () => { const schema = defineSchema('string | "special"'); // string matches first, so '"special"' is parsed as string '"special"' expect(schema.parse('normal')).toBe('normal'); expect(schema.parse('special')).toBe('special'); expect(schema.validator('normal')).toBe(true); expect(schema.validator('special')).toBe(true); }); }); describe('Tuples', () => { it('should parse and validate tuple', () => { const schema = defineSchema('[string; number]'); const value = schema.parse('[hello; 42]'); expect(value).toEqual(['hello', 42]); expect(schema.validator(['hello', 42])).toBe(true); expect(schema.validator(['hello', '42'])).toBe(false); }); it('should parse tuple without brackets', () => { const schema = defineSchema('[string; number]'); const value = schema.parse('hello; 42'); expect(value).toEqual(['hello', 42]); }); it('should parse named tuple', () => { const schema = defineSchema('[x: number; y: number]'); const value = schema.parse('[x: 10; y: 20]'); expect(value).toEqual([10, 20]); }); it('should parse tuple with union field', () => { const schema = defineSchema('[name: string; status: "active" | "inactive"]'); const value = schema.parse('[myName; "active"]'); expect(value).toEqual(['myName', 'active']); expect(schema.validator(['myName', 'active'])).toBe(true); expect(schema.validator(['myName', 'unknown'])).toBe(false); }); it('should parse nested tuple', () => { const schema = defineSchema('[point: [x: number; y: number]]'); const value = schema.parse('[point: [x: 5; y: 10]]'); expect(value).toEqual([[5, 10]]); }); }); describe('Arrays', () => { it('should parse and validate array', () => { const schema = defineSchema('string[]'); const value = schema.parse('[hello; world; test]'); expect(value).toEqual(['hello', 'world', 'test']); expect(schema.validator(['hello', 'world'])).toBe(true); expect(schema.validator(['hello', 42])).toBe(false); }); it('should parse array without brackets', () => { const schema = defineSchema('string[]'); const value = schema.parse('hello; world; test'); expect(value).toEqual(['hello', 'world', 'test']); }); it('should parse array of numbers', () => { const schema = defineSchema('number[]'); const value = schema.parse('[1; 2; 3; 4]'); expect(value).toEqual([1, 2, 3, 4]); }); it('should parse array of ints', () => { const schema = defineSchema('int[]'); expect(schema.parse('[1; 2; 3; 4]')).toEqual([1, 2, 3, 4]); expect(() => schema.parse('[1; 2.5; 3]')).toThrow(ParseError); }); it('should parse array of floats', () => { const schema = defineSchema('float[]'); expect(schema.parse('[1.5; 2.5; 3.5]')).toEqual([1.5, 2.5, 3.5]); expect(schema.parse('[1; 2; 3]')).toEqual([1, 2, 3]); }); it('should parse array of tuples', () => { const schema = defineSchema('[string; number][]'); const value = schema.parse('[[a; 1]; [b; 2]; [c; 3]]'); expect(value).toEqual([['a', 1], ['b', 2], ['c', 3]]); }); it('should parse array of tuples without outer brackets', () => { const schema = defineSchema('[string; number][]'); const value = schema.parse('[a; 1]; [b; 2]; [c; 3]'); expect(value).toEqual([['a', 1], ['b', 2], ['c', 3]]); }); it('should parse array of unions', () => { const schema = defineSchema('("pending" | "approved" | "rejected")[]'); const value = schema.parse('["pending"; "approved"; "rejected"]'); expect(value).toEqual(['pending', 'approved', 'rejected']); expect(schema.validator(['pending', 'approved'])).toBe(true); expect(schema.validator(['pending', 'unknown'])).toBe(false); }); }); describe('Escaping', () => { it('should handle escaped semicolon', () => { const schema = defineSchema('string'); expect(schema.parse('hello\\;world')).toBe('hello;world'); }); it('should handle escaped bracket', () => { const schema = defineSchema('string'); expect(schema.parse('hello\\[world')).toBe('hello[world'); }); it('should handle escaped backslash', () => { const schema = defineSchema('string'); expect(schema.parse('hello\\\\world')).toBe('hello\\world'); }); it('should handle escaped semicolon in tuple', () => { const schema = defineSchema('[string; string]'); const value = schema.parse('hello\\;world; test'); expect(value).toEqual(['hello;world', 'test']); }); }); describe('parseSchema', () => { it('should parse string schema', () => { const schema = parseSchema('string'); expect(schema).toEqual({ type: 'string' }); }); it('should parse number schema', () => { const schema = parseSchema('number'); expect(schema).toEqual({ type: 'number' }); }); it('should parse string literal schema', () => { const schema = parseSchema('"on"'); expect(schema).toEqual({ type: 'stringLiteral', value: 'on' }); }); it('should parse union schema', () => { const schema = parseSchema('"on" | "off"'); expect(schema.type).toBe('union'); if (schema.type === 'union') { expect(schema.members).toHaveLength(2); expect(schema.members[0]).toEqual({ type: 'stringLiteral', value: 'on' }); expect(schema.members[1]).toEqual({ type: 'stringLiteral', value: 'off' }); } }); }); describe('Reference schemas (parseSchema)', () => { it('should parse single reference schema @tablename', () => { const schema = parseSchema('@users'); expect(schema).toEqual({ type: 'reference', tableName: 'users', isArray: false }); }); it('should parse array reference schema @tablename[]', () => { const schema = parseSchema('@users[]'); expect(schema).toEqual({ type: 'reference', tableName: 'users', isArray: true }); }); it('should parse reference with hyphens in table name', () => { const schema = parseSchema('@my-table'); expect(schema).toEqual({ type: 'reference', tableName: 'my-table', isArray: false }); }); it('should parse reference with underscores in table name', () => { const schema = parseSchema('@my_table'); expect(schema).toEqual({ type: 'reference', tableName: 'my_table', isArray: false }); }); it('should throw ParseError for @ without table name', () => { expect(() => parseSchema('@')).toThrow(ParseError); }); it('should throw ParseError for @ followed by non-identifier', () => { expect(() => parseSchema('@ ')).toThrow(ParseError); }); it('should parse reference inside tuple [string; @users]', () => { const schema = parseSchema('[string; @users]'); expect(schema.type).toBe('tuple'); if (schema.type === 'tuple') { expect(schema.elements).toHaveLength(2); expect(schema.elements[0].schema).toEqual({ type: 'string' }); expect(schema.elements[1].schema).toEqual({ type: 'reference', tableName: 'users', isArray: false }); } }); it('should parse reference array inside tuple [string; @users[]]', () => { const schema = parseSchema('[string; @users[]]'); expect(schema.type).toBe('tuple'); if (schema.type === 'tuple') { expect(schema.elements).toHaveLength(2); expect(schema.elements[0].schema).toEqual({ type: 'string' }); expect(schema.elements[1].schema).toEqual({ type: 'reference', tableName: 'users', isArray: true }); } }); it('should parse named reference in tuple [name: string; author: @users]', () => { const schema = parseSchema('[name: string; author: @users]'); expect(schema.type).toBe('tuple'); if (schema.type === 'tuple') { expect(schema.elements[1]).toEqual({ name: 'author', schema: { type: 'reference', tableName: 'users', isArray: false }, }); } }); it('should parse array of tuples containing reference [@users; number][]', () => { const schema = parseSchema('[@users; number][]'); expect(schema.type).toBe('array'); if (schema.type === 'array') { expect(schema.element.type).toBe('tuple'); if (schema.element.type === 'tuple') { expect(schema.element.elements[0].schema).toEqual({ type: 'reference', tableName: 'users', isArray: false }); } } }); it('should parse reference in union @users | string', () => { const schema = parseSchema('@users | string'); expect(schema.type).toBe('union'); if (schema.type === 'union') { expect(schema.members).toHaveLength(2); expect(schema.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: false }); expect(schema.members[1]).toEqual({ type: 'string' }); } }); it('should parse array reference in union @users[] | string', () => { const schema = parseSchema('@users[] | string'); expect(schema.type).toBe('union'); if (schema.type === 'union') { expect(schema.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: true }); } }); it('should parse reference inside parenthesized union (@users | @parts)', () => { const schema = parseSchema('(@users | @parts)'); expect(schema.type).toBe('union'); if (schema.type === 'union') { expect(schema.members).toHaveLength(2); expect(schema.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: false }); expect(schema.members[1]).toEqual({ type: 'reference', tableName: 'parts', isArray: false }); } }); it('should parse array of reference unions (@users | @parts)[]', () => { const schema = parseSchema('(@users | @parts)[]'); expect(schema.type).toBe('array'); if (schema.type === 'array') { expect(schema.element.type).toBe('union'); if (schema.element.type === 'union') { expect(schema.element.members[0]).toEqual({ type: 'reference', tableName: 'users', isArray: false }); expect(schema.element.members[1]).toEqual({ type: 'reference', tableName: 'parts', isArray: false }); } } }); }); describe('Reference value parsing (parseValue)', () => { it('should parse single reference ID', () => { const schema: import('./types').ReferenceSchema = { type: 'reference', tableName: 'users', isArray: false }; const result = parseValue(schema, '42'); expect(result).toBe('42'); }); it('should parse array reference IDs with brackets', () => { const schema: import('./types').ReferenceSchema = { type: 'reference', tableName: 'users', isArray: true }; const result = parseValue(schema, '[1; 2; 3]'); expect(result).toEqual(['1', '2', '3']); }); it('should parse array reference IDs without brackets', () => { const schema: import('./types').ReferenceSchema = { type: 'reference', tableName: 'users', isArray: true }; const result = parseValue(schema, '1; 2; 3'); expect(result).toEqual(['1', '2', '3']); }); it('should parse empty array reference', () => { const schema: import('./types').ReferenceSchema = { type: 'reference', tableName: 'users', isArray: true }; const result = parseValue(schema, '[]'); expect(result).toEqual([]); }); it('should parse single reference ID with spaces', () => { const schema: import('./types').ReferenceSchema = { type: 'reference', tableName: 'users', isArray: false }; const result = parseValue(schema, ' 42 '); expect(result).toBe('42'); }); it('should parse array reference IDs with spaces', () => { const schema: import('./types').ReferenceSchema = { type: 'reference', tableName: 'users', isArray: true }; const result = parseValue(schema, '[ 1 ; 2 ; 3 ]'); expect(result).toEqual(['1', '2', '3']); }); it('should parse string IDs in reference', () => { const schema: import('./types').ReferenceSchema = { type: 'reference', tableName: 'users', isArray: false }; const result = parseValue(schema, 'abc-123'); expect(result).toBe('abc-123'); }); }); describe('Reference validation (createValidator)', () => { it('should validate single reference (string ID)', () => { const schema: import('./types').ReferenceSchema = { type: 'reference', tableName: 'users', isArray: false }; const validator = createValidator(schema); expect(validator('1')).toBe(true); expect(validator('abc')).toBe(true); expect(validator(42)).toBe(false); expect(validator(true)).toBe(false); }); it('should validate array reference (array of string IDs)', () => { const schema: import('./types').ReferenceSchema = { type: 'reference', tableName: 'users', isArray: true }; const validator = createValidator(schema); expect(validator(['1', '2'])).toBe(true); expect(validator([])).toBe(true); expect(validator(['1'])).toBe(true); expect(validator([1, 2])).toBe(false); expect(validator('1')).toBe(false); }); it('should allow string array for single reference (backward compat)', () => { const schema: import('./types').ReferenceSchema = { type: 'reference', tableName: 'users', isArray: false }; const validator = createValidator(schema); expect(validator(['1', '2'])).toBe(true); }); }); describe('Error handling', () => { it('should throw ParseError for invalid schema', () => { expect(() => parseSchema('')).toThrow(ParseError); }); it('should throw ParseError for unexpected input', () => { expect(() => parseSchema('string extra')).toThrow(ParseError); }); it('should throw ParseError for invalid value', () => { const schema = defineSchema('number'); expect(() => schema.parse('not-a-number')).toThrow(ParseError); }); it('should throw ParseError for mismatched enum value', () => { const schema = defineSchema('"on" | "off"'); expect(() => schema.parse('"invalid"')).toThrow(ParseError); }); });