Files
ehoandClaude Opus 4.8 deb5d77e04
CI / frontend (push) Successful in 3m11s
CI / backend (push) Successful in 2m27s
CI / storybook-a11y (push) Successful in 9m28s
CI / e2e (push) Successful in 4m38s
CI / semgrep (push) Successful in 1m20s
CI / api-client-drift (push) Successful in 2m13s
feat(dx): WP-43 — plop generators (value-object, form-machine)
Runnable `npm run gen:value-object` / `gen:form-machine` (plop) that scaffold the two
pure-TS house patterns with a co-located spec: a branded value object + parseX (mirrors
postcode/bsn), and an Elm-style form/wizard machine (Draft/Valid/Errors + Editing/
Submitting/Submitted/Failed union + initial/pure reduce/assertNever). Prompts take
context + PascalCase name (positional-arg bypass); a post-action reminds to add the
English target for the generated $localize id. Templates in plop-templates/ (prettier-
ignored). Skills (value-object, form-machine) point at the generators. ui-component +
bff-endpoint stay skill-driven (Angular {{}} / backend + gen:api).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 18:04:54 +02:00

61 lines
2.2 KiB
Markdown

---
name: value-object
description: Add a validated input type (postcode, email, hours, id number, …) as a branded value object with a parser — "parse, don't validate". Use whenever a form field or API value has format rules.
---
# Value object (parse, don't validate)
Raw input becomes a branded type only via a parser returning `Result`. Once you hold
the type, never re-check it. Never model validity as a boolean flag next to a string.
**Scaffold it:** `npm run gen:value-object` (WP-43) emits the branded type + `parseX` + spec
into `<context>/domain/value-objects/`. Then fill in the real format rule and add the English
`<target>` for the generated `@@validation.<name>` id (the localize gate).
## Skeleton
`<context>/domain/value-objects/<name>.ts` (pure TS, no Angular):
```ts
import { Brand, Result, ok, err } from '@shared/kernel/fp';
export type Postcode = Brand<string, 'Postcode'>;
export function parsePostcode(raw: string): Result<string, Postcode> {
const t = raw.trim().toUpperCase();
if (!/^[1-9]\d{3}\s?[A-Z]{2}$/.test(t)) {
return err($localize`:@@validation.postcode:Voer een geldige postcode in, bijv. 1234 AB.`);
}
// The parser also normalises — callers always hold the canonical form.
return ok(t.replace(/^(\d{4})\s?([A-Z]{2})$/, '$1 $2') as Postcode);
}
```
Rules:
- The `as Brand` cast appears **only** inside the parser — the type is mintable nowhere else.
- Error message is user-facing → `$localize` with a stable `@@validation.<name>` id.
- Trim/normalise before testing; return the cleaned value.
- This is **format** feedback only. The server re-validates as authority (ADR-0001) — never encode business rules (existence, eligibility) here.
## Spec (required)
Co-located `<name>.spec.ts`: happy path, normalisation, each rejection case. Call the
parser directly — no TestBed.
## Wiring into a form
The machine's `validate(draft)` calls the parsers and collects errors into
`StepErrors`; the `Valid` type holds the branded values (see **form-machine** skill).
## Worked examples
`src/app/registratie/domain/value-objects/` — `postcode.ts`, `email.ts`, `uren.ts`,
`big-nummer.ts`, each with a co-located spec.
## Verify
```bash
npm test && npm run lint
```