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:
+899
-503
File diff suppressed because it is too large
Load Diff
+563
-154
File diff suppressed because it is too large
Load Diff
+560
-313
File diff suppressed because it is too large
Load Diff
+32
-6
@@ -1,7 +1,17 @@
|
||||
import { parseSchema } from './parser';
|
||||
import { parseValue, createValidator, schemaToTypeString } from './validator';
|
||||
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, ReferenceSchema, StringLiteralSchema, UnionSchema, ParsedSchema } from './types';
|
||||
import { ParseError } from './parser';
|
||||
import { parseSchema } from "./parser";
|
||||
import { parseValue, createValidator, schemaToTypeString } from "./validator";
|
||||
import type {
|
||||
Schema,
|
||||
PrimitiveSchema,
|
||||
TupleSchema,
|
||||
ArraySchema,
|
||||
ReferenceSchema,
|
||||
ReverseReferenceSchema,
|
||||
StringLiteralSchema,
|
||||
UnionSchema,
|
||||
ParsedSchema,
|
||||
} from "./types";
|
||||
import { ParseError } from "./parser";
|
||||
|
||||
export function defineSchema(schemaString: string): ParsedSchema {
|
||||
const schema = parseSchema(schemaString);
|
||||
@@ -14,5 +24,21 @@ export function defineSchema(schemaString: string): ParsedSchema {
|
||||
};
|
||||
}
|
||||
|
||||
export { parseSchema, parseValue, createValidator, ParseError, schemaToTypeString };
|
||||
export type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, ReferenceSchema, StringLiteralSchema, UnionSchema, ParsedSchema };
|
||||
export {
|
||||
parseSchema,
|
||||
parseValue,
|
||||
createValidator,
|
||||
ParseError,
|
||||
schemaToTypeString,
|
||||
};
|
||||
export type {
|
||||
Schema,
|
||||
PrimitiveSchema,
|
||||
TupleSchema,
|
||||
ArraySchema,
|
||||
ReferenceSchema,
|
||||
ReverseReferenceSchema,
|
||||
StringLiteralSchema,
|
||||
UnionSchema,
|
||||
ParsedSchema,
|
||||
};
|
||||
|
||||
+167
-86
@@ -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 {
|
||||
constructor(message: string, public position?: number) {
|
||||
super(position !== undefined ? `${message} at position ${position}` : message);
|
||||
this.name = 'ParseError';
|
||||
constructor(
|
||||
message: string,
|
||||
public position?: number,
|
||||
) {
|
||||
super(
|
||||
position !== undefined ? `${message} at position ${position}` : message,
|
||||
);
|
||||
this.name = "ParseError";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +38,11 @@ class Parser {
|
||||
}
|
||||
|
||||
private peek(): string {
|
||||
return this.input[this.pos] || '';
|
||||
return this.input[this.pos] || "";
|
||||
}
|
||||
|
||||
private consume(): string {
|
||||
return this.input[this.pos++] || '';
|
||||
return this.input[this.pos++] || "";
|
||||
}
|
||||
|
||||
private skipWhitespace(): void {
|
||||
@@ -64,17 +79,17 @@ class Parser {
|
||||
this.skipWhitespace();
|
||||
|
||||
// Check for array suffix: type[]
|
||||
if (this.consumeStr('[')) {
|
||||
if (this.consumeStr("[")) {
|
||||
this.skipWhitespace();
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
if (!this.consumeStr("]")) {
|
||||
throw new ParseError("Expected ]", this.pos);
|
||||
}
|
||||
schema = { type: 'array', element: schema };
|
||||
schema = { type: "array", element: schema };
|
||||
this.skipWhitespace();
|
||||
}
|
||||
|
||||
// Check for union type (| symbol)
|
||||
if (this.consumeStr('|')) {
|
||||
if (this.consumeStr("|")) {
|
||||
const members: Schema[] = [schema];
|
||||
|
||||
while (true) {
|
||||
@@ -83,14 +98,14 @@ class Parser {
|
||||
members.push(member);
|
||||
this.skipWhitespace();
|
||||
|
||||
if (!this.consumeStr('|')) {
|
||||
if (!this.consumeStr("|")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'union',
|
||||
members
|
||||
type: "union",
|
||||
members,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -101,23 +116,23 @@ class Parser {
|
||||
this.skipWhitespace();
|
||||
|
||||
// Check for parentheses (grouping)
|
||||
if (this.consumeStr('(')) {
|
||||
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);
|
||||
if (!this.consumeStr(")")) {
|
||||
throw new ParseError("Expected )", this.pos);
|
||||
}
|
||||
|
||||
// Check if there's an array suffix: (union)[]
|
||||
this.skipWhitespace();
|
||||
if (this.consumeStr('[')) {
|
||||
if (this.consumeStr("[")) {
|
||||
this.skipWhitespace();
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
if (!this.consumeStr("]")) {
|
||||
throw new ParseError("Expected ]", this.pos);
|
||||
}
|
||||
return { type: 'array', element: schema };
|
||||
return { type: "array", element: schema };
|
||||
}
|
||||
|
||||
return schema;
|
||||
@@ -128,86 +143,91 @@ class Parser {
|
||||
return this.parseStringLiteralSchema();
|
||||
}
|
||||
|
||||
// Check for reverse reference syntax: ~tablename(foreignKey)
|
||||
if (this.consumeStr("~")) {
|
||||
return this.parseReverseReferenceSchema();
|
||||
}
|
||||
|
||||
// Check for reference syntax: @tablename[]
|
||||
if (this.consumeStr('@')) {
|
||||
if (this.consumeStr("@")) {
|
||||
return this.parseReferenceSchema();
|
||||
}
|
||||
|
||||
if (this.consumeStr('string')) {
|
||||
if (this.consumeStr('[')) {
|
||||
if (this.consumeStr("string")) {
|
||||
if (this.consumeStr("[")) {
|
||||
this.skipWhitespace();
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
if (!this.consumeStr("]")) {
|
||||
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('[')) {
|
||||
if (this.consumeStr("number")) {
|
||||
if (this.consumeStr("[")) {
|
||||
this.skipWhitespace();
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
if (!this.consumeStr("]")) {
|
||||
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('[')) {
|
||||
if (this.consumeStr("int")) {
|
||||
if (this.consumeStr("[")) {
|
||||
this.skipWhitespace();
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
if (!this.consumeStr("]")) {
|
||||
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('[')) {
|
||||
if (this.consumeStr("float")) {
|
||||
if (this.consumeStr("[")) {
|
||||
this.skipWhitespace();
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
if (!this.consumeStr("]")) {
|
||||
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('[')) {
|
||||
if (this.consumeStr("boolean")) {
|
||||
if (this.consumeStr("[")) {
|
||||
this.skipWhitespace();
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
if (!this.consumeStr("]")) {
|
||||
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[] = [];
|
||||
this.skipWhitespace();
|
||||
|
||||
if (this.peek() === ']') {
|
||||
if (this.peek() === "]") {
|
||||
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());
|
||||
this.skipWhitespace();
|
||||
|
||||
if (this.consumeStr(';')) {
|
||||
if (this.consumeStr(";")) {
|
||||
const remainingElements: NamedSchema[] = [];
|
||||
while (true) {
|
||||
this.skipWhitespace();
|
||||
remainingElements.push(this.parseNamedSchema());
|
||||
this.skipWhitespace();
|
||||
|
||||
if (!this.consumeStr(';')) {
|
||||
if (!this.consumeStr(";")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -216,57 +236,67 @@ class Parser {
|
||||
|
||||
this.skipWhitespace();
|
||||
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
if (!this.consumeStr("]")) {
|
||||
throw new ParseError("Expected ]", this.pos);
|
||||
}
|
||||
|
||||
if (this.consumeStr('[')) {
|
||||
if (this.consumeStr("[")) {
|
||||
this.skipWhitespace();
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
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: elements[0].schema };
|
||||
}
|
||||
return { type: 'array', element: { type: 'tuple', elements } };
|
||||
return { type: "array", element: { type: "tuple", elements } };
|
||||
}
|
||||
|
||||
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 {
|
||||
const value = this.parseStringLiteral();
|
||||
return {
|
||||
type: 'stringLiteral',
|
||||
value
|
||||
type: "stringLiteral",
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
private parseStringLiteral(): string {
|
||||
const quote = this.peek();
|
||||
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
|
||||
|
||||
let value = '';
|
||||
let value = "";
|
||||
while (this.pos < this.input.length) {
|
||||
const char = this.peek();
|
||||
|
||||
if (char === '\\') {
|
||||
if (char === "\\") {
|
||||
this.consume();
|
||||
const nextChar = this.consume();
|
||||
if (nextChar === '"' || nextChar === "'" || nextChar === '\\' ||
|
||||
nextChar === '|' || nextChar === ';' || nextChar === '(' || nextChar === ')') {
|
||||
if (
|
||||
nextChar === '"' ||
|
||||
nextChar === "'" ||
|
||||
nextChar === "\\" ||
|
||||
nextChar === "|" ||
|
||||
nextChar === ";" ||
|
||||
nextChar === "(" ||
|
||||
nextChar === ")"
|
||||
) {
|
||||
value += nextChar;
|
||||
} else {
|
||||
value += '\\' + nextChar;
|
||||
value += "\\" + nextChar;
|
||||
}
|
||||
} else if (char === quote) {
|
||||
this.consume(); // Consume closing quote
|
||||
@@ -276,14 +306,14 @@ class Parser {
|
||||
}
|
||||
}
|
||||
|
||||
throw new ParseError('Unterminated string literal', this.pos);
|
||||
throw new ParseError("Unterminated string literal", this.pos);
|
||||
}
|
||||
|
||||
private parseNamedSchema(): NamedSchema {
|
||||
this.skipWhitespace();
|
||||
|
||||
const startpos = this.pos;
|
||||
let identifier = '';
|
||||
let identifier = "";
|
||||
|
||||
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
|
||||
identifier += this.consume();
|
||||
@@ -296,7 +326,7 @@ class Parser {
|
||||
|
||||
this.skipWhitespace();
|
||||
|
||||
if (this.consumeStr(':')) {
|
||||
if (this.consumeStr(":")) {
|
||||
this.skipWhitespace();
|
||||
const name = identifier;
|
||||
const schema = this.parseSchema();
|
||||
@@ -310,23 +340,23 @@ class Parser {
|
||||
|
||||
private parseReferenceSchema(): Schema {
|
||||
// Parse table name
|
||||
let tableName = '';
|
||||
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);
|
||||
throw new ParseError("Expected table name after @", this.pos);
|
||||
}
|
||||
|
||||
this.skipWhitespace();
|
||||
|
||||
// Check for array syntax
|
||||
if (this.consumeStr('[]')) {
|
||||
if (this.consumeStr("[]")) {
|
||||
this.skipWhitespace();
|
||||
const isOptional = this.consumeStr('?');
|
||||
const isOptional = this.consumeStr("?");
|
||||
return {
|
||||
type: 'reference',
|
||||
type: "reference",
|
||||
tableName,
|
||||
isArray: true,
|
||||
isOptional,
|
||||
@@ -334,16 +364,67 @@ class Parser {
|
||||
}
|
||||
|
||||
// Check for optional suffix
|
||||
const isOptional = this.consumeStr('?');
|
||||
const isOptional = this.consumeStr("?");
|
||||
|
||||
// Single reference (non-array)
|
||||
return {
|
||||
type: 'reference',
|
||||
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 {
|
||||
@@ -351,7 +432,7 @@ export function parseSchema(schemaString: string): Schema {
|
||||
const schema = parser.parseSchema();
|
||||
|
||||
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;
|
||||
|
||||
+24
-7
@@ -1,4 +1,4 @@
|
||||
export type SchemaType = 'string' | 'number' | 'int' | 'float' | 'boolean';
|
||||
export type SchemaType = "string" | "number" | "int" | "float" | "boolean";
|
||||
|
||||
export interface PrimitiveSchema {
|
||||
type: SchemaType;
|
||||
@@ -10,17 +10,17 @@ export interface NamedSchema {
|
||||
}
|
||||
|
||||
export interface TupleSchema {
|
||||
type: 'tuple';
|
||||
type: "tuple";
|
||||
elements: NamedSchema[];
|
||||
}
|
||||
|
||||
export interface ArraySchema {
|
||||
type: 'array';
|
||||
type: "array";
|
||||
element: Schema;
|
||||
}
|
||||
|
||||
export interface ReferenceSchema {
|
||||
type: 'reference';
|
||||
type: "reference";
|
||||
/** Referenced table name (e.g., 'parts' from '@parts[]') */
|
||||
tableName: string;
|
||||
/** Whether it's an array reference */
|
||||
@@ -29,17 +29,34 @@ export interface ReferenceSchema {
|
||||
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 {
|
||||
type: 'stringLiteral';
|
||||
type: "stringLiteral";
|
||||
value: string; // The literal string value (without quotes)
|
||||
}
|
||||
|
||||
export interface UnionSchema {
|
||||
type: 'union';
|
||||
type: "union";
|
||||
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 {
|
||||
schema: Schema;
|
||||
|
||||
+171
-107
@@ -1,5 +1,15 @@
|
||||
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, NamedSchema, ReferenceSchema, StringLiteralSchema, UnionSchema } from './types';
|
||||
import { ParseError } from './parser';
|
||||
import type {
|
||||
Schema,
|
||||
PrimitiveSchema,
|
||||
TupleSchema,
|
||||
ArraySchema,
|
||||
NamedSchema,
|
||||
ReferenceSchema,
|
||||
ReverseReferenceSchema,
|
||||
StringLiteralSchema,
|
||||
UnionSchema,
|
||||
} from "./types";
|
||||
import { ParseError } from "./parser";
|
||||
|
||||
class ValueParser {
|
||||
private input: string;
|
||||
@@ -10,11 +20,11 @@ class ValueParser {
|
||||
}
|
||||
|
||||
private peek(): string {
|
||||
return this.input[this.pos] || '';
|
||||
return this.input[this.pos] || "";
|
||||
}
|
||||
|
||||
private consume(): string {
|
||||
return this.input[this.pos++] || '';
|
||||
return this.input[this.pos++] || "";
|
||||
}
|
||||
|
||||
private skipWhitespace(): void {
|
||||
@@ -35,46 +45,58 @@ class ValueParser {
|
||||
this.skipWhitespace();
|
||||
|
||||
switch (schema.type) {
|
||||
case 'string':
|
||||
case "string":
|
||||
return this.parseStringValue();
|
||||
case 'number':
|
||||
case "number":
|
||||
return this.parseNumberValue();
|
||||
case 'int':
|
||||
case "int":
|
||||
return this.parseIntValue();
|
||||
case 'float':
|
||||
case "float":
|
||||
return this.parseFloatValue();
|
||||
case 'boolean':
|
||||
case "boolean":
|
||||
return this.parseBooleanValue();
|
||||
case 'stringLiteral':
|
||||
case "stringLiteral":
|
||||
return this.parseStringLiteralValue(schema);
|
||||
case 'union':
|
||||
case "union":
|
||||
return this.parseUnionValue(schema);
|
||||
case 'tuple':
|
||||
case "tuple":
|
||||
return this.parseTupleValue(schema, allowOmitBrackets);
|
||||
case 'array':
|
||||
case "array":
|
||||
return this.parseArrayValue(schema, allowOmitBrackets);
|
||||
case 'reference':
|
||||
case "reference":
|
||||
// Reference values are parsed as strings (IDs) initially, resolved later
|
||||
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:
|
||||
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 {
|
||||
let result = '';
|
||||
let result = "";
|
||||
while (this.pos < this.input.length) {
|
||||
const char = this.peek();
|
||||
|
||||
if (char === '\\') {
|
||||
if (char === "\\") {
|
||||
this.consume();
|
||||
const nextChar = this.consume();
|
||||
if (nextChar === ';' || nextChar === '[' || nextChar === ']' || nextChar === '\\') {
|
||||
if (
|
||||
nextChar === ";" ||
|
||||
nextChar === "[" ||
|
||||
nextChar === "]" ||
|
||||
nextChar === "\\"
|
||||
) {
|
||||
result += nextChar;
|
||||
} else {
|
||||
result += '\\' + nextChar;
|
||||
result += "\\" + nextChar;
|
||||
}
|
||||
} else if (char === ';' || char === ']') {
|
||||
} else if (char === ";" || char === "]") {
|
||||
break;
|
||||
} else {
|
||||
result += this.consume();
|
||||
@@ -84,28 +106,28 @@ class ValueParser {
|
||||
}
|
||||
|
||||
private parseNumberValue(): number {
|
||||
let numStr = '';
|
||||
let numStr = "";
|
||||
while (this.pos < this.input.length && /[\d.\-+eE]/.test(this.peek())) {
|
||||
numStr += this.consume();
|
||||
}
|
||||
const num = parseFloat(numStr);
|
||||
if (isNaN(num)) {
|
||||
throw new ParseError('Invalid number', this.pos - numStr.length);
|
||||
throw new ParseError("Invalid number", this.pos - numStr.length);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
private parseIntValue(): number {
|
||||
let numStr = '';
|
||||
let numStr = "";
|
||||
while (this.pos < this.input.length && /[\d.\-+eE]/.test(this.peek())) {
|
||||
numStr += this.consume();
|
||||
}
|
||||
const num = parseFloat(numStr);
|
||||
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)) {
|
||||
throw new ParseError('Expected integer value', this.pos - numStr.length);
|
||||
throw new ParseError("Expected integer value", this.pos - numStr.length);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
@@ -115,13 +137,13 @@ class ValueParser {
|
||||
}
|
||||
|
||||
private parseBooleanValue(): boolean {
|
||||
if (this.consumeStr('true')) {
|
||||
if (this.consumeStr("true")) {
|
||||
return true;
|
||||
}
|
||||
if (this.consumeStr('false')) {
|
||||
if (this.consumeStr("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 {
|
||||
@@ -131,17 +153,22 @@ class ValueParser {
|
||||
if (quote === '"' || quote === "'") {
|
||||
this.consume(); // Consume opening quote
|
||||
|
||||
let value = '';
|
||||
let value = "";
|
||||
while (this.pos < this.input.length) {
|
||||
const char = this.peek();
|
||||
|
||||
if (char === '\\') {
|
||||
if (char === "\\") {
|
||||
this.consume();
|
||||
const nextChar = this.consume();
|
||||
if (nextChar === '"' || nextChar === "'" || nextChar === '\\' || nextChar === ';') {
|
||||
if (
|
||||
nextChar === '"' ||
|
||||
nextChar === "'" ||
|
||||
nextChar === "\\" ||
|
||||
nextChar === ";"
|
||||
) {
|
||||
value += nextChar;
|
||||
} else {
|
||||
value += '\\' + nextChar;
|
||||
value += "\\" + nextChar;
|
||||
}
|
||||
} else if (char === quote) {
|
||||
this.consume(); // Consume closing quote
|
||||
@@ -149,7 +176,7 @@ class ValueParser {
|
||||
if (value !== schema.value) {
|
||||
throw new ParseError(
|
||||
`Invalid value '"${value}"'. Expected '"${schema.value}"'`,
|
||||
this.pos
|
||||
this.pos,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,13 +186,13 @@ class ValueParser {
|
||||
}
|
||||
}
|
||||
|
||||
throw new ParseError('Unterminated string literal', this.pos);
|
||||
throw new ParseError("Unterminated string literal", this.pos);
|
||||
} else {
|
||||
// 不带引号的字符串,像普通字符串一样解析
|
||||
let value = '';
|
||||
let value = "";
|
||||
while (this.pos < this.input.length) {
|
||||
const char = this.peek();
|
||||
if (char === ';' || char === ']' || char === ')') {
|
||||
if (char === ";" || char === "]" || char === ")") {
|
||||
break;
|
||||
}
|
||||
value += this.consume();
|
||||
@@ -176,7 +203,7 @@ class ValueParser {
|
||||
if (value !== schema.value) {
|
||||
throw new ParseError(
|
||||
`Invalid value '${value}'. Expected '${schema.value}'`,
|
||||
this.pos - value.length
|
||||
this.pos - value.length,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,23 +229,26 @@ class ValueParser {
|
||||
// If all members fail, throw a descriptive error
|
||||
throw new ParseError(
|
||||
`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;
|
||||
|
||||
if (this.peek() === '[') {
|
||||
if (this.peek() === "[") {
|
||||
this.consume();
|
||||
hasOpenBracket = true;
|
||||
} else if (!allowOmitBrackets) {
|
||||
throw new ParseError('Expected [', this.pos);
|
||||
throw new ParseError("Expected [", this.pos);
|
||||
}
|
||||
|
||||
this.skipWhitespace();
|
||||
|
||||
if (this.peek() === ']' && hasOpenBracket) {
|
||||
if (this.peek() === "]" && hasOpenBracket) {
|
||||
this.consume();
|
||||
return [];
|
||||
}
|
||||
@@ -244,8 +274,8 @@ class ValueParser {
|
||||
this.skipWhitespace();
|
||||
|
||||
if (i < schema.elements.length - 1) {
|
||||
if (!this.consumeStr(';')) {
|
||||
throw new ParseError('Expected ;', this.pos);
|
||||
if (!this.consumeStr(";")) {
|
||||
throw new ParseError("Expected ;", this.pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,39 +283,43 @@ class ValueParser {
|
||||
this.skipWhitespace();
|
||||
|
||||
if (hasOpenBracket) {
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
if (!this.consumeStr("]")) {
|
||||
throw new ParseError("Expected ]", this.pos);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseArrayValue(schema: ArraySchema, allowOmitBrackets: boolean): unknown[] {
|
||||
private parseArrayValue(
|
||||
schema: ArraySchema,
|
||||
allowOmitBrackets: boolean,
|
||||
): unknown[] {
|
||||
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()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.peek() === '[') {
|
||||
if (this.peek() === "[") {
|
||||
if (!elementIsTupleOrArray) {
|
||||
this.consume();
|
||||
hasOpenBracket = true;
|
||||
} else if (this.input[this.pos + 1] === '[') {
|
||||
} else if (this.input[this.pos + 1] === "[") {
|
||||
this.consume();
|
||||
hasOpenBracket = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasOpenBracket && !allowOmitBrackets && !elementIsTupleOrArray) {
|
||||
throw new ParseError('Expected [', this.pos);
|
||||
throw new ParseError("Expected [", this.pos);
|
||||
}
|
||||
|
||||
this.skipWhitespace();
|
||||
|
||||
if (this.peek() === ']' && hasOpenBracket) {
|
||||
if (this.peek() === "]" && hasOpenBracket) {
|
||||
this.consume();
|
||||
return [];
|
||||
}
|
||||
@@ -296,7 +330,7 @@ class ValueParser {
|
||||
result.push(this.parseValue(schema.element, false));
|
||||
this.skipWhitespace();
|
||||
|
||||
if (!this.consumeStr(';')) {
|
||||
if (!this.consumeStr(";")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -304,15 +338,17 @@ class ValueParser {
|
||||
this.skipWhitespace();
|
||||
|
||||
if (hasOpenBracket) {
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
if (!this.consumeStr("]")) {
|
||||
throw new ParseError("Expected ]", this.pos);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private parseReferenceValue(schema: ReferenceSchema): string | string[] | null {
|
||||
private parseReferenceValue(
|
||||
schema: ReferenceSchema,
|
||||
): string | string[] | null {
|
||||
if (schema.isOptional) {
|
||||
this.skipWhitespace();
|
||||
if (this.pos >= this.input.length) {
|
||||
@@ -323,14 +359,14 @@ class ValueParser {
|
||||
if (schema.isArray) {
|
||||
// Parse array of IDs: [id1; id2; id3]
|
||||
let hasOpenBracket = false;
|
||||
if (this.peek() === '[') {
|
||||
if (this.peek() === "[") {
|
||||
this.consume();
|
||||
hasOpenBracket = true;
|
||||
}
|
||||
|
||||
this.skipWhitespace();
|
||||
|
||||
if (this.peek() === ']' && hasOpenBracket) {
|
||||
if (this.peek() === "]" && hasOpenBracket) {
|
||||
this.consume();
|
||||
return [];
|
||||
}
|
||||
@@ -339,31 +375,35 @@ class ValueParser {
|
||||
while (true) {
|
||||
this.skipWhitespace();
|
||||
// Parse each ID as a string
|
||||
let id = '';
|
||||
while (this.pos < this.input.length && this.peek() !== ';' && this.peek() !== ']') {
|
||||
let id = "";
|
||||
while (
|
||||
this.pos < this.input.length &&
|
||||
this.peek() !== ";" &&
|
||||
this.peek() !== "]"
|
||||
) {
|
||||
id += this.consume();
|
||||
}
|
||||
ids.push(id.trim());
|
||||
this.skipWhitespace();
|
||||
|
||||
if (!this.consumeStr(';')) {
|
||||
if (!this.consumeStr(";")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOpenBracket) {
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
if (!this.consumeStr("]")) {
|
||||
throw new ParseError("Expected ]", this.pos);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
} else {
|
||||
// Parse single ID as string
|
||||
let id = '';
|
||||
let id = "";
|
||||
while (this.pos < this.input.length) {
|
||||
const char = this.peek();
|
||||
if (char === ';' || char === ']' || char === ',') {
|
||||
if (char === ";" || char === "]" || char === ",") {
|
||||
break;
|
||||
}
|
||||
id += this.consume();
|
||||
@@ -383,92 +423,116 @@ class ValueParser {
|
||||
|
||||
export function parseValue(schema: Schema, valueString: string): unknown {
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function schemaToTypeString(schema: Schema, resourceNames?: Map<string, string>): string {
|
||||
export function schemaToTypeString(
|
||||
schema: Schema,
|
||||
resourceNames?: Map<string, string>,
|
||||
): string {
|
||||
switch (schema.type) {
|
||||
case 'string':
|
||||
return 'string';
|
||||
case 'number':
|
||||
case 'int':
|
||||
case 'float':
|
||||
return 'number';
|
||||
case 'boolean':
|
||||
return 'boolean';
|
||||
case 'stringLiteral':
|
||||
case "string":
|
||||
return "string";
|
||||
case "number":
|
||||
case "int":
|
||||
case "float":
|
||||
return "number";
|
||||
case "boolean":
|
||||
return "boolean";
|
||||
case "stringLiteral":
|
||||
return `"${schema.value}"`;
|
||||
case 'union':
|
||||
return schema.members.map(m => schemaToTypeString(m, resourceNames)).join(' | ');
|
||||
case 'reference': {
|
||||
const typeName = resourceNames?.get(schema.tableName) ||
|
||||
case "union":
|
||||
return schema.members
|
||||
.map((m) => schemaToTypeString(m, resourceNames))
|
||||
.join(" | ");
|
||||
case "reference": {
|
||||
const typeName =
|
||||
resourceNames?.get(schema.tableName) ||
|
||||
schema.tableName.charAt(0).toUpperCase() + schema.tableName.slice(1);
|
||||
const baseType = schema.isArray ? `${typeName}[]` : typeName;
|
||||
return schema.isOptional ? `${baseType} | null` : baseType;
|
||||
}
|
||||
case 'array':
|
||||
if (schema.element.type === 'tuple') {
|
||||
case "reverseReference": {
|
||||
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 typeStr = schemaToTypeString(el.schema, resourceNames);
|
||||
return el.name ? `${el.name}: ${typeStr}` : typeStr;
|
||||
});
|
||||
return `[${tupleElements.join(', ')}][]`;
|
||||
return `[${tupleElements.join(", ")}][]`;
|
||||
}
|
||||
const elementType = schemaToTypeString(schema.element, resourceNames);
|
||||
if (schema.element.type === 'union') {
|
||||
if (schema.element.type === "union") {
|
||||
return `(${elementType})[]`;
|
||||
}
|
||||
return `${elementType}[]`;
|
||||
case 'tuple':
|
||||
case "tuple":
|
||||
const tupleElements = schema.elements.map((el) => {
|
||||
const typeStr = schemaToTypeString(el.schema, resourceNames);
|
||||
return el.name ? `${el.name}: ${typeStr}` : typeStr;
|
||||
});
|
||||
return `[${tupleElements.join(', ')}]`;
|
||||
return `[${tupleElements.join(", ")}]`;
|
||||
default:
|
||||
return 'unknown';
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export function createValidator(schema: Schema): (value: unknown) => boolean {
|
||||
return function validate(value: unknown): boolean {
|
||||
switch (schema.type) {
|
||||
case 'string':
|
||||
return typeof value === 'string';
|
||||
case 'number':
|
||||
return typeof value === 'number' && !isNaN(value);
|
||||
case 'int':
|
||||
return typeof value === 'number' && !isNaN(value) && Number.isInteger(value);
|
||||
case 'float':
|
||||
return typeof value === 'number' && !isNaN(value);
|
||||
case 'boolean':
|
||||
return typeof value === 'boolean';
|
||||
case 'stringLiteral':
|
||||
return typeof value === 'string' && value === schema.value;
|
||||
case 'union':
|
||||
case "string":
|
||||
return typeof value === "string";
|
||||
case "number":
|
||||
return typeof value === "number" && !isNaN(value);
|
||||
case "int":
|
||||
return (
|
||||
typeof value === "number" && !isNaN(value) && Number.isInteger(value)
|
||||
);
|
||||
case "float":
|
||||
return typeof value === "number" && !isNaN(value);
|
||||
case "boolean":
|
||||
return typeof value === "boolean";
|
||||
case "stringLiteral":
|
||||
return typeof value === "string" && value === schema.value;
|
||||
case "union":
|
||||
return schema.members.some((member) => createValidator(member)(value));
|
||||
case 'tuple':
|
||||
case "tuple":
|
||||
if (!Array.isArray(value)) return false;
|
||||
if (value.length !== schema.elements.length) return false;
|
||||
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;
|
||||
return value.every((item) => createValidator(schema.element)(item));
|
||||
case 'reference':
|
||||
case "reference":
|
||||
if (schema.isOptional && value === null) return true;
|
||||
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:
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user