feat: enrich ParseError with schema and value context
Include the schema string and the input value in `ParseError` to provide more helpful debugging information when parsing fails.
This commit is contained in:
parent
b5be558b57
commit
1061e12c81
|
|
@ -14,10 +14,20 @@ export class ParseError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
public position?: number,
|
public position?: number,
|
||||||
|
public schema?: string,
|
||||||
|
public value?: string,
|
||||||
) {
|
) {
|
||||||
super(
|
let fullMessage = message;
|
||||||
position !== undefined ? `${message} at position ${position}` : message,
|
if (position !== undefined) {
|
||||||
);
|
fullMessage += ` at position ${position}`;
|
||||||
|
}
|
||||||
|
if (schema !== undefined) {
|
||||||
|
fullMessage += `. Schema: ${schema}`;
|
||||||
|
}
|
||||||
|
if (value !== undefined) {
|
||||||
|
fullMessage += `. Value: ${value}`;
|
||||||
|
}
|
||||||
|
super(fullMessage);
|
||||||
this.name = "ParseError";
|
this.name = "ParseError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,16 @@ import type {
|
||||||
UnionSchema,
|
UnionSchema,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ParseError } from "./parser";
|
import { ParseError } from "./parser";
|
||||||
|
import { schemaToTypeString } from "./type-utils";
|
||||||
|
|
||||||
class ValueParser {
|
class ValueParser {
|
||||||
private input: string;
|
private input: string;
|
||||||
|
private schemaString: string;
|
||||||
private pos: number = 0;
|
private pos: number = 0;
|
||||||
|
|
||||||
constructor(input: string) {
|
constructor(input: string, schemaString: string) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
|
this.schemaString = schemaString;
|
||||||
}
|
}
|
||||||
|
|
||||||
private peek(): string {
|
private peek(): string {
|
||||||
|
|
@ -71,6 +74,8 @@ class ValueParser {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
`Unknown schema type: ${(schema as { type: string }).type}`,
|
`Unknown schema type: ${(schema as { type: string }).type}`,
|
||||||
this.pos,
|
this.pos,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +114,12 @@ class ValueParser {
|
||||||
}
|
}
|
||||||
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,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
|
|
@ -121,10 +131,20 @@ class ValueParser {
|
||||||
}
|
}
|
||||||
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,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
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,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +160,12 @@ class ValueParser {
|
||||||
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,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseStringLiteralValue(schema: StringLiteralSchema): string {
|
private parseStringLiteralValue(schema: StringLiteralSchema): string {
|
||||||
|
|
@ -174,6 +199,8 @@ class ValueParser {
|
||||||
throw new ParseError(
|
throw new ParseError(
|
||||||
`Invalid value '"${value}"'. Expected '"${schema.value}"'`,
|
`Invalid value '"${value}"'. Expected '"${schema.value}"'`,
|
||||||
this.pos,
|
this.pos,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,7 +210,12 @@ class ValueParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ParseError("Unterminated string literal", this.pos);
|
throw new ParseError(
|
||||||
|
"Unterminated string literal",
|
||||||
|
this.pos,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// 不带引号的字符串,像普通字符串一样解析
|
// 不带引号的字符串,像普通字符串一样解析
|
||||||
let value = "";
|
let value = "";
|
||||||
|
|
@ -201,6 +233,8 @@ class ValueParser {
|
||||||
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,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,6 +261,8 @@ class ValueParser {
|
||||||
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,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,7 +276,12 @@ class ValueParser {
|
||||||
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.schemaString,
|
||||||
|
this.input,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
@ -272,7 +313,12 @@ class ValueParser {
|
||||||
|
|
||||||
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,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +327,12 @@ class ValueParser {
|
||||||
|
|
||||||
if (hasOpenBracket) {
|
if (hasOpenBracket) {
|
||||||
if (!this.consumeStr("]")) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError("Expected ]", this.pos);
|
throw new ParseError(
|
||||||
|
"Expected ]",
|
||||||
|
this.pos,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -325,7 +376,12 @@ class ValueParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasOpenBracket && !allowOmitBrackets && !elementIsTupleOrArray) {
|
if (!hasOpenBracket && !allowOmitBrackets && !elementIsTupleOrArray) {
|
||||||
throw new ParseError("Expected [", this.pos);
|
throw new ParseError(
|
||||||
|
"Expected [",
|
||||||
|
this.pos,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
@ -351,7 +407,12 @@ class ValueParser {
|
||||||
|
|
||||||
if (hasOpenBracket) {
|
if (hasOpenBracket) {
|
||||||
if (!this.consumeStr("]")) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError("Expected ]", this.pos);
|
throw new ParseError(
|
||||||
|
"Expected ]",
|
||||||
|
this.pos,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -405,7 +466,12 @@ class ValueParser {
|
||||||
|
|
||||||
if (hasOpenBracket) {
|
if (hasOpenBracket) {
|
||||||
if (!this.consumeStr("]")) {
|
if (!this.consumeStr("]")) {
|
||||||
throw new ParseError("Expected ]", this.pos);
|
throw new ParseError(
|
||||||
|
"Expected ]",
|
||||||
|
this.pos,
|
||||||
|
this.schemaString,
|
||||||
|
this.input,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -433,13 +499,23 @@ class ValueParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseValue(schema: Schema, valueString: string): unknown {
|
export function parseValue(
|
||||||
const parser = new ValueParser(valueString.trim());
|
schema: Schema,
|
||||||
|
valueString: string,
|
||||||
|
schemaString?: string,
|
||||||
|
): unknown {
|
||||||
|
const sStr = schemaString || schemaToTypeString(schema);
|
||||||
|
const parser = new ValueParser(valueString.trim(), sStr);
|
||||||
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(),
|
||||||
|
sStr,
|
||||||
|
valueString.trim(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue