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.
|
||||
- **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.
|
||||
- **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]` |
|
||||
| Array | `Type[]` | `[1; 2; 3]` |
|
||||
| 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` |
|
||||
| Reference | `@tablename` or `@tablename[]` | (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 `,`
|
||||
- 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
|
||||
- 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: `\;`, `\[`, `\]`, `\\`
|
||||
- Empty arrays/tuples are not allowed
|
||||
- For CSV loading with reference resolution, see [csv-loader.md](./csv-loader.md)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -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.
|
||||
- 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 `module-gen.ts` `generateSchemaResolutionCode` union case, apply the same ordering so generated code matches runtime behavior.
|
||||
- **Document the rule** in README (union member ordering is currently "first match wins" per AGENTS.md).
|
||||
- **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`, 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`.
|
||||
- `module-gen.ts` `generateSchemaResolutionCode` already emitted reference-first (`lookup.get(...) ?? value`), so runtime now matches generated code.
|
||||
- **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,39 +299,27 @@ export function parseValueWithReferences(
|
||||
);
|
||||
}
|
||||
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[] = [];
|
||||
for (const member of schema.members) {
|
||||
if (hasNestedReferences(member)) {
|
||||
try {
|
||||
const parsed = parseValue(member, valueString);
|
||||
return resolveNestedReferences(
|
||||
parsed,
|
||||
member,
|
||||
refBaseDir,
|
||||
defaultPrimaryKey,
|
||||
currentFilePath,
|
||||
currentRowPk,
|
||||
);
|
||||
} catch (e) {
|
||||
errors.push(e instanceof Error ? e : new Error(String(e)));
|
||||
}
|
||||
for (const member of [...refMembers, ...nonRefMembers]) {
|
||||
try {
|
||||
const parsed = parseValue(member, valueString);
|
||||
return resolveNestedReferences(
|
||||
parsed,
|
||||
member,
|
||||
refBaseDir,
|
||||
defaultPrimaryKey,
|
||||
currentFilePath,
|
||||
currentRowPk,
|
||||
);
|
||||
} catch (e) {
|
||||
errors.push(e instanceof Error ? e : new Error(String(e)));
|
||||
}
|
||||
}
|
||||
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);
|
||||
throw errors[0] ?? new Error("Value does not match any union member");
|
||||
}
|
||||
default:
|
||||
return parseValue(schema, valueString);
|
||||
@@ -425,21 +413,22 @@ export function resolveNestedReferences(
|
||||
);
|
||||
}
|
||||
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[] = [];
|
||||
for (const member of schema.members) {
|
||||
if (hasNestedReferences(member)) {
|
||||
try {
|
||||
return resolveNestedReferences(
|
||||
value,
|
||||
member,
|
||||
refBaseDir,
|
||||
defaultPrimaryKey,
|
||||
currentFilePath,
|
||||
currentRowPk,
|
||||
);
|
||||
} catch (e) {
|
||||
errors.push(e instanceof Error ? e : new Error(String(e)));
|
||||
}
|
||||
for (const member of [...refMembers, ...nonRefMembers]) {
|
||||
try {
|
||||
return resolveNestedReferences(
|
||||
value,
|
||||
member,
|
||||
refBaseDir,
|
||||
defaultPrimaryKey,
|
||||
currentFilePath,
|
||||
currentRowPk,
|
||||
);
|
||||
} catch (e) {
|
||||
errors.push(e instanceof Error ? e : new Error(String(e)));
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user