fix: fix minor gotchas

This commit is contained in:
hypercross 2026-04-15 14:09:52 +08:00
parent ae2445b79c
commit 392d5f1431
6 changed files with 10 additions and 158 deletions

View File

@ -4,7 +4,7 @@
- **Build:** `npm run build` (tsup, CJS + ESM + d.ts for all entry points)
- **Test:** `npm run test` (vitest run) | `npm run test:watch` (vitest watch)
- **Type check:** `npx tsc --noEmit` (no dedicated script; run before committing)
- **Type check:** `npm run typecheck`
- **Run a single test:** `npx vitest run -t "test name pattern"`
No linter or formatter is configured. No CI pipeline exists.
@ -27,15 +27,14 @@ Build produces separate bundles per entry point (see `tsup.config.ts`). The csv-
## Key conventions
- Schema syntax uses **semicolons** (`;`) as separators, not commas
- Identifiers with hyphens (e.g., `word-smith`) are treated as string schemas
- Unknown identifiers throw a `ParseError` — only recognized keywords (`string`, `number`, `int`, `float`, `boolean`) and string literals (`"on"`, `'off'`) are valid types
- `@tablename` / `@tablename[]` are reference schemas resolved at CSV load time
- References can appear nested inside tuples, arrays, and unions; the loader resolves them recursively
- `src/test.ts` is an ad-hoc console script, not a vitest suite — don't treat it as a real test
## Gotchas
- **Circular references** between CSV tables cause stack overflow. The loader detects this via an in-progress loading set and throws `"Circular reference detected"`.
- **No `lint` or `typecheck` npm script** — run `tsc --noEmit` manually before committing changes.
- **Run `npm run typecheck` before committing** to catch type errors.
- **Union member ordering matters**`parseValue` tries union members in order; the first one that parses wins. This affects references in unions (e.g., `@users[] | string` will try `@users[]` first).
- **csv-parse quote handling** — Double-quoted schema values like `"active" | "inactive"` in CSV rows confuse the csv-parse library. Use unquoted identifiers in the schema row of CSV data when possible.
- **csv-parse quote handling** — Double-quoted schema values like `"active" | "inactive"` in CSV rows confuse the csv-parse library. Use single-quoted string literals (`'on' | 'off'`) or unquoted identifiers in the schema row of CSV data when possible.
- **Module imports use `.js` extension** — source files import from `../index.js` etc. (ESM convention), not `../index.ts`.

View File

@ -35,6 +35,7 @@
"build": "tsup",
"dev": "tsup --watch",
"test": "vitest run",
"typecheck": "tsc --noEmit",
"test:watch": "vitest"
},
"keywords": [

View File

@ -43,7 +43,7 @@ describe('parseCsv - basic parsing', () => {
it('should parse CSV with string literal columns (unquoted in CSV)', () => {
const csv = [
'name,status',
'string,on | off',
"string,'on' | 'off'",
'Alice,on',
'Bob,off',
].join('\n');

View File

@ -20,10 +20,9 @@ describe('Primitive types', () => {
expect(schema.validator(42)).toBe(false);
});
it('should handle identifiers with hyphens', () => {
const schema = defineSchema('word-smith');
expect(schema.parse('word-smith')).toBe('word-smith');
expect(schema.validator('word-smith')).toBe(true);
it('should reject unknown identifiers', () => {
expect(() => defineSchema('word-smith')).toThrow(ParseError);
expect(() => defineSchema('strign')).toThrow(ParseError);
});
});

View File

@ -237,23 +237,7 @@ class Parser {
return { type: 'tuple', elements };
}
let identifier = '';
while (this.pos < this.input.length && /[a-zA-Z0-9\-_]/.test(this.peek())) {
identifier += this.consume();
}
if (identifier.length > 0) {
if (this.consumeStr('[')) {
this.skipWhitespace();
if (!this.consumeStr(']')) {
throw new ParseError('Expected ]', this.pos);
}
return { type: 'array', element: { type: 'string' } };
}
return { type: 'string' };
}
throw new ParseError(`Unexpected character: ${this.peek()}`, this.pos);
throw new ParseError(`Unknown type: ${this.peek() || 'end of input'}`, this.pos);
}
private parseStringLiteralSchema(): Schema {

View File

@ -1,131 +0,0 @@
import { defineSchema, parseSchema, parseValue, createValidator } from './index';
console.log('=== Testing Schema Parser ===\n');
const testCases = [
{ schema: 'string', value: 'hello', description: 'Simple string' },
{ schema: 'number', value: '42', description: 'Simple number' },
{ schema: 'int', value: '42', description: 'Simple int' },
{ schema: 'float', value: '3.14', description: 'Simple float' },
{ schema: 'float', value: '42', description: 'Float with integer value' },
{ schema: 'boolean', value: 'true', description: 'Simple boolean' },
{ schema: '[string; number]', value: '[hello; 42]', description: 'Tuple' },
{ schema: '[string; number]', value: 'hello; 42', description: 'Tuple without brackets' },
{ schema: 'string[]', value: '[hello; world; test]', description: 'Array of strings' },
{ schema: 'string[]', value: 'hello; world; test', description: 'Array without brackets' },
{ schema: 'number[]', value: '[1; 2; 3; 4]', description: 'Array of numbers' },
{ schema: 'int[]', value: '[1; 2; 3; 4]', description: 'Array of ints' },
{ schema: 'float[]', value: '[1.5; 2.5; 3.5]', description: 'Array of floats' },
{ schema: '[string; number][]', value: '[[a; 1]; [b; 2]; [c; 3]]', description: 'Array of tuples' },
{ schema: '[string; number][]', value: '[a; 1]; [b; 2]; [c; 3]', description: 'Array of tuples without outer brackets' },
{ schema: 'word-smith', value: 'word-smith', description: 'String with hyphen' },
{ schema: 'string', value: 'hello\\;world', description: 'Escaped semicolon' },
{ 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' },
];
console.log('=== Testing String Literals ===\n');
const stringLiteralCases = [
{ schema: '"hello"', value: '"hello"', description: 'Simple string literal' },
{ schema: "'world'", value: "'world'", description: 'Single quoted string literal' },
{ schema: '"on"', value: '"on"', description: 'String literal "on"' },
{ schema: '"off"', value: '"off"', description: 'String literal "off"' },
{ schema: '"hello;world"', value: '"hello;world"', description: 'String literal with semicolon' },
{ schema: '"hello\\"world"', value: '"hello\\"world"', description: 'String literal with escaped quote' },
];
testCases.push(...stringLiteralCases);
console.log('=== Testing Union Types (Enums) ===\n');
const unionCases = [
{ schema: '"on" | "off"', value: '"on"', description: 'Union: on' },
{ schema: '"on" | "off"', value: '"off"', description: 'Union: off' },
{ schema: '"pending" | "approved" | "rejected"', value: '"approved"', description: 'Union: approved' },
{ schema: '( "active" | "inactive" )', value: '"active"', description: 'Union with parentheses' },
{ schema: '[name: string; status: "active" | "inactive"]', value: '[myName; "active"]', description: 'Tuple with union field' },
{ schema: '("pending" | "approved" | "rejected")[]', value: '["pending"; "approved"; "rejected"]', description: 'Array of unions' },
{ schema: 'string | number', value: 'hello', description: 'Union: string' },
{ schema: 'string | number', value: '42', description: 'Union: number' },
{ schema: 'string | "special"', value: 'normal', description: 'Union: string type' },
{ schema: 'string | "special"', value: '"special"', description: 'Union: string literal' },
];
testCases.push(...unionCases);
testCases.forEach(({ schema, value, description }) => {
try {
console.log(`Test: ${description}`);
console.log(` Schema: ${schema}`);
console.log(` Value: "${value}"`);
const parsed = defineSchema(schema);
const parsedValue = parsed.parse(value);
const isValid = parsed.validator(parsedValue);
console.log(` Parsed: ${JSON.stringify(parsedValue)}`);
console.log(` Valid: ${isValid}`);
console.log(' ✓ Passed\n');
} catch (error) {
console.log(` ✗ Failed: ${(error as Error).message}\n`);
}
});
console.log('=== Testing Validation ===\n');
const stringSchema = defineSchema('string');
console.log('String schema validation:');
console.log(` "hello" is valid: ${stringSchema.validator('hello')}`);
console.log(` 42 is valid: ${stringSchema.validator(42)}\n`);
const numberSchema = defineSchema('number');
console.log('Number schema validation:');
console.log(` 42 is valid: ${numberSchema.validator(42)}`);
console.log(` "42" is valid: ${numberSchema.validator('42')}\n`);
const tupleSchema = defineSchema('[string; number; boolean]');
console.log('Tuple [string; number; boolean] validation:');
console.log(` ["hello", 42, true] is valid: ${tupleSchema.validator(['hello', 42, true])}`);
console.log(` ["hello", "42", true] is valid: ${tupleSchema.validator(['hello', '42', true])}\n`);
const arraySchema = defineSchema('number[]');
console.log('Array number[] validation:');
console.log(` [1, 2, 3] is valid: ${arraySchema.validator([1, 2, 3])}`);
console.log(` [1, "2", 3] is valid: ${arraySchema.validator([1, '2', 3])}\n`);
const arrayOfTuplesSchema = defineSchema('[string; number][]');
console.log('Array of tuples [string; number][] validation:');
console.log(` [["a", 1], ["b", 2]] is valid: ${arrayOfTuplesSchema.validator([['a', 1], ['b', 2]])}`);
console.log(` [["a", "1"], ["b", 2]] is valid: ${arrayOfTuplesSchema.validator([['a', '1'], ['b', 2]])}\n`);
console.log('=== Testing Int and Float Types ===\n');
const intSchema = defineSchema('int');
console.log('Int schema validation:');
console.log(` 42 is valid: ${intSchema.validator(42)}`);
console.log(` 3.14 is valid: ${intSchema.validator(3.14)}\n`);
const floatSchema = defineSchema('float');
console.log('Float schema validation:');
console.log(` 3.14 is valid: ${floatSchema.validator(3.14)}`);
console.log(` 42 is valid: ${floatSchema.validator(42)}`);
console.log(` "3.14" is valid: ${floatSchema.validator('3.14')}\n`);
const intArraySchema = defineSchema('int[]');
console.log('Int array validation:');
console.log(` [1, 2, 3] is valid: ${intArraySchema.validator([1, 2, 3])}`);
console.log(` [1, 2.5, 3] is valid: ${intArraySchema.validator([1, 2.5, 3])}\n`);
const floatArraySchema = defineSchema('float[]');
console.log('Float array validation:');
console.log(` [1.5, 2.5, 3.5] is valid: ${floatArraySchema.validator([1.5, 2.5, 3.5])}`);
console.log(` [1, 2, 3] is valid: ${floatArraySchema.validator([1, 2, 3])}\n`);
console.log('=== All tests completed ===');