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';
|
return 'boolean';
|
||||||
case 'array':
|
case 'array':
|
||||||
if (schema.element.type === 'tuple') {
|
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 `[${tupleElements.join(', ')}]`;
|
||||||
}
|
}
|
||||||
return `${schemaToTypeString(schema.element)}[]`;
|
return `${schemaToTypeString(schema.element)}[]`;
|
||||||
case 'tuple':
|
case 'tuple':
|
||||||
const tupleElements = schema.elements.map(schemaToTypeString);
|
const tupleElements = schema.elements.map((el) => schemaToTypeString(el.schema));
|
||||||
return `[${tupleElements.join(', ')}]`;
|
return `[${tupleElements.join(', ')}]`;
|
||||||
default:
|
default:
|
||||||
return 'unknown';
|
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 {
|
export class ParseError extends Error {
|
||||||
constructor(message: string, public position?: number) {
|
constructor(message: string, public position?: number) {
|
||||||
|
|
@ -51,7 +51,7 @@ class Parser {
|
||||||
|
|
||||||
parseSchema(): Schema {
|
parseSchema(): Schema {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (this.consumeStr('string')) {
|
if (this.consumeStr('string')) {
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr('[')) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
@ -62,7 +62,7 @@ class Parser {
|
||||||
}
|
}
|
||||||
return { type: 'string' };
|
return { type: 'string' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.consumeStr('number')) {
|
if (this.consumeStr('number')) {
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr('[')) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
@ -73,7 +73,7 @@ class Parser {
|
||||||
}
|
}
|
||||||
return { type: 'number' };
|
return { type: 'number' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.consumeStr('boolean')) {
|
if (this.consumeStr('boolean')) {
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr('[')) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
@ -86,50 +86,50 @@ class Parser {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.consumeStr('[')) {
|
if (this.consumeStr('[')) {
|
||||||
const elements: Schema[] = [];
|
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.parseSchema());
|
elements.push(this.parseNamedSchema());
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (this.consumeStr(';')) {
|
if (this.consumeStr(';')) {
|
||||||
const remainingElements: Schema[] = [];
|
const remainingElements: NamedSchema[] = [];
|
||||||
while (true) {
|
while (true) {
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
remainingElements.push(this.parseSchema());
|
remainingElements.push(this.parseNamedSchema());
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (!this.consumeStr(';')) {
|
if (!this.consumeStr(';')) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elements.push(...remainingElements);
|
elements.push(...remainingElements);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (elements.length === 1 && !elements[0].name) {
|
||||||
return { type: 'array', element: elements[0] };
|
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) {
|
if (elements.length === 1 && !elements[0].name) {
|
||||||
return { type: 'array', element: elements[0] };
|
return { type: 'array', element: elements[0].schema };
|
||||||
}
|
}
|
||||||
return { type: 'tuple', elements };
|
return { type: 'tuple', elements };
|
||||||
}
|
}
|
||||||
|
|
@ -152,6 +152,34 @@ class Parser {
|
||||||
|
|
||||||
throw new ParseError(`Unexpected character: ${this.peek()}`, this.pos);
|
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 {
|
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 bracket' },
|
||||||
{ schema: 'string', value: 'hello\\\\world', description: 'Escaped backslash' },
|
{ schema: 'string', value: 'hello\\\\world', description: 'Escaped backslash' },
|
||||||
{ schema: '[string; string]', value: 'hello\\;world; test', description: 'Tuple with escaped semicolon' },
|
{ 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 }) => {
|
testCases.forEach(({ schema, value, description }) => {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,14 @@ export interface PrimitiveSchema {
|
||||||
type: SchemaType;
|
type: SchemaType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NamedSchema {
|
||||||
|
name?: string;
|
||||||
|
schema: Schema;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TupleSchema {
|
export interface TupleSchema {
|
||||||
type: 'tuple';
|
type: 'tuple';
|
||||||
elements: Schema[];
|
elements: NamedSchema[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArraySchema {
|
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';
|
import { ParseError } from './parser';
|
||||||
|
|
||||||
class ValueParser {
|
class ValueParser {
|
||||||
|
|
@ -96,7 +96,7 @@ class ValueParser {
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -105,7 +105,7 @@ class ValueParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.skipWhitespace();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (this.peek() === ']' && hasOpenBracket) {
|
if (this.peek() === ']' && hasOpenBracket) {
|
||||||
this.consume();
|
this.consume();
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -114,7 +114,17 @@ class ValueParser {
|
||||||
const result: unknown[] = [];
|
const result: unknown[] = [];
|
||||||
for (let i = 0; i < schema.elements.length; i++) {
|
for (let i = 0; i < schema.elements.length; i++) {
|
||||||
this.skipWhitespace();
|
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();
|
this.skipWhitespace();
|
||||||
|
|
||||||
if (i < schema.elements.length - 1) {
|
if (i < schema.elements.length - 1) {
|
||||||
|
|
@ -125,7 +135,7 @@ 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);
|
||||||
|
|
@ -216,7 +226,7 @@ export function createValidator(schema: Schema): (value: unknown) => boolean {
|
||||||
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)(value[index])
|
createValidator(elementSchema.schema)(value[index])
|
||||||
);
|
);
|
||||||
case 'array':
|
case 'array':
|
||||||
if (!Array.isArray(value)) return false;
|
if (!Array.isArray(value)) return false;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue