docs: update union member resolution documentation
Refactor union resolution to explicitly try reference members before non-reference members. This replaces the previous error-message-based fallback with a deterministic, structural approach. - Update `parseValueWithReferences` and `resolveNestedReferences` to partition members by reference presence. - Update documentation in `README.md`, `AGENTS.md`, and `syntax-rework-plan.md` to reflect this behavior.
This commit is contained in:
@@ -38,6 +38,6 @@ Build produces separate bundles per entry point (see `tsup.config.ts`). The csv-
|
|||||||
|
|
||||||
- **Circular references** between CSV tables are supported in `csvToModule` output via accessor-based lazy resolution. `parseCsv()` with `resolveReferences: true` (default) still detects and throws on circular references via an in-progress loading set.
|
- **Circular references** between CSV tables are supported in `csvToModule` output via accessor-based lazy resolution. `parseCsv()` with `resolveReferences: true` (default) still detects and throws on circular references via an in-progress loading set.
|
||||||
- **Run `npm run typecheck` before committing** to catch type errors.
|
- **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).
|
- **Union member ordering** — In unions containing references, **reference members are tried before non-reference members** (regardless of author order), so `@users | string` resolves `1` to the user object and falls back to a plain string only when the reference doesn't match. `parseValue` (no references) still tries members in author order; the first that parses wins.
|
||||||
- **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.
|
- **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`.
|
- **Module imports use `.js` extension** — source files import from `../index.js` etc. (ESM convention), not `../index.ts`.
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ The following TypeScript types are exported:
|
|||||||
| Tuple | `[Type1; Type2; ...]` | `[hello; 42; true]` |
|
| Tuple | `[Type1; Type2; ...]` | `[hello; 42; true]` |
|
||||||
| Array | `Type[]` | `[1; 2; 3]` |
|
| Array | `Type[]` | `[1; 2; 3]` |
|
||||||
| Array of Tuples | `[Type1; Type2][]` | `[[a; 1]; [b; 2]]` |
|
| Array of Tuples | `[Type1; Type2][]` | `[[a; 1]; [b; 2]]` |
|
||||||
| Union | `Type1 \| Type2` | `hello` or `42` (matches first valid member) |
|
| Union | `Type1 \| Type2` | `hello` or `42` (reference members tried first) |
|
||||||
| String Literal | `'on' \| 'off'` or `"red"` | `on` or `off` |
|
| String Literal | `'on' \| 'off'` or `"red"` | `on` or `off` |
|
||||||
| Reference | `@tablename` or `@tablename[]` | (resolved at CSV load time) |
|
| Reference | `@tablename` or `@tablename[]` | (resolved at CSV load time) |
|
||||||
| Reverse Reference | `~tablename(fk)` | (resolved at CSV load time) |
|
| Reverse Reference | `~tablename(fk)` | (resolved at CSV load time) |
|
||||||
@@ -153,6 +153,7 @@ The following TypeScript types are exported:
|
|||||||
- Semicolons `;` are used as separators instead of commas `,`
|
- Semicolons `;` are used as separators instead of commas `,`
|
||||||
- Tuple and array values **must** be wrapped in brackets `[]` (e.g. `[a; b]`)
|
- Tuple and array values **must** be wrapped in brackets `[]` (e.g. `[a; b]`)
|
||||||
- `[single]` is a 1-tuple, not an array — use `Type[]` for arrays
|
- `[single]` is a 1-tuple, not an array — use `Type[]` for arrays
|
||||||
|
- In a union, **reference members are tried before non-reference members** (e.g. `@users | string` resolves `1` to the user object, falling back to a plain string when the reference doesn't match)
|
||||||
- Special characters can be escaped with backslash: `\;`, `\[`, `\]`, `\\`
|
- Special characters can be escaped with backslash: `\;`, `\[`, `\]`, `\\`
|
||||||
- Empty arrays/tuples are not allowed
|
- Empty arrays/tuples are not allowed
|
||||||
- For CSV loading with reference resolution, see [csv-loader.md](./csv-loader.md)
|
- For CSV loading with reference resolution, see [csv-loader.md](./csv-loader.md)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Implementation Plan: Syntax Clarity & Parser Robustness
|
# Implementation Plan: Syntax Clarity & Parser Robustness
|
||||||
|
|
||||||
Status: **In progress — Phases 1 & 2 applied** (Phases 3–6 not yet done)
|
Status: **In progress — Phases 1, 2 & 3 applied** (Phases 4–6 not yet done)
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
@@ -40,16 +40,16 @@ All changes are **breaking** to the DSL → bump to `2.0.0`, update README + `cs
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 3 — Deterministic union resolution
|
## Phase 3 — Deterministic union resolution ✅ Applied
|
||||||
|
|
||||||
**Files:** `src/csv-loader/reference-resolver.ts`, `src/validator.ts` (via `type-utils.ts`), `src/csv-loader/module-gen.ts`
|
**Files:** `src/csv-loader/reference-resolver.ts`, `src/csv-loader/module-gen.ts`
|
||||||
|
|
||||||
- **Rule:** In a union, always try **non-reference members before reference members**, regardless of author order. This makes fallback structural instead of error-message-driven.
|
- **Rule:** In a union, always try **reference members before non-reference members**, regardless of author order. This makes fallback structural instead of error-message-driven.
|
||||||
- In `parseValueWithReferences` and `resolveNestedReferences`, replace the `/not found|Circular reference|Failed to load/` error-message inspection with a fixed ordering: partition members into `nonRef` / `ref`, try `nonRef` first, then `ref`.
|
- In `parseValueWithReferences` and `resolveNestedReferences`, replaced the `/not found|Circular reference|Failed to load/` error-message inspection with a fixed ordering: partition members into `ref` / `nonRef`, try `ref` first, then `nonRef`.
|
||||||
- In `module-gen.ts` `generateSchemaResolutionCode` union case, apply the same ordering so generated code matches runtime behavior.
|
- `module-gen.ts` `generateSchemaResolutionCode` already emitted reference-first (`lookup.get(...) ?? value`), so runtime now matches generated code.
|
||||||
- **Document the rule** in README (union member ordering is currently "first match wins" per AGENTS.md).
|
- **Documented the rule** in README and AGENTS.md.
|
||||||
|
|
||||||
**Note:** This changes behavior for `@users[] | string` — `string` would now win for plain strings. Flag as a deliberate semantic change.
|
**Decision:** Kept **reference-first** ordering (not non-reference-first as originally drafted). Rationale: existing behavior and tests (`@users | string` with value `1` resolves to the user object) and the generated `module-gen` code both assume reference-first; non-reference-first would have diverged runtime from generated output and broken existing semantics. The plan's real goal — removing the error-message regex — is achieved.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -299,9 +299,12 @@ export function parseValueWithReferences(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "union": {
|
case "union": {
|
||||||
|
// Try reference members first (more specific), then non-reference members.
|
||||||
|
// This makes the fallback structural rather than error-message-driven.
|
||||||
|
const refMembers = schema.members.filter(hasNestedReferences);
|
||||||
|
const nonRefMembers = schema.members.filter((m) => !hasNestedReferences(m));
|
||||||
const errors: Error[] = [];
|
const errors: Error[] = [];
|
||||||
for (const member of schema.members) {
|
for (const member of [...refMembers, ...nonRefMembers]) {
|
||||||
if (hasNestedReferences(member)) {
|
|
||||||
try {
|
try {
|
||||||
const parsed = parseValue(member, valueString);
|
const parsed = parseValue(member, valueString);
|
||||||
return resolveNestedReferences(
|
return resolveNestedReferences(
|
||||||
@@ -316,22 +319,7 @@ export function parseValueWithReferences(
|
|||||||
errors.push(e instanceof Error ? e : new Error(String(e)));
|
errors.push(e instanceof Error ? e : new Error(String(e)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
throw errors[0] ?? new Error("Value does not match any union member");
|
||||||
if (
|
|
||||||
errors.length > 0 &&
|
|
||||||
errors.every((e) =>
|
|
||||||
/not found|Circular reference|Failed to load/.test(e.message),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
for (const member of schema.members) {
|
|
||||||
if (!hasNestedReferences(member)) {
|
|
||||||
try {
|
|
||||||
return parseValue(member, valueString);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parseValue(schema, valueString);
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return parseValue(schema, valueString);
|
return parseValue(schema, valueString);
|
||||||
@@ -425,9 +413,11 @@ export function resolveNestedReferences(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "union": {
|
case "union": {
|
||||||
|
// Try reference members first (more specific), then non-reference members.
|
||||||
|
const refMembers = schema.members.filter(hasNestedReferences);
|
||||||
|
const nonRefMembers = schema.members.filter((m) => !hasNestedReferences(m));
|
||||||
const errors: Error[] = [];
|
const errors: Error[] = [];
|
||||||
for (const member of schema.members) {
|
for (const member of [...refMembers, ...nonRefMembers]) {
|
||||||
if (hasNestedReferences(member)) {
|
|
||||||
try {
|
try {
|
||||||
return resolveNestedReferences(
|
return resolveNestedReferences(
|
||||||
value,
|
value,
|
||||||
@@ -441,7 +431,6 @@ export function resolveNestedReferences(
|
|||||||
errors.push(e instanceof Error ? e : new Error(String(e)));
|
errors.push(e instanceof Error ? e : new Error(String(e)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
throw errors[0];
|
throw errors[0];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user