import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, NamedSchema, ReferenceSchema } from './types'; export class ParseError extends Error { constructor(message: string, public position?: number) { super(position !== undefined ? `${message} at position ${position}` : message); this.name = 'ParseError'; } } export interface ReferenceInfo { /** Referenced table name (e.g., 'parts' from '@parts[]') */ tableName: string; /** Whether it's an array reference */ isArray: boolean; } 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(); // Check for reference syntax: @tablename[] if (this.consumeStr('@')) { return this.parseReferenceSchema(); } 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('int')) { if (this.consumeStr('[')) { this.skipWhitespace(); if (!this.consumeStr(']')) { throw new ParseError('Expected ]', this.pos); } return { type: 'array', element: { type: 'int' } }; } return { type: 'int' }; } if (this.consumeStr('float')) { if (this.consumeStr('[')) { this.skipWhitespace(); if (!this.consumeStr(']')) { throw new ParseError('Expected ]', this.pos); } return { type: 'array', element: { type: 'float' } }; } return { type: 'float' }; } 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 }; } } private parseReferenceSchema(): Schema { // Parse table name let tableName = ''; while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) { tableName += this.consume(); } if (tableName.length === 0) { throw new ParseError('Expected table name after @', this.pos); } this.skipWhitespace(); // Check for array syntax if (this.consumeStr('[]')) { return { type: 'reference', tableName, isArray: true, }; } // Single reference (non-array) return { type: 'reference', tableName, isArray: false, }; } } 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; }