@warlock.js/seal 5.19.1 → 5.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/llms-full.txt +1165 -1165
- package/llms.txt +7 -7
- package/package.json +3 -3
- package/skills/bridge-standard-schema/SKILL.md +138 -138
- package/skills/compose-seal-modifiers/SKILL.md +271 -271
- package/skills/define-structural-shape/SKILL.md +138 -138
- package/skills/extend-seal-with-plugins/SKILL.md +178 -178
- package/skills/generate-json-schema/SKILL.md +143 -143
- package/skills/handle-seal-errors/SKILL.md +148 -148
- package/skills/index.md +21 -0
- package/skills/pick-seal-primitive/SKILL.md +149 -149
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: define-structural-shape
|
|
3
|
-
description: "
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Structural validators — picking guide
|
|
7
|
-
|
|
8
|
-
The five structural factories. Each composes — pass leaf primitives or other structural validators inside, infer with `Infer<typeof schema>`. **Method surface for `v.object` and `v.array` lives in `object-methods.md` and `array-methods.md`** — this file is just orientation.
|
|
9
|
-
|
|
10
|
-
## `v.object` — fixed-key records
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
v.object({
|
|
14
|
-
email: v.string().email(),
|
|
15
|
-
age: v.int().min(13).optional(),
|
|
16
|
-
role: v.literal("admin", "user", "guest"),
|
|
17
|
-
})
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
- **Required by default.** `.optional()` to opt out — the `{ isOptional: true }` brand makes the key optional in `Infer<>`.
|
|
21
|
-
- **Unknown keys policy.** By default extra keys are silently dropped from `data`. Toggle with `.allowUnknown()` (forward as-is), `.stripUnknown()` (explicit drop), or `.allow(...keys)` (whitelist specific extras). See [`object-methods.md`](./object-methods.md).
|
|
22
|
-
- **Schema composition.** `.extend(schema)`, `.merge(other)`, `.pick(...keys)`, `.without(...keys)`, `.partial(...keys)`, `.requiredFields(...keys)` — all in [`object-methods.md`](./object-methods.md).
|
|
23
|
-
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`) attach to fields *inside* a `v.object`. Without a parent, sibling resolution silently passes.
|
|
24
|
-
|
|
25
|
-
## `v.array` — homogeneous lists
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
v.array(v.string()) // type: string[]
|
|
29
|
-
v.array(userSchema) // type: User[]
|
|
30
|
-
v.array(v.array(v.int())) // type: number[][] — recursive inference
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
The inner validator runs against each element; failure on any element fails the array. Method surface (`.min`, `.max`, `.unique`, `.sorted`, `.flip`, …) in [`array-methods.md`](./array-methods.md).
|
|
34
|
-
|
|
35
|
-
## `v.record` — homogeneous values, dynamic keys
|
|
36
|
-
|
|
37
|
-
```ts
|
|
38
|
-
v.record(v.int()) // type: Record<string, number>
|
|
39
|
-
v.record(v.object({ count: v.int() })) // type: Record<string, { count: number }>
|
|
40
|
-
v.record() // type: Record<string, any>
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Reach for `v.record` when keys are dynamic (user-provided, dictionary-style) but values share a schema. If keys are also constrained (e.g. only `"draft" | "published"`), use `v.object` with literal keys instead — the constraint lives in the type.
|
|
44
|
-
|
|
45
|
-
## `v.tuple` — positional types
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
v.tuple([v.string(), v.int(), v.boolean()]) // type: [string, number, boolean]
|
|
49
|
-
v.tuple([v.literal("ok"), v.string()]) // type: ["ok", string]
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Each position has its own validator; the array length must match the tuple length. Pair with `v.literal` at position 0 for result-tuple patterns (`["ok", data]` vs `["error", message]`).
|
|
53
|
-
|
|
54
|
-
## `v.union` — one of N validators (untagged)
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
v.union([v.string(), v.int()]) // type: string | number
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
The first type-matching validator wins, picked via each branch's `matchesType()`. Use for unions of **scalar** types (string vs number, etc.) where `matchesType` is enough to disambiguate. For object-vs-object unions, reach for `v.discriminatedUnion` instead — `matchesType` can't distinguish two object branches and you'll get errors from the wrong branch.
|
|
61
|
-
|
|
62
|
-
## `v.discriminatedUnion` — tagged unions (recommended for objects)
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
const email = v.object({ type: v.literal("email"), email: v.string().email() });
|
|
66
|
-
const sms = v.object({ type: v.literal("sms"), phone: v.string() });
|
|
67
|
-
const push = v.object({ type: v.literal("push"), deviceId: v.string() });
|
|
68
|
-
|
|
69
|
-
const notif = v.discriminatedUnion("type", [email, sms, push]);
|
|
70
|
-
|
|
71
|
-
type Notif = Infer<typeof notif>;
|
|
72
|
-
// { type: "email"; email: string }
|
|
73
|
-
// | { type: "sms"; phone: string }
|
|
74
|
-
// | { type: "push"; deviceId: string }
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Routes payloads by reading the discriminator field (here `type`), looking it up in a key→branch map built at construction time, and delegating to the matching branch only. Benefits over plain `v.union`:
|
|
78
|
-
|
|
79
|
-
- **Precise errors.** Failures come from the matched branch, not from every branch.
|
|
80
|
-
- **O(1) routing** instead of trial-and-error.
|
|
81
|
-
- **Exact TS inference** — discriminated union with narrowing inside `if (x.type === "email")` blocks.
|
|
82
|
-
- **Cleaner JSON Schema** — `oneOf` with literal discriminators; OpenAI-strict accepts it.
|
|
83
|
-
|
|
84
|
-
Construction-time validation throws on:
|
|
85
|
-
|
|
86
|
-
- Missing discriminator field in any branch
|
|
87
|
-
- Non-literal discriminator (must be `v.literal(...)`)
|
|
88
|
-
- Duplicate discriminator values across branches
|
|
89
|
-
|
|
90
|
-
So misconfigurations surface at schema-build time, not runtime.
|
|
91
|
-
|
|
92
|
-
## `v.lazy` — recursive and forward-referenced schemas
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
type Category = { name: string; children: Category[] };
|
|
96
|
-
|
|
97
|
-
const categorySchema: ObjectValidator<...> = v.object({
|
|
98
|
-
name: v.string(),
|
|
99
|
-
children: v.array(v.lazy(() => categorySchema)),
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
type T = Infer<typeof categorySchema>;
|
|
103
|
-
// { name: string; children: T[] } ← recursive type
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Defers resolution of the inner validator until validate-time. JavaScript evaluates the object literal before the `const` binding completes, so the inner reference would normally fail with `ReferenceError`. The thunk is invoked only when `validate()` runs, by which time the binding exists.
|
|
107
|
-
|
|
108
|
-
**Memoised.** The thunk fires once on first use; subsequent calls reuse the cached validator.
|
|
109
|
-
|
|
110
|
-
**JSON Schema caveat.** Simple-resolve in v1 — recursive shapes will infinite-loop in `toJsonSchema()`. If you need JSON Schema for a recursive shape, generate it manually with `$defs` + `$ref` until v2 lands.
|
|
111
|
-
|
|
112
|
-
**TS inference requires a recursive type alias.** TS can't infer `Category = { name: string; children: Category[] }` from the schema alone — declare the type explicitly and annotate the schema variable with `ObjectValidator<...>` or similar (same pattern as Zod's `z.ZodType<Category>`).
|
|
113
|
-
|
|
114
|
-
Use for: trees (categories, file systems), threaded data (comment chains), mutually recursive schemas (`A` references `B` references `A`), and forward references (a schema needs to use one defined later in the file).
|
|
115
|
-
|
|
116
|
-
## Quick map
|
|
117
|
-
|
|
118
|
-
| Want | Reach for |
|
|
119
|
-
| --- | --- |
|
|
120
|
-
| Fixed-shape record | `v.object({...})` |
|
|
121
|
-
| Dynamic keys, same value shape | `v.record(valueSchema)` |
|
|
122
|
-
| List of items | `v.array(itemSchema)` |
|
|
123
|
-
| Position-typed array | `v.tuple([a, b, c])` |
|
|
124
|
-
| One of N scalar types | `v.union([...])` |
|
|
125
|
-
| One of N object shapes with a tag field | `v.discriminatedUnion(key, [...])` |
|
|
126
|
-
| Self-referencing or forward reference | `v.lazy(() => schema)` |
|
|
127
|
-
| One of N constants | `v.literal(...values)` (not structural — see [`@warlock.js/seal/pick-seal-primitive/SKILL.md`](@warlock.js/seal/pick-seal-primitive/SKILL.md)) |
|
|
128
|
-
|
|
129
|
-
## A note on cascade Models
|
|
130
|
-
|
|
131
|
-
`@warlock.js/cascade` Models declare `static schema: ObjectValidator<TSchema>`. Passing a `v.object({...})` works directly — the factory return widens to fit the Model's invariant generic without breaking type checking. If TS complains *"`ObjectValidator<{specific}>` is not assignable to `ObjectValidator<TSchema>`"*, that's the variance trap and the answer isn't to widen the schema — it's almost always that `Model<TSchema>` was parameterized with a hand-rolled type that drifted from the schema's inferred shape. Fix the type, not the schema. See [`@warlock.js/seal/bridge-standard-schema/SKILL.md`](@warlock.js/seal/bridge-standard-schema/SKILL.md).
|
|
132
|
-
|
|
133
|
-
## Method-surface reference
|
|
134
|
-
|
|
135
|
-
- [`object-methods.md`](./object-methods.md) — `.extend` / `.merge` / `.pick` / `.without` / `.partial` / `.requiredFields` / `.allowUnknown` / `.stripUnknown` / `.allow` / `.trim`.
|
|
136
|
-
- [`array-methods.md`](./array-methods.md) — `.minLength` / `.maxLength` / `.length` / `.between` / `.unique` / `.sorted` / `.flip` / `.sort` / `.onlyUnique`.
|
|
137
|
-
|
|
138
|
-
`v.record` and `v.tuple` share the array-style length surface. `v.union` and `v.discriminatedUnion` have no chainable methods beyond what's shown above. `v.lazy` is a single-call factory.
|
|
1
|
+
---
|
|
2
|
+
name: define-structural-shape
|
|
3
|
+
description: 'Compose `v.object` / `v.array` / `v.record` / `v.tuple` / `v.union` / `v.discriminatedUnion` / `v.lazy`. Triggers: `v.object`, `v.array`, `v.record`, `v.tuple`, `v.union`, `v.discriminatedUnion`, `v.lazy`, `ObjectValidator`; "how do I build an object schema", "dynamic-keyed record", "tagged union with discriminator", "recursive schema", "self-referencing schema"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: leaf primitives — `@warlock.js/seal/pick-seal-primitive/SKILL.md`; modifiers — `@warlock.js/seal/compose-seal-modifiers/SKILL.md`; standard-schema bridge — `@warlock.js/seal/bridge-standard-schema/SKILL.md`; competing libs `zod`, `valibot`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Structural validators — picking guide
|
|
7
|
+
|
|
8
|
+
The five structural factories. Each composes — pass leaf primitives or other structural validators inside, infer with `Infer<typeof schema>`. **Method surface for `v.object` and `v.array` lives in `object-methods.md` and `array-methods.md`** — this file is just orientation.
|
|
9
|
+
|
|
10
|
+
## `v.object` — fixed-key records
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
v.object({
|
|
14
|
+
email: v.string().email(),
|
|
15
|
+
age: v.int().min(13).optional(),
|
|
16
|
+
role: v.literal("admin", "user", "guest"),
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **Required by default.** `.optional()` to opt out — the `{ isOptional: true }` brand makes the key optional in `Infer<>`.
|
|
21
|
+
- **Unknown keys policy.** By default extra keys are silently dropped from `data`. Toggle with `.allowUnknown()` (forward as-is), `.stripUnknown()` (explicit drop), or `.allow(...keys)` (whitelist specific extras). See [`object-methods.md`](./object-methods.md).
|
|
22
|
+
- **Schema composition.** `.extend(schema)`, `.merge(other)`, `.pick(...keys)`, `.without(...keys)`, `.partial(...keys)`, `.requiredFields(...keys)` — all in [`object-methods.md`](./object-methods.md).
|
|
23
|
+
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`) attach to fields *inside* a `v.object`. Without a parent, sibling resolution silently passes.
|
|
24
|
+
|
|
25
|
+
## `v.array` — homogeneous lists
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
v.array(v.string()) // type: string[]
|
|
29
|
+
v.array(userSchema) // type: User[]
|
|
30
|
+
v.array(v.array(v.int())) // type: number[][] — recursive inference
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The inner validator runs against each element; failure on any element fails the array. Method surface (`.min`, `.max`, `.unique`, `.sorted`, `.flip`, …) in [`array-methods.md`](./array-methods.md).
|
|
34
|
+
|
|
35
|
+
## `v.record` — homogeneous values, dynamic keys
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
v.record(v.int()) // type: Record<string, number>
|
|
39
|
+
v.record(v.object({ count: v.int() })) // type: Record<string, { count: number }>
|
|
40
|
+
v.record() // type: Record<string, any>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Reach for `v.record` when keys are dynamic (user-provided, dictionary-style) but values share a schema. If keys are also constrained (e.g. only `"draft" | "published"`), use `v.object` with literal keys instead — the constraint lives in the type.
|
|
44
|
+
|
|
45
|
+
## `v.tuple` — positional types
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
v.tuple([v.string(), v.int(), v.boolean()]) // type: [string, number, boolean]
|
|
49
|
+
v.tuple([v.literal("ok"), v.string()]) // type: ["ok", string]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Each position has its own validator; the array length must match the tuple length. Pair with `v.literal` at position 0 for result-tuple patterns (`["ok", data]` vs `["error", message]`).
|
|
53
|
+
|
|
54
|
+
## `v.union` — one of N validators (untagged)
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
v.union([v.string(), v.int()]) // type: string | number
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The first type-matching validator wins, picked via each branch's `matchesType()`. Use for unions of **scalar** types (string vs number, etc.) where `matchesType` is enough to disambiguate. For object-vs-object unions, reach for `v.discriminatedUnion` instead — `matchesType` can't distinguish two object branches and you'll get errors from the wrong branch.
|
|
61
|
+
|
|
62
|
+
## `v.discriminatedUnion` — tagged unions (recommended for objects)
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const email = v.object({ type: v.literal("email"), email: v.string().email() });
|
|
66
|
+
const sms = v.object({ type: v.literal("sms"), phone: v.string() });
|
|
67
|
+
const push = v.object({ type: v.literal("push"), deviceId: v.string() });
|
|
68
|
+
|
|
69
|
+
const notif = v.discriminatedUnion("type", [email, sms, push]);
|
|
70
|
+
|
|
71
|
+
type Notif = Infer<typeof notif>;
|
|
72
|
+
// { type: "email"; email: string }
|
|
73
|
+
// | { type: "sms"; phone: string }
|
|
74
|
+
// | { type: "push"; deviceId: string }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Routes payloads by reading the discriminator field (here `type`), looking it up in a key→branch map built at construction time, and delegating to the matching branch only. Benefits over plain `v.union`:
|
|
78
|
+
|
|
79
|
+
- **Precise errors.** Failures come from the matched branch, not from every branch.
|
|
80
|
+
- **O(1) routing** instead of trial-and-error.
|
|
81
|
+
- **Exact TS inference** — discriminated union with narrowing inside `if (x.type === "email")` blocks.
|
|
82
|
+
- **Cleaner JSON Schema** — `oneOf` with literal discriminators; OpenAI-strict accepts it.
|
|
83
|
+
|
|
84
|
+
Construction-time validation throws on:
|
|
85
|
+
|
|
86
|
+
- Missing discriminator field in any branch
|
|
87
|
+
- Non-literal discriminator (must be `v.literal(...)`)
|
|
88
|
+
- Duplicate discriminator values across branches
|
|
89
|
+
|
|
90
|
+
So misconfigurations surface at schema-build time, not runtime.
|
|
91
|
+
|
|
92
|
+
## `v.lazy` — recursive and forward-referenced schemas
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
type Category = { name: string; children: Category[] };
|
|
96
|
+
|
|
97
|
+
const categorySchema: ObjectValidator<...> = v.object({
|
|
98
|
+
name: v.string(),
|
|
99
|
+
children: v.array(v.lazy(() => categorySchema)),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
type T = Infer<typeof categorySchema>;
|
|
103
|
+
// { name: string; children: T[] } ← recursive type
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Defers resolution of the inner validator until validate-time. JavaScript evaluates the object literal before the `const` binding completes, so the inner reference would normally fail with `ReferenceError`. The thunk is invoked only when `validate()` runs, by which time the binding exists.
|
|
107
|
+
|
|
108
|
+
**Memoised.** The thunk fires once on first use; subsequent calls reuse the cached validator.
|
|
109
|
+
|
|
110
|
+
**JSON Schema caveat.** Simple-resolve in v1 — recursive shapes will infinite-loop in `toJsonSchema()`. If you need JSON Schema for a recursive shape, generate it manually with `$defs` + `$ref` until v2 lands.
|
|
111
|
+
|
|
112
|
+
**TS inference requires a recursive type alias.** TS can't infer `Category = { name: string; children: Category[] }` from the schema alone — declare the type explicitly and annotate the schema variable with `ObjectValidator<...>` or similar (same pattern as Zod's `z.ZodType<Category>`).
|
|
113
|
+
|
|
114
|
+
Use for: trees (categories, file systems), threaded data (comment chains), mutually recursive schemas (`A` references `B` references `A`), and forward references (a schema needs to use one defined later in the file).
|
|
115
|
+
|
|
116
|
+
## Quick map
|
|
117
|
+
|
|
118
|
+
| Want | Reach for |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| Fixed-shape record | `v.object({...})` |
|
|
121
|
+
| Dynamic keys, same value shape | `v.record(valueSchema)` |
|
|
122
|
+
| List of items | `v.array(itemSchema)` |
|
|
123
|
+
| Position-typed array | `v.tuple([a, b, c])` |
|
|
124
|
+
| One of N scalar types | `v.union([...])` |
|
|
125
|
+
| One of N object shapes with a tag field | `v.discriminatedUnion(key, [...])` |
|
|
126
|
+
| Self-referencing or forward reference | `v.lazy(() => schema)` |
|
|
127
|
+
| One of N constants | `v.literal(...values)` (not structural — see [`@warlock.js/seal/pick-seal-primitive/SKILL.md`](@warlock.js/seal/pick-seal-primitive/SKILL.md)) |
|
|
128
|
+
|
|
129
|
+
## A note on cascade Models
|
|
130
|
+
|
|
131
|
+
`@warlock.js/cascade` Models declare `static schema: ObjectValidator<TSchema>`. Passing a `v.object({...})` works directly — the factory return widens to fit the Model's invariant generic without breaking type checking. If TS complains *"`ObjectValidator<{specific}>` is not assignable to `ObjectValidator<TSchema>`"*, that's the variance trap and the answer isn't to widen the schema — it's almost always that `Model<TSchema>` was parameterized with a hand-rolled type that drifted from the schema's inferred shape. Fix the type, not the schema. See [`@warlock.js/seal/bridge-standard-schema/SKILL.md`](@warlock.js/seal/bridge-standard-schema/SKILL.md).
|
|
132
|
+
|
|
133
|
+
## Method-surface reference
|
|
134
|
+
|
|
135
|
+
- [`object-methods.md`](./object-methods.md) — `.extend` / `.merge` / `.pick` / `.without` / `.partial` / `.requiredFields` / `.allowUnknown` / `.stripUnknown` / `.allow` / `.trim`.
|
|
136
|
+
- [`array-methods.md`](./array-methods.md) — `.minLength` / `.maxLength` / `.length` / `.between` / `.unique` / `.sorted` / `.flip` / `.sort` / `.onlyUnique`.
|
|
137
|
+
|
|
138
|
+
`v.record` and `v.tuple` share the array-style length surface. `v.union` and `v.discriminatedUnion` have no chainable methods beyond what's shown above. `v.lazy` is a single-call factory.
|
|
@@ -1,178 +1,178 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: extend-seal-with-plugins
|
|
3
|
-
description: "
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Extend seal with plugins
|
|
7
|
-
|
|
8
|
-
Seal exposes a plugin system so you can add validator methods without forking the package. A plugin is an object with `name`, optional `version`/`description`, and an `install` function that runs once when the plugin is registered. The standard pattern is to `Object.assign(StringValidator.prototype, { ... })` to graft new methods onto a validator class.
|
|
9
|
-
|
|
10
|
-
## When to reach for a plugin
|
|
11
|
-
|
|
12
|
-
- The validator method you want **does not exist** in built-in seal — check [`@warlock.js/seal/pick-seal-primitive/SKILL.md`](@warlock.js/seal/pick-seal-primitive/SKILL.md) and its method references first.
|
|
13
|
-
- The validation is **stable and reusable** across modules — domain-specific formats (IBAN, postal codes, tax IDs, license plates, internal ID schemes).
|
|
14
|
-
- You want the **chainable syntax** — `v.string().slug()` reads better than `v.string().pattern(/.../).addMutator(s => slugify(s))` at every call site.
|
|
15
|
-
|
|
16
|
-
**Don't** reach for a plugin when a one-off `.pattern()` would do. The boilerplate (declare module, register on boot) is justified only when you'll call the new method many times.
|
|
17
|
-
|
|
18
|
-
## The plugin shape
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
import type { SealPlugin } from "@warlock.js/seal";
|
|
22
|
-
|
|
23
|
-
type SealPlugin = {
|
|
24
|
-
name: string; // unique identifier — duplicates warn and skip install
|
|
25
|
-
version?: string;
|
|
26
|
-
description?: string;
|
|
27
|
-
install: (context: { name: string; version?: string }) => void | Promise<void>;
|
|
28
|
-
uninstall?: () => void | Promise<void>;
|
|
29
|
-
};
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
The `install` function is where you add methods. Typically you patch a validator class prototype:
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
import { StringValidator, type SealPlugin } from "@warlock.js/seal";
|
|
36
|
-
|
|
37
|
-
export const slugPlugin: SealPlugin = {
|
|
38
|
-
name: "slug",
|
|
39
|
-
version: "1.0.0",
|
|
40
|
-
description: "Adds .slug() — pattern-only slug validation",
|
|
41
|
-
|
|
42
|
-
install() {
|
|
43
|
-
Object.assign(StringValidator.prototype, {
|
|
44
|
-
slug(this: StringValidator, errorMessage?: string) {
|
|
45
|
-
return this.pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, errorMessage);
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
`uninstall` is optional. Provide it when your plugin needs to clean up — e.g. removing prototype methods for hot-reload scenarios. Most production plugins skip it; methods grafted at boot stay for the process lifetime.
|
|
53
|
-
|
|
54
|
-
## Registering plugins
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
import { registerPlugin, unregisterPlugin, hasPlugin, getInstalledPlugins } from "@warlock.js/seal";
|
|
58
|
-
|
|
59
|
-
await registerPlugin(slugPlugin);
|
|
60
|
-
// console.warn if "slug" is already installed; otherwise install() runs and the plugin is tracked.
|
|
61
|
-
|
|
62
|
-
hasPlugin("slug"); // → true
|
|
63
|
-
getInstalledPlugins(); // → [slugPlugin]
|
|
64
|
-
|
|
65
|
-
await unregisterPlugin("slug");
|
|
66
|
-
// runs slugPlugin.uninstall?.(); removes from registry.
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
`registerPlugin` is async (the `install` function may be async). Await it during boot so the methods are available before the first request.
|
|
70
|
-
|
|
71
|
-
**Where to register:** the conventional place in a Warlock app is a side-effect file loaded by `warlock.config.ts` — e.g. `src/setup/seal-plugins.ts`:
|
|
72
|
-
|
|
73
|
-
```ts title="src/setup/seal-plugins.ts"
|
|
74
|
-
import { registerPlugin } from "@warlock.js/seal";
|
|
75
|
-
import { slugPlugin } from "./plugins/slug-plugin";
|
|
76
|
-
import { postalCodePlugin } from "./plugins/postal-code-plugin";
|
|
77
|
-
|
|
78
|
-
export async function setupSealPlugins() {
|
|
79
|
-
await registerPlugin(slugPlugin);
|
|
80
|
-
await registerPlugin(postalCodePlugin);
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Then call `setupSealPlugins()` in a bootstrap connector. Registering at top-level module scope works too (it's idempotent — duplicates warn and skip), but explicit setup is clearer.
|
|
85
|
-
|
|
86
|
-
## TypeScript — declare the new methods
|
|
87
|
-
|
|
88
|
-
`Object.assign` on a prototype is invisible to TypeScript. Declare the new methods with module augmentation so call sites compile:
|
|
89
|
-
|
|
90
|
-
```ts title="src/setup/seal-plugins.types.ts"
|
|
91
|
-
import "@warlock.js/seal";
|
|
92
|
-
|
|
93
|
-
declare module "@warlock.js/seal" {
|
|
94
|
-
interface StringValidator {
|
|
95
|
-
/** Pattern-only slug — `"hello-world"`, not `"Hello World"`. */
|
|
96
|
-
slug(errorMessage?: string): StringValidator;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
After this file is included in the project's `tsconfig.json` (via `include` or a side-effect import), `v.string().slug()` autocompletes and type-checks everywhere.
|
|
102
|
-
|
|
103
|
-
**Important.** The augmentation has to declare on the **class** (`StringValidator`), not the factory return type. The factory return widens with `& StandardSchemaV1<...>` (see [`@warlock.js/seal/bridge-standard-schema/SKILL.md`](@warlock.js/seal/bridge-standard-schema/SKILL.md)) — augmentations on the intersection don't propagate. Patch the class, augment the class; the factory return picks up the new methods through structural inference.
|
|
104
|
-
|
|
105
|
-
## Larger example — postal codes per country
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
import { StringValidator, type SealPlugin } from "@warlock.js/seal";
|
|
109
|
-
|
|
110
|
-
const PATTERNS: Record<string, RegExp> = {
|
|
111
|
-
US: /^\d{5}(?:-\d{4})?$/,
|
|
112
|
-
DE: /^\d{5}$/,
|
|
113
|
-
UK: /^[A-Z]{1,2}\d[A-Z\d]? \d[A-Z]{2}$/i,
|
|
114
|
-
EG: /^\d{5}$/,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
export const postalCodePlugin: SealPlugin = {
|
|
118
|
-
name: "postal-code",
|
|
119
|
-
|
|
120
|
-
install() {
|
|
121
|
-
Object.assign(StringValidator.prototype, {
|
|
122
|
-
postalCode(this: StringValidator, country: keyof typeof PATTERNS, errorMessage?: string) {
|
|
123
|
-
const pattern = PATTERNS[country];
|
|
124
|
-
|
|
125
|
-
if (!pattern) {
|
|
126
|
-
throw new Error(`postalCode: unknown country "${country}"`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return this.pattern(pattern, errorMessage ?? `Invalid ${country} postal code`);
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
// Module augmentation
|
|
138
|
-
declare module "@warlock.js/seal" {
|
|
139
|
-
interface StringValidator {
|
|
140
|
-
postalCode(country: "US" | "DE" | "UK" | "EG", errorMessage?: string): StringValidator;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Use site
|
|
145
|
-
const addressSchema = v.object({
|
|
146
|
-
country: v.literal("US", "DE", "UK", "EG"),
|
|
147
|
-
postal: v.string().postalCode("DE"),
|
|
148
|
-
});
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
## Patterns beyond `StringValidator`
|
|
152
|
-
|
|
153
|
-
The same approach works on any validator class. Pick the right prototype:
|
|
154
|
-
|
|
155
|
-
- `StringValidator` — string methods (`.slug`, `.postalCode`, `.licensePlate`).
|
|
156
|
-
- `NumberValidator` / `IntValidator` / `FloatValidator` — number methods.
|
|
157
|
-
- `DateValidator` — date methods (`.businessDayInCountry("US")`).
|
|
158
|
-
- `ArrayValidator` / `ObjectValidator` — structural methods (rarer).
|
|
159
|
-
- `BaseValidator` — universal methods (rare — usually a sign you want a separate `v.something()` factory instead).
|
|
160
|
-
|
|
161
|
-
For a method that creates a **new validator** (not chained from an existing one), expose it as a regular function alongside `v` rather than patching the factory. E.g. export `iban()` from your plugin module that returns a configured `v.string()`.
|
|
162
|
-
|
|
163
|
-
## Introspection — checking what's loaded
|
|
164
|
-
|
|
165
|
-
```ts
|
|
166
|
-
hasPlugin("slug"); // boolean
|
|
167
|
-
getInstalledPlugins(); // SealPlugin[]
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Use these in startup diagnostics or in tests that need to assert a plugin is registered before exercising a method that depends on it.
|
|
171
|
-
|
|
172
|
-
## Things NOT to do
|
|
173
|
-
|
|
174
|
-
- Don't `Object.assign(BaseValidator.prototype, ...)` for type-specific methods. The method would exist on every validator (`v.boolean().slug()` typechecks but breaks at runtime). Patch the narrowest class that owns the method.
|
|
175
|
-
- Don't forget the module augmentation. Without it, the new methods exist at runtime but TS rejects every call site.
|
|
176
|
-
- Don't make the plugin's `install` function depend on shared state mutable from elsewhere. Plugins should be idempotent — installing twice (or registering across hot-reloads) should not break anything.
|
|
177
|
-
- Don't ship a plugin that overrides a built-in method without a clear reason. If you must, `uninstall` should restore the original — but the better path is a different method name.
|
|
178
|
-
- Don't author one plugin per method. Group related methods (e.g. "country-specific validators") into one plugin so the install/uninstall lifecycle is coherent.
|
|
1
|
+
---
|
|
2
|
+
name: extend-seal-with-plugins
|
|
3
|
+
description: 'Author a custom seal plugin to add validator methods (`.slug`, `.postalCode`, etc) — `SealPlugin` shape, `registerPlugin` lifecycle, TS prototype augmentation. Triggers: `SealPlugin`, `registerPlugin`, `unregisterPlugin`, `hasPlugin`, `getInstalledPlugins`, `StringValidator`, `NumberValidator`, `DateValidator`, `BaseValidator`, `install`, `uninstall`; "how do I add a custom validator method", "write a seal plugin", "extend v.string with .slug", "module augmentation for seal"; typical import `import { StringValidator, registerPlugin, type SealPlugin } from "@warlock.js/seal"`. Skip: built-in primitives — `@warlock.js/seal/pick-seal-primitive/SKILL.md`; bridge typing — `@warlock.js/seal/bridge-standard-schema/SKILL.md`; competing `zod` `.refine`/`.transform`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Extend seal with plugins
|
|
7
|
+
|
|
8
|
+
Seal exposes a plugin system so you can add validator methods without forking the package. A plugin is an object with `name`, optional `version`/`description`, and an `install` function that runs once when the plugin is registered. The standard pattern is to `Object.assign(StringValidator.prototype, { ... })` to graft new methods onto a validator class.
|
|
9
|
+
|
|
10
|
+
## When to reach for a plugin
|
|
11
|
+
|
|
12
|
+
- The validator method you want **does not exist** in built-in seal — check [`@warlock.js/seal/pick-seal-primitive/SKILL.md`](@warlock.js/seal/pick-seal-primitive/SKILL.md) and its method references first.
|
|
13
|
+
- The validation is **stable and reusable** across modules — domain-specific formats (IBAN, postal codes, tax IDs, license plates, internal ID schemes).
|
|
14
|
+
- You want the **chainable syntax** — `v.string().slug()` reads better than `v.string().pattern(/.../).addMutator(s => slugify(s))` at every call site.
|
|
15
|
+
|
|
16
|
+
**Don't** reach for a plugin when a one-off `.pattern()` would do. The boilerplate (declare module, register on boot) is justified only when you'll call the new method many times.
|
|
17
|
+
|
|
18
|
+
## The plugin shape
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import type { SealPlugin } from "@warlock.js/seal";
|
|
22
|
+
|
|
23
|
+
type SealPlugin = {
|
|
24
|
+
name: string; // unique identifier — duplicates warn and skip install
|
|
25
|
+
version?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
install: (context: { name: string; version?: string }) => void | Promise<void>;
|
|
28
|
+
uninstall?: () => void | Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The `install` function is where you add methods. Typically you patch a validator class prototype:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { StringValidator, type SealPlugin } from "@warlock.js/seal";
|
|
36
|
+
|
|
37
|
+
export const slugPlugin: SealPlugin = {
|
|
38
|
+
name: "slug",
|
|
39
|
+
version: "1.0.0",
|
|
40
|
+
description: "Adds .slug() — pattern-only slug validation",
|
|
41
|
+
|
|
42
|
+
install() {
|
|
43
|
+
Object.assign(StringValidator.prototype, {
|
|
44
|
+
slug(this: StringValidator, errorMessage?: string) {
|
|
45
|
+
return this.pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, errorMessage);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`uninstall` is optional. Provide it when your plugin needs to clean up — e.g. removing prototype methods for hot-reload scenarios. Most production plugins skip it; methods grafted at boot stay for the process lifetime.
|
|
53
|
+
|
|
54
|
+
## Registering plugins
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { registerPlugin, unregisterPlugin, hasPlugin, getInstalledPlugins } from "@warlock.js/seal";
|
|
58
|
+
|
|
59
|
+
await registerPlugin(slugPlugin);
|
|
60
|
+
// console.warn if "slug" is already installed; otherwise install() runs and the plugin is tracked.
|
|
61
|
+
|
|
62
|
+
hasPlugin("slug"); // → true
|
|
63
|
+
getInstalledPlugins(); // → [slugPlugin]
|
|
64
|
+
|
|
65
|
+
await unregisterPlugin("slug");
|
|
66
|
+
// runs slugPlugin.uninstall?.(); removes from registry.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`registerPlugin` is async (the `install` function may be async). Await it during boot so the methods are available before the first request.
|
|
70
|
+
|
|
71
|
+
**Where to register:** the conventional place in a Warlock app is a side-effect file loaded by `warlock.config.ts` — e.g. `src/setup/seal-plugins.ts`:
|
|
72
|
+
|
|
73
|
+
```ts title="src/setup/seal-plugins.ts"
|
|
74
|
+
import { registerPlugin } from "@warlock.js/seal";
|
|
75
|
+
import { slugPlugin } from "./plugins/slug-plugin";
|
|
76
|
+
import { postalCodePlugin } from "./plugins/postal-code-plugin";
|
|
77
|
+
|
|
78
|
+
export async function setupSealPlugins() {
|
|
79
|
+
await registerPlugin(slugPlugin);
|
|
80
|
+
await registerPlugin(postalCodePlugin);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then call `setupSealPlugins()` in a bootstrap connector. Registering at top-level module scope works too (it's idempotent — duplicates warn and skip), but explicit setup is clearer.
|
|
85
|
+
|
|
86
|
+
## TypeScript — declare the new methods
|
|
87
|
+
|
|
88
|
+
`Object.assign` on a prototype is invisible to TypeScript. Declare the new methods with module augmentation so call sites compile:
|
|
89
|
+
|
|
90
|
+
```ts title="src/setup/seal-plugins.types.ts"
|
|
91
|
+
import "@warlock.js/seal";
|
|
92
|
+
|
|
93
|
+
declare module "@warlock.js/seal" {
|
|
94
|
+
interface StringValidator {
|
|
95
|
+
/** Pattern-only slug — `"hello-world"`, not `"Hello World"`. */
|
|
96
|
+
slug(errorMessage?: string): StringValidator;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
After this file is included in the project's `tsconfig.json` (via `include` or a side-effect import), `v.string().slug()` autocompletes and type-checks everywhere.
|
|
102
|
+
|
|
103
|
+
**Important.** The augmentation has to declare on the **class** (`StringValidator`), not the factory return type. The factory return widens with `& StandardSchemaV1<...>` (see [`@warlock.js/seal/bridge-standard-schema/SKILL.md`](@warlock.js/seal/bridge-standard-schema/SKILL.md)) — augmentations on the intersection don't propagate. Patch the class, augment the class; the factory return picks up the new methods through structural inference.
|
|
104
|
+
|
|
105
|
+
## Larger example — postal codes per country
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { StringValidator, type SealPlugin } from "@warlock.js/seal";
|
|
109
|
+
|
|
110
|
+
const PATTERNS: Record<string, RegExp> = {
|
|
111
|
+
US: /^\d{5}(?:-\d{4})?$/,
|
|
112
|
+
DE: /^\d{5}$/,
|
|
113
|
+
UK: /^[A-Z]{1,2}\d[A-Z\d]? \d[A-Z]{2}$/i,
|
|
114
|
+
EG: /^\d{5}$/,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const postalCodePlugin: SealPlugin = {
|
|
118
|
+
name: "postal-code",
|
|
119
|
+
|
|
120
|
+
install() {
|
|
121
|
+
Object.assign(StringValidator.prototype, {
|
|
122
|
+
postalCode(this: StringValidator, country: keyof typeof PATTERNS, errorMessage?: string) {
|
|
123
|
+
const pattern = PATTERNS[country];
|
|
124
|
+
|
|
125
|
+
if (!pattern) {
|
|
126
|
+
throw new Error(`postalCode: unknown country "${country}"`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return this.pattern(pattern, errorMessage ?? `Invalid ${country} postal code`);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// Module augmentation
|
|
138
|
+
declare module "@warlock.js/seal" {
|
|
139
|
+
interface StringValidator {
|
|
140
|
+
postalCode(country: "US" | "DE" | "UK" | "EG", errorMessage?: string): StringValidator;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Use site
|
|
145
|
+
const addressSchema = v.object({
|
|
146
|
+
country: v.literal("US", "DE", "UK", "EG"),
|
|
147
|
+
postal: v.string().postalCode("DE"),
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Patterns beyond `StringValidator`
|
|
152
|
+
|
|
153
|
+
The same approach works on any validator class. Pick the right prototype:
|
|
154
|
+
|
|
155
|
+
- `StringValidator` — string methods (`.slug`, `.postalCode`, `.licensePlate`).
|
|
156
|
+
- `NumberValidator` / `IntValidator` / `FloatValidator` — number methods.
|
|
157
|
+
- `DateValidator` — date methods (`.businessDayInCountry("US")`).
|
|
158
|
+
- `ArrayValidator` / `ObjectValidator` — structural methods (rarer).
|
|
159
|
+
- `BaseValidator` — universal methods (rare — usually a sign you want a separate `v.something()` factory instead).
|
|
160
|
+
|
|
161
|
+
For a method that creates a **new validator** (not chained from an existing one), expose it as a regular function alongside `v` rather than patching the factory. E.g. export `iban()` from your plugin module that returns a configured `v.string()`.
|
|
162
|
+
|
|
163
|
+
## Introspection — checking what's loaded
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
hasPlugin("slug"); // boolean
|
|
167
|
+
getInstalledPlugins(); // SealPlugin[]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Use these in startup diagnostics or in tests that need to assert a plugin is registered before exercising a method that depends on it.
|
|
171
|
+
|
|
172
|
+
## Things NOT to do
|
|
173
|
+
|
|
174
|
+
- Don't `Object.assign(BaseValidator.prototype, ...)` for type-specific methods. The method would exist on every validator (`v.boolean().slug()` typechecks but breaks at runtime). Patch the narrowest class that owns the method.
|
|
175
|
+
- Don't forget the module augmentation. Without it, the new methods exist at runtime but TS rejects every call site.
|
|
176
|
+
- Don't make the plugin's `install` function depend on shared state mutable from elsewhere. Plugins should be idempotent — installing twice (or registering across hot-reloads) should not break anything.
|
|
177
|
+
- Don't ship a plugin that overrides a built-in method without a clear reason. If you must, `uninstall` should restore the original — but the better path is a different method name.
|
|
178
|
+
- Don't author one plugin per method. Group related methods (e.g. "country-specific validators") into one plugin so the install/uninstall lifecycle is coherent.
|