inline-schema/src/index.test.ts

322 lines
11 KiB
TypeScript
Raw Normal View History

2026-04-13 10:16:56 +08:00
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('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);
});
});