diff --git a/AGENTS.md b/AGENTS.md index e50c568..3f41640 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`. diff --git a/package.json b/package.json index 5a81146..7191f66 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "build": "tsup", "dev": "tsup --watch", "test": "vitest run", + "typecheck": "tsc --noEmit", "test:watch": "vitest" }, "keywords": [ diff --git a/src/csv-loader/loader.test.ts b/src/csv-loader/loader.test.ts index 2e9c758..c94c147 100644 --- a/src/csv-loader/loader.test.ts +++ b/src/csv-loader/loader.test.ts @@ -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'); diff --git a/src/index.test.ts b/src/index.test.ts index 0acba8c..a16c651 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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); }); }); diff --git a/src/parser.ts b/src/parser.ts index aff7b3d..6519a2e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -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 { diff --git a/src/test.ts b/src/test.ts deleted file mode 100644 index 6894951..0000000 --- a/src/test.ts +++ /dev/null @@ -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 ===');