import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, NamedSchema, ReferenceSchema, StringLiteralSchema, UnionSchema } 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(); // Parse the first schema element let schema = this.parseSchemaInternal(); this.skipWhitespace(); // Check for array suffix: type[] if (this.consumeStr('[')) { this.skipWhitespace(); if (!this.consumeStr(']')) { throw new ParseError('Expected ]', this.pos); } schema = { type: 'array', element: schema }; this.skipWhitespace(); } // Check for union type (| symbol) if (this.consumeStr('|')) { const members: Schema[] = [schema]; while (true) { this.skipWhitespace(); const member = this.parseSchemaInternal(); members.push(member); this.skipWhitespace(); if (!this.consumeStr('|')) { break; } } return { type: 'union', members }; } return schema; } private parseSchemaInternal(): Schema { this.skipWhitespace(); // Check for parentheses (grouping) if (this.consumeStr('(')) { this.skipWhitespace(); const schema = this.parseSchema(); // Recursive call for nested unions this.skipWhitespace(); if (!this.consumeStr(')')) { throw new ParseError('Expected )', this.pos); } // Check if there's an array suffix: (union)[] this.skipWhitespace(); if (this.consumeStr('[')) { this.skipWhitespace(); if (!this.consumeStr(']')) { throw new ParseError('Expected ]', this.pos); } return { type: 'array', element: schema }; } return schema; } // Check for string literal syntax: "value" or 'value' if (this.peek() === '"' || this.peek() === "'") { return this.parseStringLiteralSchema(); } // 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 }; } throw new ParseError(`Unknown type: ${this.peek() || 'end of input'}`, this.pos); } private parseStringLiteralSchema(): Schema { const value = this.parseStringLiteral(); return { type: 'stringLiteral', value }; } private parseStringLiteral(): string { const quote = this.peek(); if (quote !== '"' && quote !== "'") { throw new ParseError('Expected string literal with quotes', this.pos); } this.consume(); // Consume opening quote let value = ''; while (this.pos < this.input.length) { const char = this.peek(); if (char === '\\') { this.consume(); const nextChar = this.consume(); if (nextChar === '"' || nextChar === "'" || nextChar === '\\' || nextChar === '|' || nextChar === ';' || nextChar === '(' || nextChar === ')') { value += nextChar; } else { value += '\\' + nextChar; } } else if (char === quote) { this.consume(); // Consume closing quote return value; } else { value += this.consume(); } } throw new ParseError('Unterminated string literal', 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) { const schema = this.parseSchema(); return { schema }; } 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('[]')) { this.skipWhitespace(); const isOptional = this.consumeStr('?'); return { type: 'reference', tableName, isArray: true, isOptional, }; } // Check for optional suffix const isOptional = this.consumeStr('?'); // Single reference (non-array) return { type: 'reference', tableName, isArray: false, isOptional, }; } } 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; }