import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, NamedSchema, ReferenceSchema, ReverseReferenceSchema, StringLiteralSchema, UnionSchema, } from "./types"; export class ParseError extends Error { constructor( message: string, public position?: number, public schema?: string, public value?: string, ) { let fullMessage = message; if (position !== undefined) { fullMessage += ` at position ${position}`; } if (schema !== undefined) { fullMessage += `. Schema: ${schema}`; } if (value !== undefined) { fullMessage += `. Value: ${value}`; } super(fullMessage); 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 reverse reference syntax: ~tablename(foreignKey) if (this.consumeStr("~")) { return this.parseReverseReferenceSchema(); } // 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, }; } private parseReverseReferenceSchema(): 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(); // Parse (foreignKey) if (!this.consumeStr("(")) { throw new ParseError( "Expected ( after reverse reference table name", this.pos, ); } this.skipWhitespace(); let foreignKey = ""; while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) { foreignKey += this.consume(); } if (foreignKey.length === 0) { throw new ParseError("Expected foreign key name inside ()", this.pos); } this.skipWhitespace(); if (!this.consumeStr(")")) { throw new ParseError("Expected ) after foreign key name", this.pos); } this.skipWhitespace(); // Check for optional suffix const isOptional = this.consumeStr("?"); return { type: "reverseReference", tableName, foreignKey, 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; }