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:
parent
0954dcf594
commit
e76ae79b2d
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
38
src/index.ts
38
src/index.ts
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
|
||||||
277
src/parser.ts
277
src/parser.ts
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
src/types.ts
35
src/types.ts
|
|
@ -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;
|
||||||
|
|
|
||||||
294
src/validator.ts
294
src/validator.ts
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue