inline-schema/src/parser.ts

259 lines
6.5 KiB
TypeScript
Raw Normal View History

2026-04-11 22:56:01 +08:00
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, NamedSchema, ReferenceSchema } 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-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 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-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' };
}
if (this.consumeStr('[')) {
const elements: NamedSchema[] = [];
2026-03-31 12:17:46 +08:00
this.skipWhitespace();
2026-03-31 12:17:46 +08:00
if (this.peek() === ']') {
this.consume();
throw new ParseError('Empty array/tuple not allowed', this.pos);
}
elements.push(this.parseNamedSchema());
2026-03-31 12:17:46 +08:00
this.skipWhitespace();
2026-03-31 12:17:46 +08:00
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();
2026-03-31 12:17:46 +08:00
if (!this.consumeStr(';')) {
break;
}
}
elements.push(...remainingElements);
}
2026-03-31 12:17:46 +08:00
this.skipWhitespace();
2026-03-31 12:17:46 +08:00
if (!this.consumeStr(']')) {
throw new ParseError('Expected ]', this.pos);
}
2026-03-31 12:17:46 +08:00
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 };
2026-03-31 12:17:46 +08:00
}
return { type: 'array', element: { type: 'tuple', elements } };
}
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 };
}
let identifier = '';
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
identifier += this.consume();
}
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' };
}
throw new ParseError(`Unexpected character: ${this.peek()}`, this.pos);
}
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) {
throw new ParseError('Expected schema or named schema', this.pos);
}
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 = '';
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;
}