@orkestrel/contract 0.0.1
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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/src/core/combinators.d.ts +384 -0
- package/dist/src/core/compilers.d.ts +119 -0
- package/dist/src/core/constants.d.ts +20 -0
- package/dist/src/core/helpers.d.ts +112 -0
- package/dist/src/core/index.d.ts +8 -0
- package/dist/src/core/index.js +2026 -0
- package/dist/src/core/index.js.map +1 -0
- package/dist/src/core/parsers.d.ts +230 -0
- package/dist/src/core/shapers.d.ts +178 -0
- package/dist/src/core/types.d.ts +372 -0
- package/dist/src/core/validators.d.ts +250 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orkestrel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @orkestrel/contract
|
|
2
|
+
|
|
3
|
+
The zero-dependency contract toolkit — runtime type guards, guard combinators, coerce-and-extract
|
|
4
|
+
parsers, and a shape DSL that compiles once into a guard, parser, JSON Schema, and generator that
|
|
5
|
+
can never drift. The foundation package of the `@orkestrel` line.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @orkestrel/contract
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node.js >= 24
|
|
16
|
+
- TypeScript-first (ships its own `.d.ts` types)
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createContract, integerShape, objectShape, stringShape } from '@orkestrel/contract'
|
|
22
|
+
|
|
23
|
+
const user = createContract(
|
|
24
|
+
objectShape({
|
|
25
|
+
name: stringShape({ min: 1 }),
|
|
26
|
+
age: integerShape({ min: 0, max: 120 }),
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
user.is({ name: 'Ada', age: 36 }) // true
|
|
31
|
+
user.parse({ name: 'Ada', age: '36' }) // { name: 'Ada', age: 36 } — coerces, or undefined
|
|
32
|
+
user.schema // the compiled JSON Schema
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Guide
|
|
36
|
+
|
|
37
|
+
For the full surface — guards, combinators, parsers, the JSON boundary, and the shape DSL — see
|
|
38
|
+
[`guides/src/contract.md`](guides/src/contract.md).
|
|
39
|
+
|
|
40
|
+
## Package
|
|
41
|
+
|
|
42
|
+
Published as a single typed entry point per the `exports` field in `package.json`.
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT © [Orkestrel](https://github.com/orkestrel) — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import type { AnyConstructor, FromGuards, Guard, GuardsShape, GuardType, IntersectionFromGuards, OptionalFromGuards, TupleFromGuards } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build a guard that accepts arrays whose every element satisfies `elementGuard`.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const isStringArray = arrayOf(isString)
|
|
8
|
+
* isStringArray(['a', 'b']) // true
|
|
9
|
+
* isStringArray(['a', 1]) // false
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export declare function arrayOf<T>(elementGuard: Guard<T>): Guard<readonly T[]>;
|
|
13
|
+
export declare function arrayOf(elementGuard: (value: unknown) => boolean): Guard<readonly unknown[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Build a guard that accepts fixed-arity tuples, testing each index with the
|
|
16
|
+
* corresponding guard.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const isPair = tupleOf(isString, isNumber)
|
|
21
|
+
* isPair(['hello', 42]) // true
|
|
22
|
+
* isPair(['hello']) // false — wrong arity
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function tupleOf<const Gs extends ReadonlyArray<Guard<unknown>>>(...guards: Gs): Guard<TupleFromGuards<Gs>>;
|
|
26
|
+
export declare function tupleOf(...predicates: ReadonlyArray<(value: unknown) => boolean>): Guard<readonly unknown[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Build a guard that accepts values identical (via `Object.is`) to one of the
|
|
29
|
+
* provided literal primitives.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const isRole = literalOf('admin', 'member', 'guest')
|
|
34
|
+
* isRole('admin') // true
|
|
35
|
+
* isRole('owner') // false
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function literalOf<const Literals extends ReadonlyArray<string | number | boolean>>(...literals: Literals): Guard<Literals[number]>;
|
|
39
|
+
/**
|
|
40
|
+
* Build a guard that accepts instances of the provided constructor.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* Verifies that `ctor` is a real constructor (via {@link isConstructor}) first,
|
|
44
|
+
* so passing an arrow function does not silently produce a broken guard.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const isDateValue = instanceOf(Date)
|
|
49
|
+
* isDateValue(new Date()) // true
|
|
50
|
+
* isDateValue({}) // false
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function instanceOf<C>(ctor: C): Guard<InstanceType<C & AnyConstructor<object>>>;
|
|
54
|
+
/**
|
|
55
|
+
* Build a guard from a native `enum` or any object whose values are strings or
|
|
56
|
+
* numbers.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* enum Direction { Up = 'up', Down = 'down' }
|
|
61
|
+
* const isDirection = enumOf(Direction)
|
|
62
|
+
* isDirection('up') // true
|
|
63
|
+
* isDirection('left') // false
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function enumOf<E extends Record<string, string | number>>(enumeration: E): Guard<E[keyof E]>;
|
|
67
|
+
/**
|
|
68
|
+
* Build a guard that accepts `Set` instances whose every element satisfies
|
|
69
|
+
* `elementGuard`.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const isStringSet = setOf(isString)
|
|
74
|
+
* isStringSet(new Set(['a', 'b'])) // true
|
|
75
|
+
* isStringSet(new Set(['a', 1])) // false
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function setOf<T>(elementGuard: Guard<T>): Guard<ReadonlySet<T>>;
|
|
79
|
+
export declare function setOf(elementGuard: (value: unknown) => boolean): Guard<ReadonlySet<unknown>>;
|
|
80
|
+
/**
|
|
81
|
+
* Build a guard that accepts `Map` instances where every key satisfies
|
|
82
|
+
* `keyGuard` and every value satisfies `valueGuard`.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const isStringNumberMap = mapOf(isString, isNumber)
|
|
87
|
+
* isStringNumberMap(new Map([['a', 1]])) // true
|
|
88
|
+
* isStringNumberMap(new Map([[1, 'a']])) // false
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare function mapOf<K, V>(keyGuard: Guard<K>, valueGuard: Guard<V>): Guard<ReadonlyMap<K, V>>;
|
|
92
|
+
export declare function mapOf(keyPredicate: (value: unknown) => boolean, valuePredicate: (value: unknown) => boolean): Guard<ReadonlyMap<unknown, unknown>>;
|
|
93
|
+
export declare function recordOf<S extends GuardsShape>(shape: S): Guard<FromGuards<S>>;
|
|
94
|
+
export declare function recordOf<S extends GuardsShape, K extends ReadonlyArray<keyof S & string>>(shape: S, optional: K): Guard<OptionalFromGuards<S, K>>;
|
|
95
|
+
export declare function recordOf<S extends GuardsShape>(shape: S, optional: true): Guard<Readonly<{
|
|
96
|
+
[P in keyof S]: FromGuards<S>[P] | undefined;
|
|
97
|
+
}>>;
|
|
98
|
+
/**
|
|
99
|
+
* Build a guard that accepts any iterable whose every element satisfies
|
|
100
|
+
* `elementGuard`.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* const isNumberIterable = iterableOf(isNumber)
|
|
105
|
+
* isNumberIterable([1, 2, 3]) // true
|
|
106
|
+
* isNumberIterable(new Set([1, 2])) // true
|
|
107
|
+
* isNumberIterable([1, 'two']) // false
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export declare function iterableOf<T>(elementGuard: Guard<T>): Guard<Iterable<T>>;
|
|
111
|
+
export declare function iterableOf(elementGuard: (value: unknown) => boolean): Guard<Iterable<unknown>>;
|
|
112
|
+
/**
|
|
113
|
+
* Build a guard that accepts values that are own keys of the provided object.
|
|
114
|
+
*
|
|
115
|
+
* @remarks
|
|
116
|
+
* Membership is tested with `Object.hasOwn`, so inherited prototype-chain keys
|
|
117
|
+
* (`toString`, `constructor`, …) are rejected. An own property that shadows a
|
|
118
|
+
* prototype name is accepted.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const COLORS = { red: '#f00', green: '#0f0', blue: '#00f' } as const
|
|
123
|
+
* const isColorKey = keyOf(COLORS)
|
|
124
|
+
* isColorKey('red') // true
|
|
125
|
+
* isColorKey('purple') // false
|
|
126
|
+
* isColorKey('toString') // false — inherited, not an own key
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare function keyOf<const O extends Readonly<Record<PropertyKey, unknown>>>(value: O): Guard<keyof O>;
|
|
130
|
+
/**
|
|
131
|
+
* Build a new guard shape by keeping only the listed keys — the structural
|
|
132
|
+
* equivalent of `Pick<T, K>`. Produces a shape for {@link recordOf}, not a guard.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const full = { name: isString, age: isNumber, role: isString }
|
|
137
|
+
* const isName = recordOf(pickOf(full, ['name']))
|
|
138
|
+
* isName({ name: 'Ada' }) // true
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export declare function pickOf<S extends GuardsShape, K extends ReadonlyArray<keyof S & string>>(shape: S, keys: K): Pick<S, K[number]>;
|
|
142
|
+
/**
|
|
143
|
+
* Build a new guard shape by removing the listed keys — the structural
|
|
144
|
+
* equivalent of `Omit<T, K>`. Produces a shape for {@link recordOf}, not a guard.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* const full = { name: isString, age: isNumber, role: isString }
|
|
149
|
+
* const isPublic = recordOf(omitOf(full, ['role']))
|
|
150
|
+
* isPublic({ name: 'Ada', age: 36 }) // true
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export declare function omitOf<S extends GuardsShape, K extends ReadonlyArray<keyof S & string>>(shape: S, keys: K): Omit<S, K[number]>;
|
|
154
|
+
/**
|
|
155
|
+
* Combine two guards with logical AND — passes only when both pass.
|
|
156
|
+
*
|
|
157
|
+
* @remarks
|
|
158
|
+
* Use {@link whereOf} when the right side refines an already-narrowed type; use
|
|
159
|
+
* `andOf` to combine two independent guards.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* const isShortString = andOf(isString, isNonEmptyString)
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export declare function andOf<A, B>(left: Guard<A>, right: Guard<B>): Guard<A & B>;
|
|
167
|
+
export declare function andOf<T, U extends T>(left: Guard<T>, right: (value: T) => value is U): Guard<U>;
|
|
168
|
+
export declare function andOf<T>(left: Guard<T>, right: (value: T) => boolean): Guard<T>;
|
|
169
|
+
export declare function andOf(left: (value: unknown) => boolean, right: (value: unknown) => boolean): Guard<unknown>;
|
|
170
|
+
/**
|
|
171
|
+
* Combine two guards with logical OR — passes when at least one passes. For more
|
|
172
|
+
* than two variants prefer {@link unionOf}.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* const isStringOrNumber = orOf(isString, isNumber)
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export declare function orOf<A, B>(left: Guard<A>, right: Guard<B>): Guard<A | B>;
|
|
180
|
+
export declare function orOf(left: (value: unknown) => boolean, right: (value: unknown) => boolean): Guard<unknown>;
|
|
181
|
+
/**
|
|
182
|
+
* Negate a guard or predicate — passes when `guard` returns `false`.
|
|
183
|
+
*
|
|
184
|
+
* @remarks
|
|
185
|
+
* Typed as `Guard<unknown>` because `Exclude<unknown, T>` is not useful; use
|
|
186
|
+
* {@link complementOf} when you need the narrowed `Exclude<TBase, TExcluded>`.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* const isNotNull = notOf(isNull)
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export declare function notOf(guard: (value: unknown) => boolean): Guard<unknown>;
|
|
194
|
+
/**
|
|
195
|
+
* Build a guard for `Exclude<TBase, TExcluded>` — accepts values that pass
|
|
196
|
+
* `base` but not `excluded`.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* const isNonEmpty = complementOf(isString, isEmptyString)
|
|
201
|
+
* isNonEmpty('hi') // true
|
|
202
|
+
* isNonEmpty('') // false
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export declare function complementOf<TBase, TExcluded extends TBase>(base: Guard<TBase>, excluded: Guard<TExcluded> | ((value: TBase) => value is TExcluded)): Guard<Exclude<TBase, TExcluded>>;
|
|
206
|
+
/**
|
|
207
|
+
* Build a guard that accepts values matching at least one of the provided
|
|
208
|
+
* guards — the variadic form of {@link orOf}.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```ts
|
|
212
|
+
* const isStringOrBoolean = unionOf(isString, isBoolean)
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
export declare function unionOf<const Gs extends ReadonlyArray<Guard<unknown>>>(...guards: Gs): Guard<GuardType<Gs[number]>>;
|
|
216
|
+
export declare function unionOf(...predicates: ReadonlyArray<(value: unknown) => boolean>): Guard<unknown>;
|
|
217
|
+
/**
|
|
218
|
+
* Build a guard that accepts values matching ALL of the provided guards — the
|
|
219
|
+
* variadic form of {@link andOf}.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```ts
|
|
223
|
+
* const isNonEmpty = intersectionOf(isString, isNonEmptyString)
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export declare function intersectionOf<const Gs extends ReadonlyArray<Guard<unknown>>>(...guards: Gs): Guard<IntersectionFromGuards<Gs>>;
|
|
227
|
+
export declare function intersectionOf(...predicates: ReadonlyArray<(value: unknown) => boolean>): Guard<unknown>;
|
|
228
|
+
/**
|
|
229
|
+
* Refine a base guard with an additional predicate that runs only when the base
|
|
230
|
+
* passes.
|
|
231
|
+
*
|
|
232
|
+
* @remarks
|
|
233
|
+
* The predicate receives a value already narrowed to `T`. When the predicate is
|
|
234
|
+
* itself a type guard (`value is U`), the result narrows to `Guard<U>` — it
|
|
235
|
+
* passes only when the value is genuinely a `U`, so the narrowing is sound. Per
|
|
236
|
+
* §14 the returned guard never throws: if `predicate` throws, the throw is
|
|
237
|
+
* contained and the guard reports a non-match.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```ts
|
|
241
|
+
* const isPositive = whereOf(isNumber, (n) => n > 0)
|
|
242
|
+
* isPositive(5) // true
|
|
243
|
+
* isPositive(-1) // false
|
|
244
|
+
*
|
|
245
|
+
* // A narrowing predicate refines the result type to Guard<5>
|
|
246
|
+
* const isFive = whereOf(isNumber, (n): n is 5 => n === 5)
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
export declare function whereOf<T, U extends T>(base: Guard<T>, predicate: (value: T) => value is U): Guard<U>;
|
|
250
|
+
export declare function whereOf<T>(base: Guard<T>, predicate: (value: T) => boolean): Guard<T>;
|
|
251
|
+
/**
|
|
252
|
+
* Defer guard creation until first use by calling `thunk()` on every
|
|
253
|
+
* invocation.
|
|
254
|
+
*
|
|
255
|
+
* @remarks
|
|
256
|
+
* `thunk` is called on every guard call, not cached — this lets it close over a
|
|
257
|
+
* binding assigned *after* `lazyOf` is called, the primary use case for
|
|
258
|
+
* self-referential recursive guards. Per §14 a throw from `thunk` (or the guard
|
|
259
|
+
* it resolves to) is contained and reported as a non-match.
|
|
260
|
+
*
|
|
261
|
+
* A recursive guard built this way has no cycle/depth detection: a cyclic or
|
|
262
|
+
* pathologically deep input is stack-bounded — the overflow is contained and the
|
|
263
|
+
* guard returns `false` rather than throwing, but it is not validated correctly
|
|
264
|
+
* past that bound.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```ts
|
|
268
|
+
* type Tree = { value: number; children: Tree[] }
|
|
269
|
+
* let isTree: Guard<Tree>
|
|
270
|
+
* isTree = recordOf({ value: isNumber, children: arrayOf(lazyOf(() => isTree)) })
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
export declare function lazyOf<T>(thunk: () => Guard<T>): Guard<T>;
|
|
274
|
+
/**
|
|
275
|
+
* Build a guard that passes when the base passes AND the projection of the value
|
|
276
|
+
* satisfies the target guard. Still narrows to `T` (the base type) — the target
|
|
277
|
+
* check is a validity constraint on a derived view, not a type transformation.
|
|
278
|
+
*
|
|
279
|
+
* @remarks
|
|
280
|
+
* `project` is a plain `(value: T) => U`. Per §14 the returned guard never
|
|
281
|
+
* throws: a throw from `project` is contained and reported as a non-match.
|
|
282
|
+
* `target` is itself a Guard (already §14-bound), so it stays outside the
|
|
283
|
+
* contained region. (Unlike the reference implementation, there is no
|
|
284
|
+
* "curried projector" branch — a projection that legitimately returns a function
|
|
285
|
+
* would be double-invoked under that scheme. Compose explicitly if you need it.)
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* const isBounded = transformOf(
|
|
290
|
+
* isString,
|
|
291
|
+
* (s) => s.trim().length,
|
|
292
|
+
* whereOf(isNumber, (n) => n >= 1 && n <= 50),
|
|
293
|
+
* )
|
|
294
|
+
* isBounded('hello') // true
|
|
295
|
+
* isBounded('') // false
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export declare function transformOf<T, U>(base: Guard<T>, project: (value: T) => U, target: Guard<U>): Guard<T>;
|
|
299
|
+
export declare function transformOf<T>(base: Guard<T>, project: (value: T) => unknown, target: (value: unknown) => boolean): Guard<T>;
|
|
300
|
+
/**
|
|
301
|
+
* Build a guard that accepts finite numbers within an inclusive `[min, max]`
|
|
302
|
+
* range.
|
|
303
|
+
*
|
|
304
|
+
* @remarks
|
|
305
|
+
* Refines {@link isFiniteNumber} with the bound comparison, so `NaN` /
|
|
306
|
+
* `±Infinity` are rejected before any comparison runs. An absent bound never
|
|
307
|
+
* constrains that side. Reused for a number's own value AND, applied to a
|
|
308
|
+
* `.length`, for string and array length refinements — the single source of the
|
|
309
|
+
* bound logic shared by the compiled guard and parser (compilers.ts).
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```ts
|
|
313
|
+
* const inRange = boundsOf(1, 5)
|
|
314
|
+
* inRange(3) // true
|
|
315
|
+
* inRange(0) // false — below min
|
|
316
|
+
* inRange(6) // false — above max
|
|
317
|
+
*
|
|
318
|
+
* const atLeastTwo = boundsOf(2)
|
|
319
|
+
* atLeastTwo(2) // true — unbounded above
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
export declare function boundsOf(min?: number, max?: number): Guard<number>;
|
|
323
|
+
/**
|
|
324
|
+
* Build a guard that accepts strings matching a regular expression.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* const isHex = matchOf(/^[0-9a-f]+$/)
|
|
329
|
+
* isHex('1a2f') // true
|
|
330
|
+
* isHex('xyz') // false
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
export declare function matchOf(pattern: RegExp): Guard<string>;
|
|
334
|
+
/**
|
|
335
|
+
* Build a guard that accepts strings satisfying optional length and pattern
|
|
336
|
+
* refinements — `min` / `max` length and a `pattern`.
|
|
337
|
+
*
|
|
338
|
+
* @remarks
|
|
339
|
+
* Composes {@link isString} with {@link boundsOf} on the string's `.length` and
|
|
340
|
+
* an inline `pattern.test` (the same refinement {@link matchOf} performs). When all three options are absent it returns
|
|
341
|
+
* the bare {@link isString} guard (the unconstrained fast path), so an
|
|
342
|
+
* unrefined string leaf pays no wrapping cost. The single source of the string
|
|
343
|
+
* refinement shared by the compiled guard and parser (compilers.ts).
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* ```ts
|
|
347
|
+
* const isSlug = stringOf({ min: 1, max: 32, pattern: /^[a-z-]+$/ })
|
|
348
|
+
* isSlug('hello-world') // true
|
|
349
|
+
* isSlug('') // false — below min
|
|
350
|
+
* isSlug('Hello') // false — pattern miss
|
|
351
|
+
*
|
|
352
|
+
* stringOf() // identical to isString
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
export declare function stringOf(options?: {
|
|
356
|
+
min?: number;
|
|
357
|
+
max?: number;
|
|
358
|
+
pattern?: RegExp;
|
|
359
|
+
}): Guard<string>;
|
|
360
|
+
/**
|
|
361
|
+
* Extend a guard to also allow `null`.
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```ts
|
|
365
|
+
* const isNullableString = nullableOf(isString)
|
|
366
|
+
* isNullableString('hi') // true
|
|
367
|
+
* isNullableString(null) // true
|
|
368
|
+
* isNullableString(42) // false
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
export declare function nullableOf<T>(guard: Guard<T>): Guard<T | null>;
|
|
372
|
+
/**
|
|
373
|
+
* Extend a guard to also allow `undefined` — the optional counterpart of
|
|
374
|
+
* {@link nullableOf}.
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* ```ts
|
|
378
|
+
* const isOptionalString = optionalOf(isString)
|
|
379
|
+
* isOptionalString('hi') // true
|
|
380
|
+
* isOptionalString(undefined) // true
|
|
381
|
+
* isOptionalString(null) // false
|
|
382
|
+
* ```
|
|
383
|
+
*/
|
|
384
|
+
export declare function optionalOf<T>(guard: Guard<T>): Guard<T | undefined>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ContractInterface, ContractShape, Guard, Infer, JSONSchema, Parser, RandomFunction } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate that a {@link ContractShape} tree is well-formed — a pure recursive
|
|
4
|
+
* prepass run before compilation.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Fail-fast, per AGENTS §12: a malformed shape is a programmer error, so this
|
|
8
|
+
* throws a plain `Error` immediately rather than surfacing as a silently-wrong
|
|
9
|
+
* guard, parser, schema, or generator later. Checks, recursively:
|
|
10
|
+
*
|
|
11
|
+
* - An {@link OptionalShape} is only legal as a direct object-property value —
|
|
12
|
+
* `optionalShape` wrapping an array item, a union variant, another
|
|
13
|
+
* optional/nullable's inner shape, `additionalProperties`, or the top-level
|
|
14
|
+
* shape all throw. An object property IS the one legal placement: its value
|
|
15
|
+
* is unwrapped to `.inner` before recursing, so `.inner` itself is validated
|
|
16
|
+
* as a normal (non-optional-wrapping) shape.
|
|
17
|
+
* - A {@link UnionShape} needs at least one variant; a {@link LiteralShape}
|
|
18
|
+
* needs at least one value and rejects non-finite (`NaN` / `Infinity` /
|
|
19
|
+
* `-Infinity`) number values.
|
|
20
|
+
* - A bounded {@link StringShape} / {@link NumberShape} / {@link ArrayShape}
|
|
21
|
+
* needs `min <= max` when both are set.
|
|
22
|
+
* - An integer {@link NumberShape} (`integer: true`) needs a non-empty integer
|
|
23
|
+
* range: `Math.ceil(min ?? -Infinity) <= Math.floor(max ?? Infinity)`.
|
|
24
|
+
* - `null` / `json` / `raw` / `boolean` are always-valid leaves. Recursion
|
|
25
|
+
* continues into array items, object properties (and `additionalProperties`
|
|
26
|
+
* when it is a shape), union variants, and optional/nullable inner shapes.
|
|
27
|
+
*
|
|
28
|
+
* @param shape - The shape to validate
|
|
29
|
+
* @throws {Error} When the shape is malformed
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateShape(shape: ContractShape): void;
|
|
32
|
+
/**
|
|
33
|
+
* Compile a {@link ContractShape} into a JSON Schema document.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Object shapes emit `additionalProperties: false` (unless opened) and list only
|
|
37
|
+
* required keys in `required`; nullable shapes emit an `anyOf` with `{ type:
|
|
38
|
+
* 'null' }`. Emission only — it never inspects a runtime value.
|
|
39
|
+
*
|
|
40
|
+
* @param shape - The shape to compile
|
|
41
|
+
* @returns The emitted JSON Schema
|
|
42
|
+
*/
|
|
43
|
+
export declare function compileSchema(shape: ContractShape): JSONSchema;
|
|
44
|
+
/**
|
|
45
|
+
* Compile a {@link ContractShape} into a runtime type guard.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* Reuses the combinators: `literalOf` for literals, `arrayOf` for arrays,
|
|
49
|
+
* `recordOf` for closed objects, `unionOf` for unions, `nullableOf` for nullable,
|
|
50
|
+
* and `whereOf` for constraint refinement. Like every guard it is total — it
|
|
51
|
+
* never throws (AGENTS §14).
|
|
52
|
+
*
|
|
53
|
+
* @param shape - The shape to compile
|
|
54
|
+
* @returns A guard narrowing to the shape's inferred type
|
|
55
|
+
*/
|
|
56
|
+
export declare function compileGuard(shape: ContractShape): Guard<unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Compile a {@link ContractShape} into an input parser.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* Reuses the leaf parsers (`parseString` / `parseInteger` / `parseNumber` /
|
|
62
|
+
* `parseBoolean` / `parseRecord`) and coerces structurally. An object fails as a
|
|
63
|
+
* whole on any required-field failure; a union returns a guard-valid value
|
|
64
|
+
* unchanged, otherwise the first variant that both parses and guards wins.
|
|
65
|
+
*
|
|
66
|
+
* After coercing a leaf, it re-applies that leaf's REFINEMENTS through the same
|
|
67
|
+
* combinators `compileGuard` uses — `stringOf` for a string's length/pattern and
|
|
68
|
+
* `boundsOf` for a number's value and an array's length — so a value that coerces
|
|
69
|
+
* but violates a bound parses to `undefined`. The result is full parse↔guard
|
|
70
|
+
* soundness (AGENTS §14): a non-`undefined` parse always satisfies the contract's
|
|
71
|
+
* `is`, refinements included.
|
|
72
|
+
*
|
|
73
|
+
* @param shape - The shape to compile
|
|
74
|
+
* @returns A parser yielding the shape's inferred type or `undefined`
|
|
75
|
+
*/
|
|
76
|
+
export declare function compileParser(shape: ContractShape): Parser<unknown>;
|
|
77
|
+
/**
|
|
78
|
+
* Compile a {@link ContractShape} into a deterministic seed value.
|
|
79
|
+
*
|
|
80
|
+
* @remarks
|
|
81
|
+
* The same shape and the same `random` source always produce the same value, so
|
|
82
|
+
* seed data is reproducible. Defaults to a {@link seededRandom} source seeded
|
|
83
|
+
* from the wall clock when none is supplied. Throws on a degenerate empty
|
|
84
|
+
* `literalShape` / `unionShape`, on a pattern-constrained `stringShape` whose
|
|
85
|
+
* generated sample cannot satisfy the pattern, or on a `rawShape` (its embedded
|
|
86
|
+
* schema is arbitrary and cannot be auto-generated) — a programmer error that
|
|
87
|
+
* cannot generate a value (AGENTS §12). `createContract` runs
|
|
88
|
+
* {@link validateShape} first, so a degenerate `literalShape` / `unionShape` /
|
|
89
|
+
* bounded shape is normally caught there; these throws remain here as defense
|
|
90
|
+
* for standalone `compileGenerator` use.
|
|
91
|
+
*
|
|
92
|
+
* @param shape - The shape to generate from
|
|
93
|
+
* @param random - A seeded random source (defaults to `seededRandom(Date.now())`)
|
|
94
|
+
* @returns A value matching the shape
|
|
95
|
+
*/
|
|
96
|
+
export declare function compileGenerator(shape: ContractShape, random?: RandomFunction): unknown;
|
|
97
|
+
/**
|
|
98
|
+
* Compile a {@link ContractShape} into a {@link ContractInterface} — the four
|
|
99
|
+
* lockstep outputs from one declaration.
|
|
100
|
+
*
|
|
101
|
+
* @remarks
|
|
102
|
+
* Runs {@link validateShape} first — a malformed shape throws immediately
|
|
103
|
+
* rather than compiling into a silently-wrong contract (AGENTS §12). Then
|
|
104
|
+
* precompiles the schema, guard, and parser once; `generate` walks the shape
|
|
105
|
+
* per call with the supplied random source.
|
|
106
|
+
*
|
|
107
|
+
* @param shape - The shape to compile
|
|
108
|
+
* @returns A contract bundling `schema` / `is` / `parse` / `generate`
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const user = createContract(objectShape({ name: stringShape(), age: integerShape() }))
|
|
113
|
+
* user.is({ name: 'Ada', age: 36 }) // true
|
|
114
|
+
* user.parse({ name: 'Ada', age: '36' }) // { name: 'Ada', age: 36 }
|
|
115
|
+
* user.schema // { type: 'object', properties: { … }, … }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export declare function createContract<S extends ContractShape>(shape: S): ContractInterface<Infer<S>>;
|
|
119
|
+
export declare function createContract(shape: ContractShape): ContractInterface<unknown>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JSONSchemaType } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The seven standard JSON Schema `type` names, frozen.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The runtime source of truth for the {@link JSONSchemaType} vocabulary. Compose
|
|
7
|
+
* it with the shipped primitives instead of reaching for a bespoke guard:
|
|
8
|
+
* `literalOf(...JSON_SCHEMA_TYPES)` is the guard, and
|
|
9
|
+
* `parseEnum(value, JSON_SCHEMA_TYPES)` / `parseEnumField(record, path, JSON_SCHEMA_TYPES)`
|
|
10
|
+
* is the parser.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { JSON_SCHEMA_TYPES, literalOf, parseEnumField } from '@src/core'
|
|
15
|
+
*
|
|
16
|
+
* const isSchemaType = literalOf(...JSON_SCHEMA_TYPES) // Guard<JSONSchemaType>
|
|
17
|
+
* parseEnumField(schema, 'type', JSON_SCHEMA_TYPES) // JSONSchemaType | undefined
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare const JSON_SCHEMA_TYPES: readonly JSONSchemaType[];
|