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:
hyper 2026-03-31 16:36:32 +08:00
parent d056145462
commit 23ee60bc20
5 changed files with 77 additions and 29 deletions

View File

@ -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';

View File

@ -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) {
@ -86,7 +86,7 @@ class Parser {
}
if (this.consumeStr('[')) {
const elements: Schema[] = [];
const elements: NamedSchema[] = [];
this.skipWhitespace();
if (this.peek() === ']') {
@ -94,14 +94,14 @@ class Parser {
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(';')) {
@ -122,14 +122,14 @@ class Parser {
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 {

View File

@ -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 }) => {

View File

@ -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 {

View File

@ -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 {
@ -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) {
@ -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;