Files
inline-schema/src/value-parser.ts
T
hypercross c969c7f6fc refactor: enforce mandatory brackets for composite values
Implements Phase 1 and 2 of the syntax rework plan:
- Brackets `[]` are now mandatory for all tuple and array values.
- Removed the `[Type][]` array syntax; arrays now only use `Type[]`.
- `[single]` is now strictly a 1-tuple rather than an array.
- Updated documentation and test fixtures to reflect these breaking
  changes.
2026-08-06 09:19:39 +08:00

482 lines
11 KiB
TypeScript

import type {
Schema,
TupleSchema,
ArraySchema,
ReferenceSchema,
StringLiteralSchema,
UnionSchema,
} from "./types";
import { ParseError } from "./parser";
import { schemaToTypeString } from "./type-utils";
class ValueParser {
private input: string;
private schemaString: string;
private pos: number = 0;
constructor(input: string, schemaString: string) {
this.input = input;
this.schemaString = schemaString;
}
private peek(): string {
return this.input[this.pos] || "";
}
private consume(): string {
return this.input[this.pos++] || "";
}
private skipWhitespace(): void {
while (this.pos < this.input.length && /\s/.test(this.input[this.pos])) {
this.pos++;
}
}
private consumeStr(str: string): boolean {
if (this.input.slice(this.pos, this.pos + str.length) === str) {
this.pos += str.length;
return true;
}
return false;
}
parseValue(schema: Schema): unknown {
this.skipWhitespace();
switch (schema.type) {
case "string":
return this.parseStringValue();
case "number":
return this.parseNumberValue();
case "int":
return this.parseIntValue();
case "float":
return this.parseFloatValue();
case "boolean":
return this.parseBooleanValue();
case "stringLiteral":
return this.parseStringLiteralValue(schema);
case "union":
return this.parseUnionValue(schema);
case "tuple":
return this.parseTupleValue(schema);
case "array":
return this.parseArrayValue(schema);
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,
this.schemaString,
this.input,
);
}
}
private parseStringValue(): string {
let result = "";
while (this.pos < this.input.length) {
const char = this.peek();
if (char === "\\") {
this.consume();
const nextChar = this.consume();
if (
nextChar === ";" ||
nextChar === "[" ||
nextChar === "]" ||
nextChar === "\\"
) {
result += nextChar;
} else {
result += "\\" + nextChar;
}
} else if (char === ";" || char === "]") {
break;
} else {
result += this.consume();
}
}
return result.trim();
}
private parseNumberValue(): number {
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,
this.schemaString,
this.input,
);
}
return num;
}
private parseIntValue(): number {
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,
this.schemaString,
this.input,
);
}
if (!Number.isInteger(num)) {
throw new ParseError(
"Expected integer value",
this.pos - numStr.length,
this.schemaString,
this.input,
);
}
return num;
}
private parseFloatValue(): number {
return this.parseNumberValue();
}
private parseBooleanValue(): boolean {
if (this.consumeStr("true")) {
return true;
}
if (this.consumeStr("false")) {
return false;
}
throw new ParseError(
"Expected true or false",
this.pos,
this.schemaString,
this.input,
);
}
private parseStringLiteralValue(schema: StringLiteralSchema): string {
const quote = this.peek();
// 支持带引号或不带引号的字符串值
if (quote === '"' || quote === "'") {
this.consume(); // Consume opening quote
let value = "";
while (this.pos < this.input.length) {
const char = this.peek();
if (char === "\\") {
this.consume();
const nextChar = this.consume();
if (
nextChar === '"' ||
nextChar === "'" ||
nextChar === "\\" ||
nextChar === ";"
) {
value += nextChar;
} else {
value += "\\" + nextChar;
}
} else if (char === quote) {
this.consume(); // Consume closing quote
if (value !== schema.value) {
throw new ParseError(
`Invalid value '"${value}"'. Expected '"${schema.value}"'`,
this.pos,
this.schemaString,
this.input,
);
}
return value;
} else {
value += this.consume();
}
}
throw new ParseError(
"Unterminated string literal",
this.pos,
this.schemaString,
this.input,
);
} else {
// 不带引号的字符串,像普通字符串一样解析
let value = "";
while (this.pos < this.input.length) {
const char = this.peek();
if (char === ";" || char === "]" || char === ")") {
break;
}
value += this.consume();
}
value = value.trim();
if (value !== schema.value) {
throw new ParseError(
`Invalid value '${value}'. Expected '${schema.value}'`,
this.pos - value.length,
this.schemaString,
this.input,
);
}
return value;
}
}
private parseUnionValue(schema: UnionSchema): unknown {
const savedPos = this.pos;
const errors: Error[] = [];
// Try each union member until one succeeds
for (let i = 0; i < schema.members.length; i++) {
this.pos = savedPos;
try {
return this.parseValue(schema.members[i]);
} catch (e) {
errors.push(e as Error);
// Continue to next member
}
}
// 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.schemaString,
this.input,
);
}
private parseTupleValue(schema: TupleSchema): unknown[] {
if (!this.consumeStr("[")) {
throw new ParseError(
"Expected [",
this.pos,
this.schemaString,
this.input,
);
}
this.skipWhitespace();
if (this.peek() === "]") {
this.consume();
return [];
}
const result: unknown[] = [];
for (let i = 0; i < schema.elements.length; i++) {
this.skipWhitespace();
const elementSchema = schema.elements[i];
// Try to consume optional name prefix (e.g., "current:")
if (elementSchema.name) {
this.skipWhitespace();
const savedPos = this.pos;
if (this.consumeStr(`${elementSchema.name}:`)) {
this.skipWhitespace();
} else {
// Name not found, reset position and continue without name
this.pos = savedPos;
}
}
result.push(this.parseValue(elementSchema.schema));
this.skipWhitespace();
if (i < schema.elements.length - 1) {
if (!this.consumeStr(";")) {
throw new ParseError(
"Expected ;",
this.pos,
this.schemaString,
this.input,
);
}
}
}
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError(
"Expected ]",
this.pos,
this.schemaString,
this.input,
);
}
return result;
}
private parseArrayValue(schema: ArraySchema): unknown[] {
if (this.pos >= this.input.length || !this.input.trim()) {
return [];
}
if (!this.consumeStr("[")) {
throw new ParseError(
"Expected [",
this.pos,
this.schemaString,
this.input,
);
}
this.skipWhitespace();
if (this.peek() === "]") {
this.consume();
return [];
}
const result: unknown[] = [];
while (true) {
this.skipWhitespace();
result.push(this.parseValue(schema.element));
this.skipWhitespace();
if (!this.consumeStr(";")) {
break;
}
}
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError(
"Expected ]",
this.pos,
this.schemaString,
this.input,
);
}
return result;
}
private parseReferenceValue(
schema: ReferenceSchema,
): string | string[] | null {
if (schema.isOptional) {
this.skipWhitespace();
if (this.pos >= this.input.length) {
return null;
}
}
if (schema.isArray) {
// Parse array of IDs: [id1; id2; id3]
if (!this.consumeStr("[")) {
throw new ParseError(
"Expected [",
this.pos,
this.schemaString,
this.input,
);
}
this.skipWhitespace();
if (this.peek() === "]") {
this.consume();
return [];
}
const ids: string[] = [];
while (true) {
this.skipWhitespace();
// Parse each ID as a string
let id = "";
while (
this.pos < this.input.length &&
this.peek() !== ";" &&
this.peek() !== "]"
) {
id += this.consume();
}
ids.push(id.trim());
this.skipWhitespace();
if (!this.consumeStr(";")) {
break;
}
}
this.skipWhitespace();
if (!this.consumeStr("]")) {
throw new ParseError(
"Expected ]",
this.pos,
this.schemaString,
this.input,
);
}
return ids;
} else {
// Parse single ID as string
let id = "";
while (this.pos < this.input.length) {
const char = this.peek();
if (char === ";" || char === "]" || char === ",") {
break;
}
id += this.consume();
}
return id.trim();
}
}
getPosition(): number {
return this.pos;
}
getInputLength(): number {
return this.input.length;
}
}
export function parseValue(
schema: Schema,
valueString: string,
schemaString?: string,
): unknown {
const sStr = schemaString || schemaToTypeString(schema);
const parser = new ValueParser(valueString.trim(), sStr);
const value = parser.parseValue(schema);
if (parser.getPosition() < parser.getInputLength()) {
throw new ParseError(
"Unexpected input after value",
parser.getPosition(),
sStr,
valueString.trim(),
);
}
return value;
}