2026-04-13 10:05:49 +08:00
|
|
|
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, NamedSchema, ReferenceSchema, StringLiteralSchema, UnionSchema } from './types';
|
2026-03-31 12:17:46 +08:00
|
|
|
|
|
|
|
|
export class ParseError extends Error {
|
|
|
|
|
constructor(message: string, public position?: number) {
|
|
|
|
|
super(position !== undefined ? `${message} at position ${position}` : message);
|
|
|
|
|
this.name = 'ParseError';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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] || '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
2026-03-31 16:36:32 +08:00
|
|
|
|
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('[')) {
|
|
|
|
|
this.skipWhitespace();
|
|
|
|
|
if (!this.consumeStr(']')) {
|
|
|
|
|
throw new ParseError('Expected ]', this.pos);
|
|
|
|
|
}
|
|
|
|
|
schema = { type: 'array', element: schema };
|
|
|
|
|
this.skipWhitespace();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 10:05:49 +08:00
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 22:56:01 +08:00
|
|
|
// Check for reference syntax: @tablename[]
|
|
|
|
|
if (this.consumeStr('@')) {
|
|
|
|
|
return this.parseReferenceSchema();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
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' };
|
|
|
|
|
}
|
2026-03-31 16:36:32 +08:00
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
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' };
|
|
|
|
|
}
|
2026-03-31 16:36:32 +08:00
|
|
|
|
2026-04-04 17:05:25 +08:00
|
|
|
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' };
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
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' };
|
|
|
|
|
}
|
2026-04-13 10:05:49 +08:00
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
if (this.consumeStr('[')) {
|
2026-03-31 16:36:32 +08:00
|
|
|
const elements: NamedSchema[] = [];
|
2026-03-31 12:17:46 +08:00
|
|
|
this.skipWhitespace();
|
2026-03-31 16:36:32 +08:00
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
if (this.peek() === ']') {
|
|
|
|
|
this.consume();
|
|
|
|
|
throw new ParseError('Empty array/tuple not allowed', this.pos);
|
|
|
|
|
}
|
2026-03-31 16:36:32 +08:00
|
|
|
|
|
|
|
|
elements.push(this.parseNamedSchema());
|
2026-03-31 12:17:46 +08:00
|
|
|
this.skipWhitespace();
|
2026-03-31 16:36:32 +08:00
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
if (this.consumeStr(';')) {
|
2026-03-31 16:36:32 +08:00
|
|
|
const remainingElements: NamedSchema[] = [];
|
2026-03-31 12:17:46 +08:00
|
|
|
while (true) {
|
|
|
|
|
this.skipWhitespace();
|
2026-03-31 16:36:32 +08:00
|
|
|
remainingElements.push(this.parseNamedSchema());
|
2026-03-31 12:17:46 +08:00
|
|
|
this.skipWhitespace();
|
2026-03-31 16:36:32 +08:00
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
if (!this.consumeStr(';')) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
elements.push(...remainingElements);
|
|
|
|
|
}
|
2026-03-31 16:36:32 +08:00
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
this.skipWhitespace();
|
2026-03-31 16:36:32 +08:00
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
if (!this.consumeStr(']')) {
|
|
|
|
|
throw new ParseError('Expected ]', this.pos);
|
|
|
|
|
}
|
2026-03-31 16:36:32 +08:00
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
if (this.consumeStr('[')) {
|
|
|
|
|
this.skipWhitespace();
|
|
|
|
|
if (!this.consumeStr(']')) {
|
|
|
|
|
throw new ParseError('Expected ]', this.pos);
|
|
|
|
|
}
|
2026-03-31 16:36:32 +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 16:36:32 +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-04-13 10:05:49 +08:00
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
let identifier = '';
|
|
|
|
|
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
|
|
|
|
|
identifier += this.consume();
|
|
|
|
|
}
|
2026-04-13 10:05:49 +08:00
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
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' };
|
|
|
|
|
}
|
2026-04-13 10:05:49 +08:00
|
|
|
|
2026-03-31 12:17:46 +08:00
|
|
|
throw new ParseError(`Unexpected character: ${this.peek()}`, this.pos);
|
|
|
|
|
}
|
2026-03-31 16:36:32 +08:00
|
|
|
|
2026-04-13 10:05:49 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 16:36:32 +08:00
|
|
|
private parseNamedSchema(): NamedSchema {
|
|
|
|
|
this.skipWhitespace();
|
2026-04-11 22:56:01 +08:00
|
|
|
|
2026-03-31 16:36:32 +08:00
|
|
|
const startpos = this.pos;
|
|
|
|
|
let identifier = '';
|
2026-04-11 22:56:01 +08:00
|
|
|
|
2026-03-31 16:36:32 +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
|
|
|
|
2026-03-31 16:36:32 +08:00
|
|
|
if (identifier.length === 0) {
|
2026-04-15 13:24:51 +08:00
|
|
|
const schema = this.parseSchema();
|
|
|
|
|
return { schema };
|
2026-03-31 16:36:32 +08:00
|
|
|
}
|
2026-04-11 22:56:01 +08:00
|
|
|
|
2026-03-31 16:36:32 +08:00
|
|
|
this.skipWhitespace();
|
2026-04-11 22:56:01 +08:00
|
|
|
|
2026-03-31 16:36:32 +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 = '';
|
|
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-03-31 12:17:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|