Add reverse reference schema (~tablename(foreignKey))
Support reverse references via ~tablename(foreignKey) syntax, complementing forward @tablename references. Includes parser, validator, and CSV loader integration with the new ReverseReferenceSchema type.
This commit is contained in:
+900
-504
File diff suppressed because it is too large
Load Diff
+605
-196
File diff suppressed because it is too large
Load Diff
+568
-321
File diff suppressed because it is too large
Load Diff
+32
-6
@@ -1,7 +1,17 @@
|
|||||||
import { parseSchema } from './parser';
|
import { parseSchema } from "./parser";
|
||||||
import { parseValue, createValidator, schemaToTypeString } from './validator';
|
import { parseValue, createValidator, schemaToTypeString } from "./validator";
|
||||||
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, ReferenceSchema, StringLiteralSchema, UnionSchema, ParsedSchema } from './types';
|
import type {
|
||||||
import { ParseError } from './parser';
|
Schema,
|
||||||
|
PrimitiveSchema,
|
||||||
|
TupleSchema,
|
||||||
|
ArraySchema,
|
||||||
|
ReferenceSchema,
|
||||||
|
ReverseReferenceSchema,
|
||||||
|
StringLiteralSchema,
|
||||||
|
UnionSchema,
|
||||||
|
ParsedSchema,
|
||||||
|
} from "./types";
|
||||||
|
import { ParseError } from "./parser";
|
||||||
|
|
||||||
export function defineSchema(schemaString: string): ParsedSchema {
|
export function defineSchema(schemaString: string): ParsedSchema {
|
||||||
const schema = parseSchema(schemaString);
|
const schema = parseSchema(schemaString);
|
||||||
@@ -14,5 +24,21 @@ export function defineSchema(schemaString: string): ParsedSchema {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { parseSchema, parseValue, createValidator, ParseError, schemaToTypeString };
|
export {
|
||||||
export type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, ReferenceSchema, StringLiteralSchema, UnionSchema, ParsedSchema };
|
parseSchema,
|
||||||
|
parseValue,
|
||||||
|
createValidator,
|
||||||
|
ParseError,
|
||||||
|
schemaToTypeString,
|
||||||
|
};
|
||||||
|
export type {
|
||||||
|
Schema,
|
||||||
|
PrimitiveSchema,
|
||||||
|
TupleSchema,
|
||||||
|
ArraySchema,
|
||||||
|
ReferenceSchema,
|
||||||
|
ReverseReferenceSchema,
|
||||||
|
StringLiteralSchema,
|
||||||
|
UnionSchema,
|
||||||
|
ParsedSchema,
|
||||||
|
};
|
||||||
|
|||||||
+179
-98
@@ -1,9 +1,24 @@
|
|||||||
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, NamedSchema, ReferenceSchema, StringLiteralSchema, UnionSchema } from './types';
|
import type {
|
||||||
|
Schema,
|
||||||
|
PrimitiveSchema,
|
||||||
|
TupleSchema,
|
||||||
|
ArraySchema,
|
||||||
|
NamedSchema,
|
||||||
|
ReferenceSchema,
|
||||||
|
ReverseReferenceSchema,
|
||||||
|
StringLiteralSchema,
|
||||||
|
UnionSchema,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export class ParseError extends Error {
|
export class ParseError extends Error {
|
||||||
constructor(message: string, public position?: number) {
|
constructor(
|
||||||
super(position !== undefined ? `${message} at position ${position}` : message);
|
message: string,
|
||||||
this.name = 'ParseError';
|
public position?: number,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
position !== undefined ? `${message} at position ${position}` : message,
|
||||||
|
);
|
||||||
|
this.name = "ParseError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,11 +38,11 @@ class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private peek(): string {
|
private peek(): string {
|
||||||
return this.input[this.pos] || '';
|
return this.input[this.pos] || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private consume(): string {
|
private consume(): string {
|
||||||
return this.input[this.pos++] || '';
|
return this.input[this.pos++] || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private skipWhitespace(): void {
|
private skipWhitespace(): void {
|
||||||
@@ -64,33 +79,33 @@ class Parser {
|
|||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
// Check for array suffix: type[]
|
// Check for array suffix: type[]
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr("[")) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
schema = { type: 'array', element: schema };
|
schema = { type: "array", element: schema };
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for union type (| symbol)
|
// Check for union type (| symbol)
|
||||||
if (this.consumeStr('|')) {
|
if (this.consumeStr("|")) {
|
||||||
const members: Schema[] = [schema];
|
const members: Schema[] = [schema];
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
const member = this.parseSchemaInternal();
|
const member = this.parseSchemaInternal();
|
||||||
members.push(member);
|
members.push(member);
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (!this.consumeStr('|')) {
|
if (!this.consumeStr("|")) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'union',
|
type: "union",
|
||||||
members
|
members,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,25 +116,25 @@ class Parser {
|
|||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
// Check for parentheses (grouping)
|
// Check for parentheses (grouping)
|
||||||
if (this.consumeStr('(')) {
|
if (this.consumeStr("(")) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
const schema = this.parseSchema(); // Recursive call for nested unions
|
const schema = this.parseSchema(); // Recursive call for nested unions
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (!this.consumeStr(')')) {
|
if (!this.consumeStr(")")) {
|
||||||
throw new ParseError('Expected )', this.pos);
|
throw new ParseError("Expected )", this.pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there's an array suffix: (union)[]
|
// Check if there's an array suffix: (union)[]
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr("[")) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
return { type: 'array', element: schema };
|
return { type: "array", element: schema };
|
||||||
}
|
}
|
||||||
|
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,86 +143,91 @@ class Parser {
|
|||||||
return this.parseStringLiteralSchema();
|
return this.parseStringLiteralSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for reverse reference syntax: ~tablename(foreignKey)
|
||||||
|
if (this.consumeStr("~")) {
|
||||||
|
return this.parseReverseReferenceSchema();
|
||||||
|
}
|
||||||
|
|
||||||
// Check for reference syntax: @tablename[]
|
// Check for reference syntax: @tablename[]
|
||||||
if (this.consumeStr('@')) {
|
if (this.consumeStr("@")) {
|
||||||
return this.parseReferenceSchema();
|
return this.parseReferenceSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.consumeStr('string')) {
|
if (this.consumeStr("string")) {
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr("[")) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
return { type: 'array', element: { type: 'string' } };
|
return { type: "array", element: { type: "string" } };
|
||||||
}
|
}
|
||||||
return { type: 'string' };
|
return { type: "string" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.consumeStr('number')) {
|
if (this.consumeStr("number")) {
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr("[")) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
return { type: 'array', element: { type: 'number' } };
|
return { type: "array", element: { type: "number" } };
|
||||||
}
|
}
|
||||||
return { type: 'number' };
|
return { type: "number" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.consumeStr('int')) {
|
if (this.consumeStr("int")) {
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr("[")) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
return { type: 'array', element: { type: 'int' } };
|
return { type: "array", element: { type: "int" } };
|
||||||
}
|
}
|
||||||
return { type: 'int' };
|
return { type: "int" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.consumeStr('float')) {
|
if (this.consumeStr("float")) {
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr("[")) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
return { type: 'array', element: { type: 'float' } };
|
return { type: "array", element: { type: "float" } };
|
||||||
}
|
}
|
||||||
return { type: 'float' };
|
return { type: "float" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.consumeStr('boolean')) {
|
if (this.consumeStr("boolean")) {
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr("[")) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
return { type: 'array', element: { type: 'boolean' } };
|
return { type: "array", element: { type: "boolean" } };
|
||||||
}
|
}
|
||||||
return { type: 'boolean' };
|
return { type: "boolean" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr("[")) {
|
||||||
const elements: NamedSchema[] = [];
|
const elements: NamedSchema[] = [];
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (this.peek() === ']') {
|
if (this.peek() === "]") {
|
||||||
this.consume();
|
this.consume();
|
||||||
throw new ParseError('Empty array/tuple not allowed', this.pos);
|
throw new ParseError("Empty array/tuple not allowed", this.pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.push(this.parseNamedSchema());
|
elements.push(this.parseNamedSchema());
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (this.consumeStr(';')) {
|
if (this.consumeStr(";")) {
|
||||||
const remainingElements: NamedSchema[] = [];
|
const remainingElements: NamedSchema[] = [];
|
||||||
while (true) {
|
while (true) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
remainingElements.push(this.parseNamedSchema());
|
remainingElements.push(this.parseNamedSchema());
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (!this.consumeStr(';')) {
|
if (!this.consumeStr(";")) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,57 +236,67 @@ class Parser {
|
|||||||
|
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr("[")) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
if (elements.length === 1 && !elements[0].name) {
|
if (elements.length === 1 && !elements[0].name) {
|
||||||
return { type: 'array', element: elements[0].schema };
|
return { type: "array", element: elements[0].schema };
|
||||||
}
|
}
|
||||||
return { type: 'array', element: { type: 'tuple', elements } };
|
return { type: "array", element: { type: "tuple", elements } };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elements.length === 1 && !elements[0].name) {
|
if (elements.length === 1 && !elements[0].name) {
|
||||||
return { type: 'array', element: elements[0].schema };
|
return { type: "array", element: elements[0].schema };
|
||||||
}
|
}
|
||||||
return { type: 'tuple', elements };
|
return { type: "tuple", elements };
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ParseError(`Unknown type: ${this.peek() || 'end of input'}`, this.pos);
|
throw new ParseError(
|
||||||
|
`Unknown type: ${this.peek() || "end of input"}`,
|
||||||
|
this.pos,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseStringLiteralSchema(): Schema {
|
private parseStringLiteralSchema(): Schema {
|
||||||
const value = this.parseStringLiteral();
|
const value = this.parseStringLiteral();
|
||||||
return {
|
return {
|
||||||
type: 'stringLiteral',
|
type: "stringLiteral",
|
||||||
value
|
value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseStringLiteral(): string {
|
private parseStringLiteral(): string {
|
||||||
const quote = this.peek();
|
const quote = this.peek();
|
||||||
if (quote !== '"' && quote !== "'") {
|
if (quote !== '"' && quote !== "'") {
|
||||||
throw new ParseError('Expected string literal with quotes', this.pos);
|
throw new ParseError("Expected string literal with quotes", this.pos);
|
||||||
}
|
}
|
||||||
this.consume(); // Consume opening quote
|
this.consume(); // Consume opening quote
|
||||||
|
|
||||||
let value = '';
|
let value = "";
|
||||||
while (this.pos < this.input.length) {
|
while (this.pos < this.input.length) {
|
||||||
const char = this.peek();
|
const char = this.peek();
|
||||||
|
|
||||||
if (char === '\\') {
|
if (char === "\\") {
|
||||||
this.consume();
|
this.consume();
|
||||||
const nextChar = this.consume();
|
const nextChar = this.consume();
|
||||||
if (nextChar === '"' || nextChar === "'" || nextChar === '\\' ||
|
if (
|
||||||
nextChar === '|' || nextChar === ';' || nextChar === '(' || nextChar === ')') {
|
nextChar === '"' ||
|
||||||
|
nextChar === "'" ||
|
||||||
|
nextChar === "\\" ||
|
||||||
|
nextChar === "|" ||
|
||||||
|
nextChar === ";" ||
|
||||||
|
nextChar === "(" ||
|
||||||
|
nextChar === ")"
|
||||||
|
) {
|
||||||
value += nextChar;
|
value += nextChar;
|
||||||
} else {
|
} else {
|
||||||
value += '\\' + nextChar;
|
value += "\\" + nextChar;
|
||||||
}
|
}
|
||||||
} else if (char === quote) {
|
} else if (char === quote) {
|
||||||
this.consume(); // Consume closing quote
|
this.consume(); // Consume closing quote
|
||||||
@@ -275,15 +305,15 @@ class Parser {
|
|||||||
value += this.consume();
|
value += this.consume();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ParseError('Unterminated string literal', this.pos);
|
throw new ParseError("Unterminated string literal", this.pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseNamedSchema(): NamedSchema {
|
private parseNamedSchema(): NamedSchema {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
const startpos = this.pos;
|
const startpos = this.pos;
|
||||||
let identifier = '';
|
let identifier = "";
|
||||||
|
|
||||||
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
|
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
|
||||||
identifier += this.consume();
|
identifier += this.consume();
|
||||||
@@ -296,7 +326,7 @@ class Parser {
|
|||||||
|
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (this.consumeStr(':')) {
|
if (this.consumeStr(":")) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
const name = identifier;
|
const name = identifier;
|
||||||
const schema = this.parseSchema();
|
const schema = this.parseSchema();
|
||||||
@@ -310,23 +340,23 @@ class Parser {
|
|||||||
|
|
||||||
private parseReferenceSchema(): Schema {
|
private parseReferenceSchema(): Schema {
|
||||||
// Parse table name
|
// Parse table name
|
||||||
let tableName = '';
|
let tableName = "";
|
||||||
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
|
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
|
||||||
tableName += this.consume();
|
tableName += this.consume();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tableName.length === 0) {
|
if (tableName.length === 0) {
|
||||||
throw new ParseError('Expected table name after @', this.pos);
|
throw new ParseError("Expected table name after @", this.pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
// Check for array syntax
|
// Check for array syntax
|
||||||
if (this.consumeStr('[]')) {
|
if (this.consumeStr("[]")) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
const isOptional = this.consumeStr('?');
|
const isOptional = this.consumeStr("?");
|
||||||
return {
|
return {
|
||||||
type: 'reference',
|
type: "reference",
|
||||||
tableName,
|
tableName,
|
||||||
isArray: true,
|
isArray: true,
|
||||||
isOptional,
|
isOptional,
|
||||||
@@ -334,25 +364,76 @@ class Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for optional suffix
|
// Check for optional suffix
|
||||||
const isOptional = this.consumeStr('?');
|
const isOptional = this.consumeStr("?");
|
||||||
|
|
||||||
// Single reference (non-array)
|
// Single reference (non-array)
|
||||||
return {
|
return {
|
||||||
type: 'reference',
|
type: "reference",
|
||||||
tableName,
|
tableName,
|
||||||
isArray: false,
|
isArray: false,
|
||||||
isOptional,
|
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 {
|
export function parseSchema(schemaString: string): Schema {
|
||||||
const parser = new Parser(schemaString.trim());
|
const parser = new Parser(schemaString.trim());
|
||||||
const schema = parser.parseSchema();
|
const schema = parser.parseSchema();
|
||||||
|
|
||||||
if (parser.getPosition() < parser.getInputLength()) {
|
if (parser.getPosition() < parser.getInputLength()) {
|
||||||
throw new ParseError('Unexpected input after schema', parser.getPosition());
|
throw new ParseError("Unexpected input after schema", parser.getPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-9
@@ -1,4 +1,4 @@
|
|||||||
export type SchemaType = 'string' | 'number' | 'int' | 'float' | 'boolean';
|
export type SchemaType = "string" | "number" | "int" | "float" | "boolean";
|
||||||
|
|
||||||
export interface PrimitiveSchema {
|
export interface PrimitiveSchema {
|
||||||
type: SchemaType;
|
type: SchemaType;
|
||||||
@@ -10,17 +10,17 @@ export interface NamedSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface TupleSchema {
|
export interface TupleSchema {
|
||||||
type: 'tuple';
|
type: "tuple";
|
||||||
elements: NamedSchema[];
|
elements: NamedSchema[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArraySchema {
|
export interface ArraySchema {
|
||||||
type: 'array';
|
type: "array";
|
||||||
element: Schema;
|
element: Schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReferenceSchema {
|
export interface ReferenceSchema {
|
||||||
type: 'reference';
|
type: "reference";
|
||||||
/** Referenced table name (e.g., 'parts' from '@parts[]') */
|
/** Referenced table name (e.g., 'parts' from '@parts[]') */
|
||||||
tableName: string;
|
tableName: string;
|
||||||
/** Whether it's an array reference */
|
/** Whether it's an array reference */
|
||||||
@@ -29,17 +29,34 @@ export interface ReferenceSchema {
|
|||||||
isOptional?: boolean;
|
isOptional?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReverseReferenceSchema {
|
||||||
|
type: "reverseReference";
|
||||||
|
/** Referenced table name (e.g., 'orders' from '~orders(user)') */
|
||||||
|
tableName: string;
|
||||||
|
/** The foreign key field name in the referenced table (e.g., 'user' from '~orders(user)') */
|
||||||
|
foreignKey: string;
|
||||||
|
/** Whether it's optional (null if no matches, instead of empty array) */
|
||||||
|
isOptional?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StringLiteralSchema {
|
export interface StringLiteralSchema {
|
||||||
type: 'stringLiteral';
|
type: "stringLiteral";
|
||||||
value: string; // The literal string value (without quotes)
|
value: string; // The literal string value (without quotes)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnionSchema {
|
export interface UnionSchema {
|
||||||
type: 'union';
|
type: "union";
|
||||||
members: Schema[]; // Union members
|
members: Schema[]; // Union members
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Schema = PrimitiveSchema | TupleSchema | ArraySchema | ReferenceSchema | StringLiteralSchema | UnionSchema;
|
export type Schema =
|
||||||
|
| PrimitiveSchema
|
||||||
|
| TupleSchema
|
||||||
|
| ArraySchema
|
||||||
|
| ReferenceSchema
|
||||||
|
| ReverseReferenceSchema
|
||||||
|
| StringLiteralSchema
|
||||||
|
| UnionSchema;
|
||||||
|
|
||||||
export interface ParsedSchema {
|
export interface ParsedSchema {
|
||||||
schema: Schema;
|
schema: Schema;
|
||||||
|
|||||||
+179
-115
@@ -1,5 +1,15 @@
|
|||||||
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, NamedSchema, ReferenceSchema, StringLiteralSchema, UnionSchema } from './types';
|
import type {
|
||||||
import { ParseError } from './parser';
|
Schema,
|
||||||
|
PrimitiveSchema,
|
||||||
|
TupleSchema,
|
||||||
|
ArraySchema,
|
||||||
|
NamedSchema,
|
||||||
|
ReferenceSchema,
|
||||||
|
ReverseReferenceSchema,
|
||||||
|
StringLiteralSchema,
|
||||||
|
UnionSchema,
|
||||||
|
} from "./types";
|
||||||
|
import { ParseError } from "./parser";
|
||||||
|
|
||||||
class ValueParser {
|
class ValueParser {
|
||||||
private input: string;
|
private input: string;
|
||||||
@@ -10,11 +20,11 @@ class ValueParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private peek(): string {
|
private peek(): string {
|
||||||
return this.input[this.pos] || '';
|
return this.input[this.pos] || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private consume(): string {
|
private consume(): string {
|
||||||
return this.input[this.pos++] || '';
|
return this.input[this.pos++] || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private skipWhitespace(): void {
|
private skipWhitespace(): void {
|
||||||
@@ -35,46 +45,58 @@ class ValueParser {
|
|||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
switch (schema.type) {
|
switch (schema.type) {
|
||||||
case 'string':
|
case "string":
|
||||||
return this.parseStringValue();
|
return this.parseStringValue();
|
||||||
case 'number':
|
case "number":
|
||||||
return this.parseNumberValue();
|
return this.parseNumberValue();
|
||||||
case 'int':
|
case "int":
|
||||||
return this.parseIntValue();
|
return this.parseIntValue();
|
||||||
case 'float':
|
case "float":
|
||||||
return this.parseFloatValue();
|
return this.parseFloatValue();
|
||||||
case 'boolean':
|
case "boolean":
|
||||||
return this.parseBooleanValue();
|
return this.parseBooleanValue();
|
||||||
case 'stringLiteral':
|
case "stringLiteral":
|
||||||
return this.parseStringLiteralValue(schema);
|
return this.parseStringLiteralValue(schema);
|
||||||
case 'union':
|
case "union":
|
||||||
return this.parseUnionValue(schema);
|
return this.parseUnionValue(schema);
|
||||||
case 'tuple':
|
case "tuple":
|
||||||
return this.parseTupleValue(schema, allowOmitBrackets);
|
return this.parseTupleValue(schema, allowOmitBrackets);
|
||||||
case 'array':
|
case "array":
|
||||||
return this.parseArrayValue(schema, allowOmitBrackets);
|
return this.parseArrayValue(schema, allowOmitBrackets);
|
||||||
case 'reference':
|
case "reference":
|
||||||
// Reference values are parsed as strings (IDs) initially, resolved later
|
// Reference values are parsed as strings (IDs) initially, resolved later
|
||||||
return this.parseReferenceValue(schema);
|
return this.parseReferenceValue(schema);
|
||||||
|
case "reverseReference":
|
||||||
|
// Reverse references are derived fields, not stored in CSV cells
|
||||||
|
// They resolve to null at parse time; actual resolution happens in the loader
|
||||||
|
return null;
|
||||||
default:
|
default:
|
||||||
throw new ParseError(`Unknown schema type: ${(schema as { type: string }).type}`, this.pos);
|
throw new ParseError(
|
||||||
|
`Unknown schema type: ${(schema as { type: string }).type}`,
|
||||||
|
this.pos,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseStringValue(): string {
|
private parseStringValue(): string {
|
||||||
let result = '';
|
let result = "";
|
||||||
while (this.pos < this.input.length) {
|
while (this.pos < this.input.length) {
|
||||||
const char = this.peek();
|
const char = this.peek();
|
||||||
|
|
||||||
if (char === '\\') {
|
if (char === "\\") {
|
||||||
this.consume();
|
this.consume();
|
||||||
const nextChar = this.consume();
|
const nextChar = this.consume();
|
||||||
if (nextChar === ';' || nextChar === '[' || nextChar === ']' || nextChar === '\\') {
|
if (
|
||||||
|
nextChar === ";" ||
|
||||||
|
nextChar === "[" ||
|
||||||
|
nextChar === "]" ||
|
||||||
|
nextChar === "\\"
|
||||||
|
) {
|
||||||
result += nextChar;
|
result += nextChar;
|
||||||
} else {
|
} else {
|
||||||
result += '\\' + nextChar;
|
result += "\\" + nextChar;
|
||||||
}
|
}
|
||||||
} else if (char === ';' || char === ']') {
|
} else if (char === ";" || char === "]") {
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
result += this.consume();
|
result += this.consume();
|
||||||
@@ -84,28 +106,28 @@ class ValueParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private parseNumberValue(): number {
|
private parseNumberValue(): number {
|
||||||
let numStr = '';
|
let numStr = "";
|
||||||
while (this.pos < this.input.length && /[\d.\-+eE]/.test(this.peek())) {
|
while (this.pos < this.input.length && /[\d.\-+eE]/.test(this.peek())) {
|
||||||
numStr += this.consume();
|
numStr += this.consume();
|
||||||
}
|
}
|
||||||
const num = parseFloat(numStr);
|
const num = parseFloat(numStr);
|
||||||
if (isNaN(num)) {
|
if (isNaN(num)) {
|
||||||
throw new ParseError('Invalid number', this.pos - numStr.length);
|
throw new ParseError("Invalid number", this.pos - numStr.length);
|
||||||
}
|
}
|
||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseIntValue(): number {
|
private parseIntValue(): number {
|
||||||
let numStr = '';
|
let numStr = "";
|
||||||
while (this.pos < this.input.length && /[\d.\-+eE]/.test(this.peek())) {
|
while (this.pos < this.input.length && /[\d.\-+eE]/.test(this.peek())) {
|
||||||
numStr += this.consume();
|
numStr += this.consume();
|
||||||
}
|
}
|
||||||
const num = parseFloat(numStr);
|
const num = parseFloat(numStr);
|
||||||
if (isNaN(num)) {
|
if (isNaN(num)) {
|
||||||
throw new ParseError('Invalid number', this.pos - numStr.length);
|
throw new ParseError("Invalid number", this.pos - numStr.length);
|
||||||
}
|
}
|
||||||
if (!Number.isInteger(num)) {
|
if (!Number.isInteger(num)) {
|
||||||
throw new ParseError('Expected integer value', this.pos - numStr.length);
|
throw new ParseError("Expected integer value", this.pos - numStr.length);
|
||||||
}
|
}
|
||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
@@ -115,33 +137,38 @@ class ValueParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private parseBooleanValue(): boolean {
|
private parseBooleanValue(): boolean {
|
||||||
if (this.consumeStr('true')) {
|
if (this.consumeStr("true")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.consumeStr('false')) {
|
if (this.consumeStr("false")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
throw new ParseError('Expected true or false', this.pos);
|
throw new ParseError("Expected true or false", this.pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseStringLiteralValue(schema: StringLiteralSchema): string {
|
private parseStringLiteralValue(schema: StringLiteralSchema): string {
|
||||||
const quote = this.peek();
|
const quote = this.peek();
|
||||||
|
|
||||||
// 支持带引号或不带引号的字符串值
|
// 支持带引号或不带引号的字符串值
|
||||||
if (quote === '"' || quote === "'") {
|
if (quote === '"' || quote === "'") {
|
||||||
this.consume(); // Consume opening quote
|
this.consume(); // Consume opening quote
|
||||||
|
|
||||||
let value = '';
|
let value = "";
|
||||||
while (this.pos < this.input.length) {
|
while (this.pos < this.input.length) {
|
||||||
const char = this.peek();
|
const char = this.peek();
|
||||||
|
|
||||||
if (char === '\\') {
|
if (char === "\\") {
|
||||||
this.consume();
|
this.consume();
|
||||||
const nextChar = this.consume();
|
const nextChar = this.consume();
|
||||||
if (nextChar === '"' || nextChar === "'" || nextChar === '\\' || nextChar === ';') {
|
if (
|
||||||
|
nextChar === '"' ||
|
||||||
|
nextChar === "'" ||
|
||||||
|
nextChar === "\\" ||
|
||||||
|
nextChar === ";"
|
||||||
|
) {
|
||||||
value += nextChar;
|
value += nextChar;
|
||||||
} else {
|
} else {
|
||||||
value += '\\' + nextChar;
|
value += "\\" + nextChar;
|
||||||
}
|
}
|
||||||
} else if (char === quote) {
|
} else if (char === quote) {
|
||||||
this.consume(); // Consume closing quote
|
this.consume(); // Consume closing quote
|
||||||
@@ -149,7 +176,7 @@ class ValueParser {
|
|||||||
if (value !== schema.value) {
|
if (value !== schema.value) {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
`Invalid value '"${value}"'. Expected '"${schema.value}"'`,
|
`Invalid value '"${value}"'. Expected '"${schema.value}"'`,
|
||||||
this.pos
|
this.pos,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,24 +186,24 @@ class ValueParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ParseError('Unterminated string literal', this.pos);
|
throw new ParseError("Unterminated string literal", this.pos);
|
||||||
} else {
|
} else {
|
||||||
// 不带引号的字符串,像普通字符串一样解析
|
// 不带引号的字符串,像普通字符串一样解析
|
||||||
let value = '';
|
let value = "";
|
||||||
while (this.pos < this.input.length) {
|
while (this.pos < this.input.length) {
|
||||||
const char = this.peek();
|
const char = this.peek();
|
||||||
if (char === ';' || char === ']' || char === ')') {
|
if (char === ";" || char === "]" || char === ")") {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
value += this.consume();
|
value += this.consume();
|
||||||
}
|
}
|
||||||
|
|
||||||
value = value.trim();
|
value = value.trim();
|
||||||
|
|
||||||
if (value !== schema.value) {
|
if (value !== schema.value) {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
`Invalid value '${value}'. Expected '${schema.value}'`,
|
`Invalid value '${value}'. Expected '${schema.value}'`,
|
||||||
this.pos - value.length
|
this.pos - value.length,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +214,7 @@ class ValueParser {
|
|||||||
private parseUnionValue(schema: UnionSchema): unknown {
|
private parseUnionValue(schema: UnionSchema): unknown {
|
||||||
const savedPos = this.pos;
|
const savedPos = this.pos;
|
||||||
const errors: Error[] = [];
|
const errors: Error[] = [];
|
||||||
|
|
||||||
// Try each union member until one succeeds
|
// Try each union member until one succeeds
|
||||||
for (let i = 0; i < schema.members.length; i++) {
|
for (let i = 0; i < schema.members.length; i++) {
|
||||||
this.pos = savedPos;
|
this.pos = savedPos;
|
||||||
@@ -198,27 +225,30 @@ class ValueParser {
|
|||||||
// Continue to next member
|
// Continue to next member
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If all members fail, throw a descriptive error
|
// If all members fail, throw a descriptive error
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
`Value does not match any union member. Tried ${schema.members.length} alternatives.`,
|
`Value does not match any union member. Tried ${schema.members.length} alternatives.`,
|
||||||
this.pos
|
this.pos,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseTupleValue(schema: TupleSchema, allowOmitBrackets: boolean): unknown[] {
|
private parseTupleValue(
|
||||||
|
schema: TupleSchema,
|
||||||
|
allowOmitBrackets: boolean,
|
||||||
|
): unknown[] {
|
||||||
let hasOpenBracket = false;
|
let hasOpenBracket = false;
|
||||||
|
|
||||||
if (this.peek() === '[') {
|
if (this.peek() === "[") {
|
||||||
this.consume();
|
this.consume();
|
||||||
hasOpenBracket = true;
|
hasOpenBracket = true;
|
||||||
} else if (!allowOmitBrackets) {
|
} else if (!allowOmitBrackets) {
|
||||||
throw new ParseError('Expected [', this.pos);
|
throw new ParseError("Expected [", this.pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (this.peek() === ']' && hasOpenBracket) {
|
if (this.peek() === "]" && hasOpenBracket) {
|
||||||
this.consume();
|
this.consume();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -227,7 +257,7 @@ class ValueParser {
|
|||||||
for (let i = 0; i < schema.elements.length; i++) {
|
for (let i = 0; i < schema.elements.length; i++) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
const elementSchema = schema.elements[i];
|
const elementSchema = schema.elements[i];
|
||||||
|
|
||||||
// Try to consume optional name prefix (e.g., "current:")
|
// Try to consume optional name prefix (e.g., "current:")
|
||||||
if (elementSchema.name) {
|
if (elementSchema.name) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
@@ -239,13 +269,13 @@ class ValueParser {
|
|||||||
this.pos = savedPos;
|
this.pos = savedPos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push(this.parseValue(elementSchema.schema, false));
|
result.push(this.parseValue(elementSchema.schema, false));
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (i < schema.elements.length - 1) {
|
if (i < schema.elements.length - 1) {
|
||||||
if (!this.consumeStr(';')) {
|
if (!this.consumeStr(";")) {
|
||||||
throw new ParseError('Expected ;', this.pos);
|
throw new ParseError("Expected ;", this.pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,39 +283,43 @@ class ValueParser {
|
|||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (hasOpenBracket) {
|
if (hasOpenBracket) {
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseArrayValue(schema: ArraySchema, allowOmitBrackets: boolean): unknown[] {
|
private parseArrayValue(
|
||||||
|
schema: ArraySchema,
|
||||||
|
allowOmitBrackets: boolean,
|
||||||
|
): unknown[] {
|
||||||
let hasOpenBracket = false;
|
let hasOpenBracket = false;
|
||||||
const elementIsTupleOrArray = schema.element.type === 'tuple' || schema.element.type === 'array';
|
const elementIsTupleOrArray =
|
||||||
|
schema.element.type === "tuple" || schema.element.type === "array";
|
||||||
|
|
||||||
if (this.pos >= this.input.length || !this.input.trim()) {
|
if (this.pos >= this.input.length || !this.input.trim()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.peek() === '[') {
|
if (this.peek() === "[") {
|
||||||
if (!elementIsTupleOrArray) {
|
if (!elementIsTupleOrArray) {
|
||||||
this.consume();
|
this.consume();
|
||||||
hasOpenBracket = true;
|
hasOpenBracket = true;
|
||||||
} else if (this.input[this.pos + 1] === '[') {
|
} else if (this.input[this.pos + 1] === "[") {
|
||||||
this.consume();
|
this.consume();
|
||||||
hasOpenBracket = true;
|
hasOpenBracket = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasOpenBracket && !allowOmitBrackets && !elementIsTupleOrArray) {
|
if (!hasOpenBracket && !allowOmitBrackets && !elementIsTupleOrArray) {
|
||||||
throw new ParseError('Expected [', this.pos);
|
throw new ParseError("Expected [", this.pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (this.peek() === ']' && hasOpenBracket) {
|
if (this.peek() === "]" && hasOpenBracket) {
|
||||||
this.consume();
|
this.consume();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -296,7 +330,7 @@ class ValueParser {
|
|||||||
result.push(this.parseValue(schema.element, false));
|
result.push(this.parseValue(schema.element, false));
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (!this.consumeStr(';')) {
|
if (!this.consumeStr(";")) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,15 +338,17 @@ class ValueParser {
|
|||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (hasOpenBracket) {
|
if (hasOpenBracket) {
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseReferenceValue(schema: ReferenceSchema): string | string[] | null {
|
private parseReferenceValue(
|
||||||
|
schema: ReferenceSchema,
|
||||||
|
): string | string[] | null {
|
||||||
if (schema.isOptional) {
|
if (schema.isOptional) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
if (this.pos >= this.input.length) {
|
if (this.pos >= this.input.length) {
|
||||||
@@ -323,14 +359,14 @@ class ValueParser {
|
|||||||
if (schema.isArray) {
|
if (schema.isArray) {
|
||||||
// Parse array of IDs: [id1; id2; id3]
|
// Parse array of IDs: [id1; id2; id3]
|
||||||
let hasOpenBracket = false;
|
let hasOpenBracket = false;
|
||||||
if (this.peek() === '[') {
|
if (this.peek() === "[") {
|
||||||
this.consume();
|
this.consume();
|
||||||
hasOpenBracket = true;
|
hasOpenBracket = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (this.peek() === ']' && hasOpenBracket) {
|
if (this.peek() === "]" && hasOpenBracket) {
|
||||||
this.consume();
|
this.consume();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -339,31 +375,35 @@ class ValueParser {
|
|||||||
while (true) {
|
while (true) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
// Parse each ID as a string
|
// Parse each ID as a string
|
||||||
let id = '';
|
let id = "";
|
||||||
while (this.pos < this.input.length && this.peek() !== ';' && this.peek() !== ']') {
|
while (
|
||||||
|
this.pos < this.input.length &&
|
||||||
|
this.peek() !== ";" &&
|
||||||
|
this.peek() !== "]"
|
||||||
|
) {
|
||||||
id += this.consume();
|
id += this.consume();
|
||||||
}
|
}
|
||||||
ids.push(id.trim());
|
ids.push(id.trim());
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (!this.consumeStr(';')) {
|
if (!this.consumeStr(";")) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasOpenBracket) {
|
if (hasOpenBracket) {
|
||||||
if (!this.consumeStr(']')) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError('Expected ]', this.pos);
|
throw new ParseError("Expected ]", this.pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ids;
|
return ids;
|
||||||
} else {
|
} else {
|
||||||
// Parse single ID as string
|
// Parse single ID as string
|
||||||
let id = '';
|
let id = "";
|
||||||
while (this.pos < this.input.length) {
|
while (this.pos < this.input.length) {
|
||||||
const char = this.peek();
|
const char = this.peek();
|
||||||
if (char === ';' || char === ']' || char === ',') {
|
if (char === ";" || char === "]" || char === ",") {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
id += this.consume();
|
id += this.consume();
|
||||||
@@ -383,92 +423,116 @@ class ValueParser {
|
|||||||
|
|
||||||
export function parseValue(schema: Schema, valueString: string): unknown {
|
export function parseValue(schema: Schema, valueString: string): unknown {
|
||||||
const parser = new ValueParser(valueString.trim());
|
const parser = new ValueParser(valueString.trim());
|
||||||
const allowOmitBrackets = schema.type === 'tuple' || schema.type === 'array';
|
const allowOmitBrackets = schema.type === "tuple" || schema.type === "array";
|
||||||
const value = parser.parseValue(schema, allowOmitBrackets);
|
const value = parser.parseValue(schema, allowOmitBrackets);
|
||||||
|
|
||||||
if (parser.getPosition() < parser.getInputLength()) {
|
if (parser.getPosition() < parser.getInputLength()) {
|
||||||
throw new ParseError('Unexpected input after value', parser.getPosition());
|
throw new ParseError("Unexpected input after value", parser.getPosition());
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function schemaToTypeString(schema: Schema, resourceNames?: Map<string, string>): string {
|
export function schemaToTypeString(
|
||||||
|
schema: Schema,
|
||||||
|
resourceNames?: Map<string, string>,
|
||||||
|
): string {
|
||||||
switch (schema.type) {
|
switch (schema.type) {
|
||||||
case 'string':
|
case "string":
|
||||||
return 'string';
|
return "string";
|
||||||
case 'number':
|
case "number":
|
||||||
case 'int':
|
case "int":
|
||||||
case 'float':
|
case "float":
|
||||||
return 'number';
|
return "number";
|
||||||
case 'boolean':
|
case "boolean":
|
||||||
return 'boolean';
|
return "boolean";
|
||||||
case 'stringLiteral':
|
case "stringLiteral":
|
||||||
return `"${schema.value}"`;
|
return `"${schema.value}"`;
|
||||||
case 'union':
|
case "union":
|
||||||
return schema.members.map(m => schemaToTypeString(m, resourceNames)).join(' | ');
|
return schema.members
|
||||||
case 'reference': {
|
.map((m) => schemaToTypeString(m, resourceNames))
|
||||||
const typeName = resourceNames?.get(schema.tableName) ||
|
.join(" | ");
|
||||||
|
case "reference": {
|
||||||
|
const typeName =
|
||||||
|
resourceNames?.get(schema.tableName) ||
|
||||||
schema.tableName.charAt(0).toUpperCase() + schema.tableName.slice(1);
|
schema.tableName.charAt(0).toUpperCase() + schema.tableName.slice(1);
|
||||||
const baseType = schema.isArray ? `${typeName}[]` : typeName;
|
const baseType = schema.isArray ? `${typeName}[]` : typeName;
|
||||||
return schema.isOptional ? `${baseType} | null` : baseType;
|
return schema.isOptional ? `${baseType} | null` : baseType;
|
||||||
}
|
}
|
||||||
case 'array':
|
case "reverseReference": {
|
||||||
if (schema.element.type === 'tuple') {
|
const typeName =
|
||||||
|
resourceNames?.get(schema.tableName) ||
|
||||||
|
schema.tableName.charAt(0).toUpperCase() + schema.tableName.slice(1);
|
||||||
|
// Reverse references always resolve to an array (one-to-many)
|
||||||
|
const baseType = `${typeName}[]`;
|
||||||
|
return schema.isOptional ? `${baseType} | null` : baseType;
|
||||||
|
}
|
||||||
|
case "array":
|
||||||
|
if (schema.element.type === "tuple") {
|
||||||
const tupleElements = schema.element.elements.map((el) => {
|
const tupleElements = schema.element.elements.map((el) => {
|
||||||
const typeStr = schemaToTypeString(el.schema, resourceNames);
|
const typeStr = schemaToTypeString(el.schema, resourceNames);
|
||||||
return el.name ? `${el.name}: ${typeStr}` : typeStr;
|
return el.name ? `${el.name}: ${typeStr}` : typeStr;
|
||||||
});
|
});
|
||||||
return `[${tupleElements.join(', ')}][]`;
|
return `[${tupleElements.join(", ")}][]`;
|
||||||
}
|
}
|
||||||
const elementType = schemaToTypeString(schema.element, resourceNames);
|
const elementType = schemaToTypeString(schema.element, resourceNames);
|
||||||
if (schema.element.type === 'union') {
|
if (schema.element.type === "union") {
|
||||||
return `(${elementType})[]`;
|
return `(${elementType})[]`;
|
||||||
}
|
}
|
||||||
return `${elementType}[]`;
|
return `${elementType}[]`;
|
||||||
case 'tuple':
|
case "tuple":
|
||||||
const tupleElements = schema.elements.map((el) => {
|
const tupleElements = schema.elements.map((el) => {
|
||||||
const typeStr = schemaToTypeString(el.schema, resourceNames);
|
const typeStr = schemaToTypeString(el.schema, resourceNames);
|
||||||
return el.name ? `${el.name}: ${typeStr}` : typeStr;
|
return el.name ? `${el.name}: ${typeStr}` : typeStr;
|
||||||
});
|
});
|
||||||
return `[${tupleElements.join(', ')}]`;
|
return `[${tupleElements.join(", ")}]`;
|
||||||
default:
|
default:
|
||||||
return 'unknown';
|
return "unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createValidator(schema: Schema): (value: unknown) => boolean {
|
export function createValidator(schema: Schema): (value: unknown) => boolean {
|
||||||
return function validate(value: unknown): boolean {
|
return function validate(value: unknown): boolean {
|
||||||
switch (schema.type) {
|
switch (schema.type) {
|
||||||
case 'string':
|
case "string":
|
||||||
return typeof value === 'string';
|
return typeof value === "string";
|
||||||
case 'number':
|
case "number":
|
||||||
return typeof value === 'number' && !isNaN(value);
|
return typeof value === "number" && !isNaN(value);
|
||||||
case 'int':
|
case "int":
|
||||||
return typeof value === 'number' && !isNaN(value) && Number.isInteger(value);
|
return (
|
||||||
case 'float':
|
typeof value === "number" && !isNaN(value) && Number.isInteger(value)
|
||||||
return typeof value === 'number' && !isNaN(value);
|
);
|
||||||
case 'boolean':
|
case "float":
|
||||||
return typeof value === 'boolean';
|
return typeof value === "number" && !isNaN(value);
|
||||||
case 'stringLiteral':
|
case "boolean":
|
||||||
return typeof value === 'string' && value === schema.value;
|
return typeof value === "boolean";
|
||||||
case 'union':
|
case "stringLiteral":
|
||||||
|
return typeof value === "string" && value === schema.value;
|
||||||
|
case "union":
|
||||||
return schema.members.some((member) => createValidator(member)(value));
|
return schema.members.some((member) => createValidator(member)(value));
|
||||||
case 'tuple':
|
case "tuple":
|
||||||
if (!Array.isArray(value)) return false;
|
if (!Array.isArray(value)) return false;
|
||||||
if (value.length !== schema.elements.length) return false;
|
if (value.length !== schema.elements.length) return false;
|
||||||
return schema.elements.every((elementSchema, index) =>
|
return schema.elements.every((elementSchema, index) =>
|
||||||
createValidator(elementSchema.schema)(value[index])
|
createValidator(elementSchema.schema)(value[index]),
|
||||||
);
|
);
|
||||||
case 'array':
|
case "array":
|
||||||
if (!Array.isArray(value)) return false;
|
if (!Array.isArray(value)) return false;
|
||||||
return value.every((item) => createValidator(schema.element)(item));
|
return value.every((item) => createValidator(schema.element)(item));
|
||||||
case 'reference':
|
case "reference":
|
||||||
if (schema.isOptional && value === null) return true;
|
if (schema.isOptional && value === null) return true;
|
||||||
if (schema.isArray) {
|
if (schema.isArray) {
|
||||||
return Array.isArray(value) && value.every((id) => typeof id === 'string');
|
return (
|
||||||
|
Array.isArray(value) && value.every((id) => typeof id === "string")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return typeof value === 'string' || (Array.isArray(value) && value.every((id) => typeof id === 'string'));
|
return (
|
||||||
|
typeof value === "string" ||
|
||||||
|
(Array.isArray(value) && value.every((id) => typeof id === "string"))
|
||||||
|
);
|
||||||
|
case "reverseReference":
|
||||||
|
if (schema.isOptional && value === null) return true;
|
||||||
|
return Array.isArray(value);
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user