feat: support named members in tuples
- Add NamedSchema interface with optional name property - Update TupleSchema.elements to use NamedSchema[] - Add parseNamedSchema() to support [x: number; y: number] syntax - Update validator to parse named members in tuple values - Fix schemaToTypeString in csv-loader for NamedSchema - Fix createValidator to handle NamedSchema.schema - Ensure single named element stays as tuple (not array) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
d056145462
commit
23ee60bc20
|
|
@ -40,12 +40,12 @@ function schemaToTypeString(schema: Schema): string {
|
|||
return 'boolean';
|
||||
case 'array':
|
||||
if (schema.element.type === 'tuple') {
|
||||
const tupleElements = schema.element.elements.map(schemaToTypeString);
|
||||
const tupleElements = schema.element.elements.map((el) => schemaToTypeString(el.schema));
|
||||
return `[${tupleElements.join(', ')}]`;
|
||||
}
|
||||
return `${schemaToTypeString(schema.element)}[]`;
|
||||
case 'tuple':
|
||||
const tupleElements = schema.elements.map(schemaToTypeString);
|
||||
const tupleElements = schema.elements.map((el) => schemaToTypeString(el.schema));
|
||||
return `[${tupleElements.join(', ')}]`;
|
||||
default:
|
||||
return 'unknown';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema } from './types';
|
||||
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, NamedSchema } from './types';
|
||||
|
||||
export class ParseError extends Error {
|
||||
constructor(message: string, public position?: number) {
|
||||
|
|
@ -51,7 +51,7 @@ class Parser {
|
|||
|
||||
parseSchema(): Schema {
|
||||
this.skipWhitespace();
|
||||
|
||||
|
||||
if (this.consumeStr('string')) {
|
||||
if (this.consumeStr('[')) {
|
||||
this.skipWhitespace();
|
||||
|
|
@ -62,7 +62,7 @@ class Parser {
|
|||
}
|
||||
return { type: 'string' };
|
||||
}
|
||||
|
||||
|
||||
if (this.consumeStr('number')) {
|
||||
if (this.consumeStr('[')) {
|
||||
this.skipWhitespace();
|
||||
|
|
@ -73,7 +73,7 @@ class Parser {
|
|||
}
|
||||
return { type: 'number' };
|
||||
}
|
||||
|
||||
|
||||
if (this.consumeStr('boolean')) {
|
||||
if (this.consumeStr('[')) {
|
||||
this.skipWhitespace();
|
||||
|
|
@ -86,50 +86,50 @@ class Parser {
|
|||
}
|
||||
|
||||
if (this.consumeStr('[')) {
|
||||
const elements: Schema[] = [];
|
||||
const elements: NamedSchema[] = [];
|
||||
this.skipWhitespace();
|
||||
|
||||
|
||||
if (this.peek() === ']') {
|
||||
this.consume();
|
||||
throw new ParseError('Empty array/tuple not allowed', this.pos);
|
||||
}
|
||||
|
||||
elements.push(this.parseSchema());
|
||||
|
||||
elements.push(this.parseNamedSchema());
|
||||
this.skipWhitespace();
|
||||
|
||||
|
||||
if (this.consumeStr(';')) {
|
||||
const remainingElements: Schema[] = [];
|
||||
const remainingElements: NamedSchema[] = [];
|
||||
while (true) {
|
||||
this.skipWhitespace();
|
||||
remainingElements.push(this.parseSchema());
|
||||
remainingElements.push(this.parseNamedSchema());
|
||||
this.skipWhitespace();
|
||||
|
||||
|
||||
if (!this.consumeStr(';')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
elements.push(...remainingElements);
|
||||
}
|
||||
|
||||
|
||||
this.skipWhitespace();
|
||||
|
||||
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
}
|
||||
|
||||
|
||||
if (this.consumeStr('[')) {
|
||||
this.skipWhitespace();
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
}
|
||||
if (elements.length === 1) {
|
||||
return { type: 'array', element: elements[0] };
|
||||
if (elements.length === 1 && !elements[0].name) {
|
||||
return { type: 'array', element: elements[0].schema };
|
||||
}
|
||||
return { type: 'array', element: { type: 'tuple', elements } };
|
||||
}
|
||||
|
||||
if (elements.length === 1) {
|
||||
return { type: 'array', element: elements[0] };
|
||||
|
||||
if (elements.length === 1 && !elements[0].name) {
|
||||
return { type: 'array', element: elements[0].schema };
|
||||
}
|
||||
return { type: 'tuple', elements };
|
||||
}
|
||||
|
|
@ -152,6 +152,34 @@ class Parser {
|
|||
|
||||
throw new ParseError(`Unexpected character: ${this.peek()}`, this.pos);
|
||||
}
|
||||
|
||||
private parseNamedSchema(): NamedSchema {
|
||||
this.skipWhitespace();
|
||||
|
||||
const startpos = this.pos;
|
||||
let identifier = '';
|
||||
|
||||
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
|
||||
identifier += this.consume();
|
||||
}
|
||||
|
||||
if (identifier.length === 0) {
|
||||
throw new ParseError('Expected schema or named schema', this.pos);
|
||||
}
|
||||
|
||||
this.skipWhitespace();
|
||||
|
||||
if (this.consumeStr(':')) {
|
||||
this.skipWhitespace();
|
||||
const name = identifier;
|
||||
const schema = this.parseSchema();
|
||||
return { name, schema };
|
||||
} else {
|
||||
this.pos = startpos;
|
||||
const schema = this.parseSchema();
|
||||
return { schema };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSchema(schemaString: string): Schema {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ const testCases = [
|
|||
{ schema: 'string', value: 'hello\\[world', description: 'Escaped bracket' },
|
||||
{ schema: 'string', value: 'hello\\\\world', description: 'Escaped backslash' },
|
||||
{ schema: '[string; string]', value: 'hello\\;world; test', description: 'Tuple with escaped semicolon' },
|
||||
{ schema: '[x: number; y: number]', value: '[x: 10; y: 20]', description: 'Named tuple' },
|
||||
{ schema: '[x: number; y: number]', value: 'x: 10; y: 20', description: 'Named tuple without brackets' },
|
||||
{ schema: '[name: string; age: number; active: boolean]', value: '[name: Alice; age: 30; active: true]', description: 'Named tuple with mixed types' },
|
||||
{ schema: '[name: string; age: number]', value: 'name: Bob; age: 25', description: 'Named tuple without brackets' },
|
||||
{ schema: '[point: [x: number; y: number]]', value: '[point: [x: 5; y: 10]]', description: 'Nested named tuple' },
|
||||
];
|
||||
|
||||
testCases.forEach(({ schema, value, description }) => {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,14 @@ export interface PrimitiveSchema {
|
|||
type: SchemaType;
|
||||
}
|
||||
|
||||
export interface NamedSchema {
|
||||
name?: string;
|
||||
schema: Schema;
|
||||
}
|
||||
|
||||
export interface TupleSchema {
|
||||
type: 'tuple';
|
||||
elements: Schema[];
|
||||
elements: NamedSchema[];
|
||||
}
|
||||
|
||||
export interface ArraySchema {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema } from './types';
|
||||
import type { Schema, PrimitiveSchema, TupleSchema, ArraySchema, NamedSchema } from './types';
|
||||
import { ParseError } from './parser';
|
||||
|
||||
class ValueParser {
|
||||
|
|
@ -96,7 +96,7 @@ class ValueParser {
|
|||
|
||||
private parseTupleValue(schema: TupleSchema, allowOmitBrackets: boolean): unknown[] {
|
||||
let hasOpenBracket = false;
|
||||
|
||||
|
||||
if (this.peek() === '[') {
|
||||
this.consume();
|
||||
hasOpenBracket = true;
|
||||
|
|
@ -105,7 +105,7 @@ class ValueParser {
|
|||
}
|
||||
|
||||
this.skipWhitespace();
|
||||
|
||||
|
||||
if (this.peek() === ']' && hasOpenBracket) {
|
||||
this.consume();
|
||||
return [];
|
||||
|
|
@ -114,7 +114,17 @@ class ValueParser {
|
|||
const result: unknown[] = [];
|
||||
for (let i = 0; i < schema.elements.length; i++) {
|
||||
this.skipWhitespace();
|
||||
result.push(this.parseValue(schema.elements[i], false));
|
||||
const elementSchema = schema.elements[i];
|
||||
|
||||
if (elementSchema.name) {
|
||||
this.skipWhitespace();
|
||||
if (!this.consumeStr(`${elementSchema.name}:`)) {
|
||||
throw new ParseError(`Expected ${elementSchema.name}:`, this.pos);
|
||||
}
|
||||
this.skipWhitespace();
|
||||
}
|
||||
|
||||
result.push(this.parseValue(elementSchema.schema, false));
|
||||
this.skipWhitespace();
|
||||
|
||||
if (i < schema.elements.length - 1) {
|
||||
|
|
@ -125,7 +135,7 @@ class ValueParser {
|
|||
}
|
||||
|
||||
this.skipWhitespace();
|
||||
|
||||
|
||||
if (hasOpenBracket) {
|
||||
if (!this.consumeStr(']')) {
|
||||
throw new ParseError('Expected ]', this.pos);
|
||||
|
|
@ -216,7 +226,7 @@ export function createValidator(schema: Schema): (value: unknown) => boolean {
|
|||
if (!Array.isArray(value)) return false;
|
||||
if (value.length !== schema.elements.length) return false;
|
||||
return schema.elements.every((elementSchema, index) =>
|
||||
createValidator(elementSchema)(value[index])
|
||||
createValidator(elementSchema.schema)(value[index])
|
||||
);
|
||||
case 'array':
|
||||
if (!Array.isArray(value)) return false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue