inline-schema/src/parser.ts

450 lines
11 KiB
TypeScript

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;
}