inline-schema/src/parser.ts

450 lines
11 KiB
TypeScript
Raw Normal View History

import type {
Schema,
PrimitiveSchema,
TupleSchema,
ArraySchema,
NamedSchema,
ReferenceSchema,
ReverseReferenceSchema,
StringLiteralSchema,
UnionSchema,
} from "./types";
2026-03-31 12:17:46 +08:00
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";
2026-03-31 12:17:46 +08:00
}
}
2026-04-11 22:56:01 +08:00
export interface ReferenceInfo {
/** Referenced table name (e.g., 'parts' from '@parts[]') */
tableName: string;
/** Whether it's an array reference */
isArray: boolean;
}
2026-03-31 12:17:46 +08:00
class Parser {
private input: string;
private pos: number = 0;
constructor(input: string) {
this.input = input;
}
private peek(): string {
return this.input[this.pos] || "";
2026-03-31 12:17:46 +08:00
}
private consume(): string {
return this.input[this.pos++] || "";
2026-03-31 12:17:46 +08:00
}
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();
2026-04-13 10:05:49 +08:00
// Parse the first schema element
let schema = this.parseSchemaInternal();
this.skipWhitespace();
2026-04-13 10:16:56 +08:00
// Check for array suffix: type[]
if (this.consumeStr("[")) {
2026-04-13 10:16:56 +08:00
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
2026-04-13 10:16:56 +08:00
}
schema = { type: "array", element: schema };
2026-04-13 10:16:56 +08:00
this.skipWhitespace();
}
2026-04-13 10:05:49 +08:00
// Check for union type (| symbol)
if (this.consumeStr("|")) {
2026-04-13 10:05:49 +08:00
const members: Schema[] = [schema];
2026-04-13 10:05:49 +08:00
while (true) {
this.skipWhitespace();
const member = this.parseSchemaInternal();
members.push(member);
this.skipWhitespace();
if (!this.consumeStr("|")) {
2026-04-13 10:05:49 +08:00
break;
}
}
2026-04-13 10:05:49 +08:00
return {
type: "union",
members,
2026-04-13 10:05:49 +08:00
};
}
return schema;
}
private parseSchemaInternal(): Schema {
this.skipWhitespace();
// Check for parentheses (grouping)
if (this.consumeStr("(")) {
2026-04-13 10:05:49 +08:00
this.skipWhitespace();
const schema = this.parseSchema(); // Recursive call for nested unions
2026-04-13 10:05:49 +08:00
this.skipWhitespace();
if (!this.consumeStr(")")) {
throw new ParseError("Expected )", this.pos);
2026-04-13 10:05:49 +08:00
}
2026-04-13 10:05:49 +08:00
// Check if there's an array suffix: (union)[]
this.skipWhitespace();
if (this.consumeStr("[")) {
2026-04-13 10:05:49 +08:00
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
2026-04-13 10:05:49 +08:00
}
return { type: "array", element: schema };
2026-04-13 10:05:49 +08:00
}
2026-04-13 10:05:49 +08:00
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();
}
2026-04-11 22:56:01 +08:00
// Check for reference syntax: @tablename[]
if (this.consumeStr("@")) {
2026-04-11 22:56:01 +08:00
return this.parseReferenceSchema();
}
if (this.consumeStr("string")) {
if (this.consumeStr("[")) {
2026-03-31 12:17:46 +08:00
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
2026-03-31 12:17:46 +08:00
}
return { type: "array", element: { type: "string" } };
2026-03-31 12:17:46 +08:00
}
return { type: "string" };
2026-03-31 12:17:46 +08:00
}
if (this.consumeStr("number")) {
if (this.consumeStr("[")) {
2026-03-31 12:17:46 +08:00
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
2026-03-31 12:17:46 +08:00
}
return { type: "array", element: { type: "number" } };
2026-03-31 12:17:46 +08:00
}
return { type: "number" };
2026-03-31 12:17:46 +08:00
}
if (this.consumeStr("int")) {
if (this.consumeStr("[")) {
2026-04-04 17:05:25 +08:00
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
2026-04-04 17:05:25 +08:00
}
return { type: "array", element: { type: "int" } };
2026-04-04 17:05:25 +08:00
}
return { type: "int" };
2026-04-04 17:05:25 +08:00
}
if (this.consumeStr("float")) {
if (this.consumeStr("[")) {
2026-04-04 17:05:25 +08:00
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
2026-04-04 17:05:25 +08:00
}
return { type: "array", element: { type: "float" } };
2026-04-04 17:05:25 +08:00
}
return { type: "float" };
2026-04-04 17:05:25 +08:00
}
if (this.consumeStr("boolean")) {
if (this.consumeStr("[")) {
2026-03-31 12:17:46 +08:00
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
2026-03-31 12:17:46 +08:00
}
return { type: "array", element: { type: "boolean" } };
2026-03-31 12:17:46 +08:00
}
return { type: "boolean" };
2026-03-31 12:17:46 +08:00
}
2026-04-13 10:05:49 +08:00
if (this.consumeStr("[")) {
const elements: NamedSchema[] = [];
2026-03-31 12:17:46 +08:00
this.skipWhitespace();
if (this.peek() === "]") {
2026-03-31 12:17:46 +08:00
this.consume();
throw new ParseError("Empty array/tuple not allowed", this.pos);
2026-03-31 12:17:46 +08:00
}
elements.push(this.parseNamedSchema());
2026-03-31 12:17:46 +08:00
this.skipWhitespace();
if (this.consumeStr(";")) {
const remainingElements: NamedSchema[] = [];
2026-03-31 12:17:46 +08:00
while (true) {
this.skipWhitespace();
remainingElements.push(this.parseNamedSchema());
2026-03-31 12:17:46 +08:00
this.skipWhitespace();
if (!this.consumeStr(";")) {
2026-03-31 12:17:46 +08:00
break;
}
}
elements.push(...remainingElements);
}
2026-03-31 12:17:46 +08:00
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
2026-03-31 12:17:46 +08:00
}
if (this.consumeStr("[")) {
2026-03-31 12:17:46 +08:00
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
2026-03-31 12:17:46 +08:00
}
if (elements.length === 1 && !elements[0].name) {
return { type: "array", element: elements[0].schema };
2026-03-31 12:17:46 +08:00
}
return { type: "array", element: { type: "tuple", elements } };
2026-03-31 12:17:46 +08:00
}
if (elements.length === 1 && !elements[0].name) {
return { type: "array", element: elements[0].schema };
2026-03-31 12:17:46 +08:00
}
return { type: "tuple", elements };
2026-03-31 12:17:46 +08:00
}
2026-04-13 10:05:49 +08:00
throw new ParseError(
`Unknown type: ${this.peek() || "end of input"}`,
this.pos,
);
2026-03-31 12:17:46 +08:00
}
2026-04-13 10:05:49 +08:00
private parseStringLiteralSchema(): Schema {
const value = this.parseStringLiteral();
return {
type: "stringLiteral",
value,
2026-04-13 10:05:49 +08:00
};
}
private parseStringLiteral(): string {
const quote = this.peek();
if (quote !== '"' && quote !== "'") {
throw new ParseError("Expected string literal with quotes", this.pos);
2026-04-13 10:05:49 +08:00
}
this.consume(); // Consume opening quote
let value = "";
2026-04-13 10:05:49 +08:00
while (this.pos < this.input.length) {
const char = this.peek();
if (char === "\\") {
2026-04-13 10:05:49 +08:00
this.consume();
const nextChar = this.consume();
if (
nextChar === '"' ||
nextChar === "'" ||
nextChar === "\\" ||
nextChar === "|" ||
nextChar === ";" ||
nextChar === "(" ||
nextChar === ")"
) {
2026-04-13 10:05:49 +08:00
value += nextChar;
} else {
value += "\\" + nextChar;
2026-04-13 10:05:49 +08:00
}
} else if (char === quote) {
this.consume(); // Consume closing quote
return value;
} else {
value += this.consume();
}
}
throw new ParseError("Unterminated string literal", this.pos);
2026-04-13 10:05:49 +08:00
}
private parseNamedSchema(): NamedSchema {
this.skipWhitespace();
2026-04-11 22:56:01 +08:00
const startpos = this.pos;
let identifier = "";
2026-04-11 22:56:01 +08:00
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
identifier += this.consume();
}
2026-04-11 22:56:01 +08:00
if (identifier.length === 0) {
2026-04-15 13:24:51 +08:00
const schema = this.parseSchema();
return { schema };
}
2026-04-11 22:56:01 +08:00
this.skipWhitespace();
2026-04-11 22:56:01 +08:00
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 };
}
}
2026-04-11 22:56:01 +08:00
private parseReferenceSchema(): Schema {
// Parse table name
let tableName = "";
2026-04-11 22:56:01 +08:00
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);
2026-04-11 22:56:01 +08:00
}
this.skipWhitespace();
// Check for array syntax
if (this.consumeStr("[]")) {
2026-04-17 11:41:06 +08:00
this.skipWhitespace();
const isOptional = this.consumeStr("?");
2026-04-11 22:56:01 +08:00
return {
type: "reference",
2026-04-11 22:56:01 +08:00
tableName,
isArray: true,
2026-04-17 11:41:06 +08:00
isOptional,
2026-04-11 22:56:01 +08:00
};
}
2026-04-17 11:41:06 +08:00
// Check for optional suffix
const isOptional = this.consumeStr("?");
2026-04-17 11:41:06 +08:00
2026-04-11 22:56:01 +08:00
// Single reference (non-array)
return {
type: "reference",
2026-04-11 22:56:01 +08:00
tableName,
isArray: false,
2026-04-17 11:41:06 +08:00
isOptional,
2026-04-11 22:56:01 +08:00
};
}
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,
};
}
2026-03-31 12:17:46 +08:00
}
export function parseSchema(schemaString: string): Schema {
const parser = new Parser(schemaString.trim());
const schema = parser.parseSchema();
2026-03-31 12:17:46 +08:00
if (parser.getPosition() < parser.getInputLength()) {
throw new ParseError("Unexpected input after schema", parser.getPosition());
2026-03-31 12:17:46 +08:00
}
2026-03-31 12:17:46 +08:00
return schema;
}