|
1 | 1 | // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. |
2 | 2 |
|
| 3 | +import type { z } from 'zod'; |
| 4 | + |
3 | 5 | /** |
4 | 6 | * # Conditional-visibility predicate normalization (ADR-0089) |
5 | 7 | * |
@@ -64,3 +66,46 @@ export function normalizeVisibleWhen<T extends WithVisibilityAliases>( |
64 | 66 | } |
65 | 67 | return { ...rest, visibleWhen: canonical } as Omit<T, 'visibleOn' | 'visibility'>; |
66 | 68 | } |
| 69 | + |
| 70 | +/** A key that is (or is a likely mis-spelling of) the visibility predicate. */ |
| 71 | +function looksLikeVisibilityKey(key: string): boolean { |
| 72 | + return /vis|conceal|hidden|show.?when/i.test(key); |
| 73 | +} |
| 74 | + |
| 75 | +/** |
| 76 | + * Custom zod `error` for the `.strict()` view/page schemas (ADR-0089 D3a). |
| 77 | + * |
| 78 | + * With `.strict()`, a key these schemas do not declare — a stale `visibleOn` past |
| 79 | + * removal, a `visibleWhen` typo, or a wrong-layer paste — is now a **loud parse |
| 80 | + * error** instead of a silent strip (ADR-0049 enforce-or-remove, ADR-0078 |
| 81 | + * no-silently-inert). This error map turns that rejection into a *fixable* one: it |
| 82 | + * always names the offending key(s), and when a key looks like the |
| 83 | + * conditional-visibility predicate it points the author at the canonical |
| 84 | + * `visibleWhen`. Every other issue code defers to zod's default (`undefined`). |
| 85 | + * |
| 86 | + * Wire it as the object's `error` alongside `.strict()`: |
| 87 | + * |
| 88 | + * ```ts |
| 89 | + * z.object({ ..., visibleWhen: Expr.optional() }, { error: strictVisibilityError }) |
| 90 | + * .strict() |
| 91 | + * .transform(normalizeVisibleWhen) |
| 92 | + * ``` |
| 93 | + */ |
| 94 | +export const strictVisibilityError: z.core.$ZodErrorMap = (issue) => { |
| 95 | + if (issue.code !== 'unrecognized_keys') return undefined; |
| 96 | + const keys = (issue as { keys?: readonly string[] }).keys ?? []; |
| 97 | + const list = keys.map((k) => `\`${k}\``).join(', '); |
| 98 | + const base = |
| 99 | + `Unrecognized key(s) on this view/page schema: ${list}. ` + |
| 100 | + `Before ADR-0089 D3a these were dropped silently, shipping inert metadata; ` + |
| 101 | + `a mis-layered or stale key is now a loud parse error.`; |
| 102 | + if (keys.some(looksLikeVisibilityKey)) { |
| 103 | + return ( |
| 104 | + base + |
| 105 | + ' If this is the conditional-visibility predicate, the canonical key is ' + |
| 106 | + '`visibleWhen` (ADR-0089) — `visibleOn` (view form) and `visibility` (page ' + |
| 107 | + 'component) are still accepted as deprecated aliases.' |
| 108 | + ); |
| 109 | + } |
| 110 | + return base; |
| 111 | +}; |
0 commit comments