import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, NamedSchema } from './types'; export class ParseError extends Error { constructor(message: string, public position?: number) { super(position !== undefined ? `${message} at position ${position}` : message); this.name = 'ParseError'; } } class Parser { private input: string; private pos: number = 0; constructor(input: string) { this.input = input; } private peek(): string { return this.input[this.pos] || ''; } private consume(): string { return this.input[this.pos++] || ''; } private skipWhitespace(): void { while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) { this.pos++; } } private match(str: string): boolean { return this.input.slice(this.pos, this.pos + str.length) === str; } private consumeStr(str: string): boolean { if (this.match(str)) { this.pos += str.length; return true; } return false; } getPosition(): number { return this.pos; } getInputLength(): number { return this.input.length; } parseSchema(): Schema { this.skipWhitespace(); if (this.consumeStr('string')) { if (this.consumeStr('[')) { this.skipWhitespace(); if (!this.consumeStr(']')) { throw new ParseError('Expected ]', this.pos); } return { type: 'array', element: { type: 'string' } }; } return { type: 'string' }; } if (this.consumeStr('number')) { if (this.consumeStr('[')) { this.skipWhitespace(); if (!this.consumeStr(']')) { throw new ParseError('Expected ]', this.pos); } return { type: 'array', element: { type: 'number' } }; } return { type: 'number' }; } if (this.consumeStr('boolean')) { if (this.consumeStr('[')) { this.skipWhitespace(); if (!this.consumeStr(']')) { throw new ParseError('Expected ]', this.pos); } return { type: 'array', element: { type: 'boolean' } }; } return { type: 'boolean' }; } if (this.consumeStr('[')) { const elements: NamedSchema[] = []; this.skipWhitespace(); if (this.peek() === ']') { this.consume(); throw new ParseError('Empty array/tuple not allowed', this.pos); } elements.push(this.parseNamedSchema()); this.skipWhitespace(); if (this.consumeStr(';')) { const remainingElements: NamedSchema[] = []; while (true) { this.skipWhitespace(); remainingElements.push(this.parseNamedSchema()); this.skipWhitespace(); if (!this.consumeStr(';')) { break; } } elements.push(...remainingElements); } this.skipWhitespace(); if (!this.consumeStr(']')) { throw new ParseError('Expected ]', this.pos); } if (this.consumeStr('[')) { this.skipWhitespace(); if (!this.consumeStr(']')) { throw new ParseError('Expected ]', this.pos); } if (elements.length === 1 && !elements[0].name) { return { type: 'array', element: elements[0].schema }; } return { type: 'array', element: { type: 'tuple', elements } }; } if (elements.length === 1 && !elements[0].name) { return { type: 'array', element: elements[0].schema }; } return { type: 'tuple', elements }; } let identifier = ''; while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) { identifier += this.consume(); } if (identifier.length > 0) { if (this.consumeStr('[')) { this.skipWhitespace(); if (!this.consumeStr(']')) { throw new ParseError('Expected ]', this.pos); } return { type: 'array', element: { type: 'string' } }; } return { type: 'string' }; } throw new ParseError(`Unexpected character: ${this.peek()}`, this.pos); } private parseNamedSchema(): NamedSchema { this.skipWhitespace(); const startpos = this.pos; let identifier = ''; while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) { identifier += this.consume(); } if (identifier.length === 0) { throw new ParseError('Expected schema or named schema', this.pos); } this.skipWhitespace(); if (this.consumeStr(':')) { this.skipWhitespace(); const name = identifier; const schema = this.parseSchema(); return { name, schema }; } else { this.pos = startpos; const schema = this.parseSchema(); return { schema }; } } } export function parseSchema(schemaString: string): Schema { const parser = new Parser(schemaString.trim()); const schema = parser.parseSchema(); if (parser.getPosition() < parser.getInputLength()) { throw new ParseError('Unexpected input after schema', parser.getPosition()); } return schema; }