@orkestrel/validator 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/README.md +948 -0
- package/dist/arrays.d.ts +168 -0
- package/dist/collections.d.ts +72 -0
- package/dist/constants.d.ts +13 -0
- package/dist/emptiness.d.ts +119 -0
- package/dist/errors.d.ts +29 -0
- package/dist/factories.d.ts +24 -0
- package/dist/functions.d.ts +85 -0
- package/dist/guards.d.ts +297 -0
- package/dist/helpers.d.ts +223 -0
- package/dist/index.cjs +781 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +781 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives.d.ts +219 -0
- package/dist/types.d.ts +256 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
# @orkestrel/validator
|
|
2
|
+
|
|
3
|
+
> Runtime validation toolkit for TypeScript — zero dependencies, fully typed.
|
|
4
|
+
|
|
5
|
+
`@orkestrel/validator` gives you two independent tools that work together:
|
|
6
|
+
|
|
7
|
+
- **String validation** — a declarative rule engine for validating user input. Define which rules apply, call `validateInput`, and get back a structured result with every failing rule and its message.
|
|
8
|
+
- **Type guards** — a composable system for narrowing `unknown` values to precise TypeScript types at runtime. Over 60 built-in `is*` guards plus a full set of builder functions for constructing guards over objects, arrays, tuples, enums, and more.
|
|
9
|
+
|
|
10
|
+
Both axes are ESM-first, tree-shakeable, and produce no side effects.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install @orkestrel/validator
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
All exports are available from the package root:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { validateInput, isString, objectOf } from '@orkestrel/validator'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## String Validation
|
|
29
|
+
|
|
30
|
+
Use the rule engine when you need to validate free-form user input — form fields, query parameters, CLI arguments — and want structured error feedback.
|
|
31
|
+
|
|
32
|
+
### Defining rules
|
|
33
|
+
|
|
34
|
+
Describe constraints as a plain object:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import type { ValidationRules } from '@orkestrel/validator'
|
|
38
|
+
|
|
39
|
+
const usernameRules: ValidationRules = {
|
|
40
|
+
required: true,
|
|
41
|
+
minimum: 3,
|
|
42
|
+
maximum: 20,
|
|
43
|
+
alphanumeric: true,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const emailRules: ValidationRules = {
|
|
47
|
+
required: true,
|
|
48
|
+
email: true,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const commentRules: ValidationRules = {
|
|
52
|
+
required: true,
|
|
53
|
+
minimum: 1,
|
|
54
|
+
maximum: 500,
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Validating input
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { validateInput } from '@orkestrel/validator'
|
|
62
|
+
|
|
63
|
+
const result = validateInput('Ada123', usernameRules)
|
|
64
|
+
|
|
65
|
+
if (result.valid) {
|
|
66
|
+
// result.errors is []
|
|
67
|
+
} else {
|
|
68
|
+
for (const error of result.errors) {
|
|
69
|
+
console.log(error.rule, error.message)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`validateInput` evaluates every active rule and collects **all** failures — it does not stop at the first one. The returned `ValidationResult` always has a `valid` boolean and an `errors` array.
|
|
75
|
+
|
|
76
|
+
### Quick pass/fail check
|
|
77
|
+
|
|
78
|
+
When you only need a boolean and don't need the error list:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { testInput } from '@orkestrel/validator'
|
|
82
|
+
|
|
83
|
+
if (testInput('Ada123', usernameRules)) {
|
|
84
|
+
// input is valid
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Built-in rules
|
|
89
|
+
|
|
90
|
+
| Rule | Value | Fails when |
|
|
91
|
+
| -------------- | ------------------- | -------------------------------------------------------- |
|
|
92
|
+
| `required` | `true` | The trimmed input is empty |
|
|
93
|
+
| `minimum` | `number ≥ 0` | `input.length < minimum` |
|
|
94
|
+
| `maximum` | `number ≥ 0` | `input.length > maximum` |
|
|
95
|
+
| `pattern` | regex string | Input does not match the compiled pattern |
|
|
96
|
+
| `email` | `true` | Input does not look like `local@domain.tld` |
|
|
97
|
+
| `url` | `true` | Input does not begin with `http://` or `https://` |
|
|
98
|
+
| `numeric` | `true` | Input is not an integer or decimal (optionally negative) |
|
|
99
|
+
| `integer` | `true` | Input is not a whole integer (optionally negative) |
|
|
100
|
+
| `alphanumeric` | `true` | Input contains anything other than letters and digits |
|
|
101
|
+
| `custom` | `ValidatorFunction` | The function returns `false` or an error string |
|
|
102
|
+
|
|
103
|
+
Rules set to `false` or omitted are skipped. Rules are always evaluated in this order:
|
|
104
|
+
`required → minimum → maximum → pattern → email → url → numeric → integer → alphanumeric → custom`
|
|
105
|
+
|
|
106
|
+
### Default error messages
|
|
107
|
+
|
|
108
|
+
Each built-in rule produces a default message when it fails:
|
|
109
|
+
|
|
110
|
+
| Rule | Default message |
|
|
111
|
+
| -------------- | ---------------------------------------- |
|
|
112
|
+
| `required` | `'This field is required'` |
|
|
113
|
+
| `minimum` | `'Must be at least N characters'` |
|
|
114
|
+
| `maximum` | `'Must be at most N characters'` |
|
|
115
|
+
| `pattern` | `'Must match pattern: <pattern>'` |
|
|
116
|
+
| `email` | `'Must be a valid email address'` |
|
|
117
|
+
| `url` | `'Must be a valid URL'` |
|
|
118
|
+
| `numeric` | `'Must be a numeric value'` |
|
|
119
|
+
| `integer` | `'Must be an integer'` |
|
|
120
|
+
| `alphanumeric` | `'Must contain only letters and digits'` |
|
|
121
|
+
|
|
122
|
+
### Custom validators
|
|
123
|
+
|
|
124
|
+
Every rule slot accepts a `ValidatorFunction` instead of its canonical value type. The function replaces the built-in check entirely for that slot.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
type ValidatorFunction = (input: string) => boolean | string
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Return `true` when the input passes. Return `false` or a message string when it fails.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const rules: ValidationRules = {
|
|
134
|
+
// Replace the built-in minimum check with custom logic
|
|
135
|
+
minimum: (input) => input.startsWith('z') || 'Must start with z',
|
|
136
|
+
|
|
137
|
+
// The custom slot has no built-in behaviour — always use a function
|
|
138
|
+
custom: (input) => input !== 'forbidden' || 'That value is not allowed',
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Using a function on a rule slot still uses that slot's position in the evaluation order — a `minimum` function runs in the `minimum` position.
|
|
143
|
+
|
|
144
|
+
### Collecting multiple errors
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const result = validateInput('', {
|
|
148
|
+
required: true,
|
|
149
|
+
minimum: 3,
|
|
150
|
+
email: true,
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// {
|
|
154
|
+
// valid: false,
|
|
155
|
+
// errors: [
|
|
156
|
+
// { rule: 'required', message: 'This field is required' },
|
|
157
|
+
// { rule: 'minimum', message: 'Must be at least 3 characters' },
|
|
158
|
+
// { rule: 'email', message: 'Must be a valid email address' },
|
|
159
|
+
// ]
|
|
160
|
+
// }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Validating rule objects at runtime
|
|
164
|
+
|
|
165
|
+
If rules arrive from an untrusted source (config file, API), assert them before use:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { assertValidationRules } from '@orkestrel/validator'
|
|
169
|
+
|
|
170
|
+
assertValidationRules(untrustedRules) // throws ValidatorError when invalid
|
|
171
|
+
// now untrustedRules is narrowed to ValidationRules
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`assertValidationRules` checks:
|
|
175
|
+
|
|
176
|
+
- All keys are supported rule names
|
|
177
|
+
- All values are the correct kind for their rule (`boolean` for flag rules, `number ≥ 0` for `minimum`/`maximum`, string for `pattern`)
|
|
178
|
+
- `minimum ≤ maximum` when both are provided as numbers
|
|
179
|
+
- `pattern` compiles as a valid regular expression
|
|
180
|
+
|
|
181
|
+
### ValidatorError
|
|
182
|
+
|
|
183
|
+
Invalid rule configuration throws a `ValidatorError` — a subclass of `Error` with two extra fields:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { ValidatorError, isValidatorError } from '@orkestrel/validator'
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
assertValidationRules({ minimum: 10, maximum: 3 })
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (isValidatorError(error)) {
|
|
192
|
+
console.log(error.code) // 'INVALID_RULE_VALUE'
|
|
193
|
+
console.log(error.message) // 'Minimum cannot be greater than maximum'
|
|
194
|
+
console.log(error.context.rule) // 'minimum'
|
|
195
|
+
console.log(error.context.value) // 10
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The three error codes:
|
|
201
|
+
|
|
202
|
+
| Code | When thrown |
|
|
203
|
+
| -------------------- | -------------------------------------------------------------------- |
|
|
204
|
+
| `INVALID_RULES` | The rule object has unsupported keys or wrong value kinds |
|
|
205
|
+
| `INVALID_RULE_VALUE` | A numeric rule is out of range (`minimum > maximum`, negative value) |
|
|
206
|
+
| `INVALID_PATTERN` | The `pattern` string is not a valid regular expression |
|
|
207
|
+
|
|
208
|
+
Use `isValidatorError` in `catch` blocks to distinguish package errors from unexpected throws.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Type Guards
|
|
213
|
+
|
|
214
|
+
Use type guards when you need to narrow `unknown` values — incoming API responses, `JSON.parse` output, event payloads, dynamic config — to specific TypeScript types.
|
|
215
|
+
|
|
216
|
+
All guards follow the same contract:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
// Returns true and narrows the type — never throws
|
|
220
|
+
isString(value) // value is string
|
|
221
|
+
isNumber(value) // value is number
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Primitive guards
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import {
|
|
228
|
+
isNull,
|
|
229
|
+
isUndefined,
|
|
230
|
+
isDefined,
|
|
231
|
+
isString,
|
|
232
|
+
isNumber,
|
|
233
|
+
isBoolean,
|
|
234
|
+
isBigInt,
|
|
235
|
+
isSymbol,
|
|
236
|
+
isFunction,
|
|
237
|
+
isDate,
|
|
238
|
+
isRegExp,
|
|
239
|
+
isError,
|
|
240
|
+
isPromise,
|
|
241
|
+
isPromiseLike,
|
|
242
|
+
isIterable,
|
|
243
|
+
isAsyncIterator,
|
|
244
|
+
isArrayBuffer,
|
|
245
|
+
isSharedArrayBuffer,
|
|
246
|
+
} from '@orkestrel/validator'
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
| Guard | Narrows to | Notes |
|
|
250
|
+
| ------------------------ | ------------------------ | ----------------------------------------- |
|
|
251
|
+
| `isNull(v)` | `null` | Strict `=== null` |
|
|
252
|
+
| `isUndefined(v)` | `undefined` | Strict `=== undefined` |
|
|
253
|
+
| `isDefined(v)` | `T` | Excludes both `null` and `undefined` |
|
|
254
|
+
| `isString(v)` | `string` | `typeof === 'string'` |
|
|
255
|
+
| `isNumber(v)` | `number` | Includes `NaN` and `Infinity` |
|
|
256
|
+
| `isBoolean(v)` | `boolean` | |
|
|
257
|
+
| `isBigInt(v)` | `bigint` | |
|
|
258
|
+
| `isSymbol(v)` | `symbol` | |
|
|
259
|
+
| `isFunction(v)` | `AnyFunction` | `typeof === 'function'` |
|
|
260
|
+
| `isDate(v)` | `Date` | `instanceof Date` |
|
|
261
|
+
| `isRegExp(v)` | `RegExp` | `instanceof RegExp` |
|
|
262
|
+
| `isError(v)` | `Error` | `instanceof Error` |
|
|
263
|
+
| `isPromise(v)` | `Promise<T>` | Native `instanceof Promise` only |
|
|
264
|
+
| `isPromiseLike(v)` | `Promise<T>` or thenable | Checks `then`, `catch`, `finally` methods |
|
|
265
|
+
| `isIterable(v)` | `Iterable<T>` | Strings are iterable |
|
|
266
|
+
| `isAsyncIterator(v)` | `AsyncIterable<T>` | Checks `Symbol.asyncIterator` |
|
|
267
|
+
| `isArrayBuffer(v)` | `ArrayBuffer` | |
|
|
268
|
+
| `isSharedArrayBuffer(v)` | `SharedArrayBuffer` | Feature-checks availability first |
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
const raw: unknown = JSON.parse(text)
|
|
272
|
+
|
|
273
|
+
if (isString(raw)) {
|
|
274
|
+
// raw: string
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// isDefined removes both null and undefined
|
|
278
|
+
function process<T>(value: T | null | undefined) {
|
|
279
|
+
if (isDefined(value)) {
|
|
280
|
+
// value: T
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Object and collection guards
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import { isObject, isRecord, isMap, isSet, isWeakMap, isWeakSet } from '@orkestrel/validator'
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
| Guard | Narrows to | Notes |
|
|
292
|
+
| -------------- | -------------------------- | ------------------------------------- |
|
|
293
|
+
| `isObject(v)` | `object` | Any non-null object, including arrays |
|
|
294
|
+
| `isRecord(v)` | `Record<string, unknown>` | Non-null, non-array object |
|
|
295
|
+
| `isMap(v)` | `ReadonlyMap<K, V>` | `instanceof Map` |
|
|
296
|
+
| `isSet(v)` | `ReadonlySet<T>` | `instanceof Set` |
|
|
297
|
+
| `isWeakMap(v)` | `WeakMap<object, unknown>` | |
|
|
298
|
+
| `isWeakSet(v)` | `WeakSet<object>` | |
|
|
299
|
+
|
|
300
|
+
`isRecord` is the right choice for plain objects — it excludes arrays:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
isObject([]) // true — arrays are objects
|
|
304
|
+
isRecord([]) // false — arrays excluded
|
|
305
|
+
isRecord({}) // true
|
|
306
|
+
isRecord(null) // false
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Array and typed array guards
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
import {
|
|
313
|
+
isArray,
|
|
314
|
+
isDataView,
|
|
315
|
+
isArrayBufferView,
|
|
316
|
+
isUint8Array,
|
|
317
|
+
isInt32Array,
|
|
318
|
+
isFloat64Array,
|
|
319
|
+
// ... all 11 typed-array guards
|
|
320
|
+
} from '@orkestrel/validator'
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`isArray` wraps `Array.isArray`. For element-level checking use `arrayOf` from the guard builders section.
|
|
324
|
+
|
|
325
|
+
`isArrayBufferView` accepts any typed array or `DataView` via `ArrayBuffer.isView`.
|
|
326
|
+
|
|
327
|
+
All 11 typed-array guards: `isInt8Array`, `isUint8Array`, `isUint8ClampedArray`, `isInt16Array`, `isUint16Array`, `isInt32Array`, `isUint32Array`, `isFloat32Array`, `isFloat64Array`, `isBigInt64Array`, `isBigUint64Array`.
|
|
328
|
+
|
|
329
|
+
### Emptiness guards
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import {
|
|
333
|
+
isEmptyString,
|
|
334
|
+
isEmptyArray,
|
|
335
|
+
isEmptyObject,
|
|
336
|
+
isEmptyMap,
|
|
337
|
+
isEmptySet,
|
|
338
|
+
isNonEmptyString,
|
|
339
|
+
isNonEmptyArray,
|
|
340
|
+
isNonEmptyObject,
|
|
341
|
+
isNonEmptyMap,
|
|
342
|
+
isNonEmptySet,
|
|
343
|
+
} from '@orkestrel/validator'
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Empty:**
|
|
347
|
+
|
|
348
|
+
| Guard | Narrows to |
|
|
349
|
+
| ------------------ | --------------------------------- |
|
|
350
|
+
| `isEmptyString(v)` | `''` |
|
|
351
|
+
| `isEmptyArray(v)` | `readonly []` |
|
|
352
|
+
| `isEmptyObject(v)` | `Record<string \| symbol, never>` |
|
|
353
|
+
| `isEmptyMap(v)` | `ReadonlyMap<never, never>` |
|
|
354
|
+
| `isEmptySet(v)` | `ReadonlySet<never>` |
|
|
355
|
+
|
|
356
|
+
**Non-empty:**
|
|
357
|
+
|
|
358
|
+
| Guard | Narrows to |
|
|
359
|
+
| --------------------- | ----------------------------------- |
|
|
360
|
+
| `isNonEmptyString(v)` | `string` (length > 0) |
|
|
361
|
+
| `isNonEmptyArray(v)` | `readonly [T, ...T[]]` |
|
|
362
|
+
| `isNonEmptyObject(v)` | `Record<string \| symbol, unknown>` |
|
|
363
|
+
| `isNonEmptyMap(v)` | `ReadonlyMap<K, V>` |
|
|
364
|
+
| `isNonEmptySet(v)` | `ReadonlySet<T>` |
|
|
365
|
+
|
|
366
|
+
`isEmptyObject` counts both string keys and **enumerable symbol keys** — an object with only a symbol property is not considered empty:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
isEmptyObject({}) // true
|
|
370
|
+
isEmptyObject({ [Symbol('x')]: true }) // false
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Function guards
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
import {
|
|
377
|
+
isAsyncFunction,
|
|
378
|
+
isGeneratorFunction,
|
|
379
|
+
isAsyncGeneratorFunction,
|
|
380
|
+
isZeroArg,
|
|
381
|
+
isZeroArgAsync,
|
|
382
|
+
isZeroArgGenerator,
|
|
383
|
+
isZeroArgAsyncGenerator,
|
|
384
|
+
} from '@orkestrel/validator'
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
| Guard | Passes when |
|
|
388
|
+
| ------------------------------ | ---------------------------------- |
|
|
389
|
+
| `isZeroArg(fn)` | `fn.length === 0` |
|
|
390
|
+
| `isAsyncFunction(fn)` | Native `async function` |
|
|
391
|
+
| `isGeneratorFunction(fn)` | `function*` |
|
|
392
|
+
| `isAsyncGeneratorFunction(fn)` | `async function*` |
|
|
393
|
+
| `isZeroArgAsync(fn)` | Async and zero arguments |
|
|
394
|
+
| `isZeroArgGenerator(fn)` | Generator and zero arguments |
|
|
395
|
+
| `isZeroArgAsyncGenerator(fn)` | Async generator and zero arguments |
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
isAsyncFunction(async () => true) // true
|
|
399
|
+
isAsyncFunction(() => Promise.resolve(true)) // false — sync, returns a Promise
|
|
400
|
+
isGeneratorFunction(function* () {
|
|
401
|
+
yield 1
|
|
402
|
+
}) // true
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
> **Note:** Detection uses `constructor.name`. Minifiers that rename function constructors will break these guards.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Guard Builders
|
|
410
|
+
|
|
411
|
+
Guard builders compose primitive guards into guards for complex shapes. They are the primary API for narrowing structured data like API responses.
|
|
412
|
+
|
|
413
|
+
### Building object schemas — `objectOf`
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
import { objectOf, isString, isNumber } from '@orkestrel/validator'
|
|
417
|
+
|
|
418
|
+
const isUser = objectOf({ id: isString, age: isNumber })
|
|
419
|
+
|
|
420
|
+
const raw: unknown = await fetchUser()
|
|
421
|
+
if (isUser(raw)) {
|
|
422
|
+
// raw: Readonly<{ id: string; age: number }>
|
|
423
|
+
console.log(raw.id, raw.age)
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
`objectOf` is **exact** by default: it rejects any value with extra string keys not in the shape. Symbol keys on the value are silently ignored.
|
|
428
|
+
|
|
429
|
+
#### Optional keys
|
|
430
|
+
|
|
431
|
+
Pass an array of key names to make those keys optional:
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
const isUser = objectOf(
|
|
435
|
+
{ id: isString, role: isString, note: isString },
|
|
436
|
+
['note'], // note may be absent
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
isUser({ id: 'u1', role: 'admin' }) // true
|
|
440
|
+
isUser({ id: 'u1', role: 'admin', note: 'hi' }) // true
|
|
441
|
+
isUser({ id: 'u1', role: 'admin', note: 1 }) // false — note fails its guard
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Pass `true` to make every key optional:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
const isPartialUser = objectOf({ id: isString, age: isNumber }, true)
|
|
448
|
+
isPartialUser({}) // true
|
|
449
|
+
isPartialUser({ id: 'u1' }) // true
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
#### `recordOf`
|
|
453
|
+
|
|
454
|
+
Identical signature to `objectOf`. Use `recordOf` when the value is a dictionary-style record rather than a structured entity — purely a naming convention for readability.
|
|
455
|
+
|
|
456
|
+
### Arrays — `arrayOf`
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import { arrayOf, isString } from '@orkestrel/validator'
|
|
460
|
+
|
|
461
|
+
const isStrings = arrayOf(isString)
|
|
462
|
+
isStrings(['a', 'b']) // true
|
|
463
|
+
isStrings(['a', 1]) // false
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Tuples — `tupleOf`
|
|
467
|
+
|
|
468
|
+
Validates a fixed-length array with per-index guards:
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
import { tupleOf, isString, isNumber } from '@orkestrel/validator'
|
|
472
|
+
|
|
473
|
+
const isEntry = tupleOf(isString, isNumber)
|
|
474
|
+
isEntry(['id', 42]) // true
|
|
475
|
+
isEntry(['id', 'x']) // false — second element fails
|
|
476
|
+
isEntry(['id']) // false — length mismatch
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Literals and enums — `literalOf`, `enumOf`
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
import { literalOf, enumOf } from '@orkestrel/validator'
|
|
483
|
+
|
|
484
|
+
// Narrow to a fixed set of literal values
|
|
485
|
+
const isRole = literalOf('user', 'admin', 'guest')
|
|
486
|
+
isRole('admin') // true
|
|
487
|
+
isRole('owner') // false
|
|
488
|
+
|
|
489
|
+
// Narrow to a TypeScript enum
|
|
490
|
+
enum Status {
|
|
491
|
+
Idle,
|
|
492
|
+
Busy,
|
|
493
|
+
}
|
|
494
|
+
const isStatus = enumOf(Status)
|
|
495
|
+
isStatus(Status.Idle) // true
|
|
496
|
+
isStatus(0) // true — numeric enum value
|
|
497
|
+
isStatus('Idle') // false — reverse-mapped names are not values
|
|
498
|
+
|
|
499
|
+
// String enum
|
|
500
|
+
const isFlag = enumOf({ active: 'ACTIVE', paused: 'PAUSED' })
|
|
501
|
+
isFlag('ACTIVE') // true
|
|
502
|
+
isFlag('active') // false — keys are not values
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Sets and Maps — `setOf`, `mapOf`
|
|
506
|
+
|
|
507
|
+
```ts
|
|
508
|
+
import { setOf, mapOf, isString, isNumber } from '@orkestrel/validator'
|
|
509
|
+
|
|
510
|
+
const isTagSet = setOf(isString)
|
|
511
|
+
const isScores = mapOf(isString, isNumber)
|
|
512
|
+
|
|
513
|
+
isTagSet(new Set(['a', 'b'])) // true
|
|
514
|
+
isTagSet(new Set(['a', 1])) // false
|
|
515
|
+
isScores(new Map([['alice', 98]])) // true
|
|
516
|
+
isScores(new Map([['alice', '98']])) // false
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Iterables — `iterableOf`
|
|
520
|
+
|
|
521
|
+
Accepts any value with `Symbol.iterator` — arrays, sets, generators — and validates every yielded element:
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
import { iterableOf, isNumber } from '@orkestrel/validator'
|
|
525
|
+
|
|
526
|
+
const isNumbers = iterableOf(isNumber)
|
|
527
|
+
isNumbers([1, 2, 3]) // true
|
|
528
|
+
isNumbers(new Set([1, 2])) // true
|
|
529
|
+
isNumbers([1, '2']) // false
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Keys and instance checks — `keyOf`, `instanceOf`
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
import { keyOf, instanceOf } from '@orkestrel/validator'
|
|
536
|
+
|
|
537
|
+
// Accept only keys present on a specific object
|
|
538
|
+
const COLORS = { red: '#f00', blue: '#00f', green: '#0f0' } as const
|
|
539
|
+
const isColorKey = keyOf(COLORS)
|
|
540
|
+
isColorKey('red') // true
|
|
541
|
+
isColorKey('yellow') // false
|
|
542
|
+
|
|
543
|
+
// Accept instances of a class
|
|
544
|
+
class ApiError extends Error {
|
|
545
|
+
constructor(
|
|
546
|
+
readonly status: number,
|
|
547
|
+
message: string,
|
|
548
|
+
) {
|
|
549
|
+
super(message)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const isApiError = instanceOf(ApiError)
|
|
553
|
+
isApiError(new ApiError(404, 'Not found')) // true
|
|
554
|
+
isApiError(new Error('generic')) // false
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### Shape transforms — `pickOf`, `omitOf`
|
|
558
|
+
|
|
559
|
+
Build a new guard shape by selecting or removing keys from an existing one. The result is a `GuardsShape` — pass it to `objectOf` or `recordOf`.
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
import { pickOf, omitOf, objectOf, isString, isNumber } from '@orkestrel/validator'
|
|
563
|
+
|
|
564
|
+
const userShape = { id: isString, age: isNumber, name: isString, role: isString }
|
|
565
|
+
|
|
566
|
+
// Keep only the keys you need
|
|
567
|
+
const isPublicUser = objectOf(pickOf(userShape, ['id', 'name']))
|
|
568
|
+
isPublicUser({ id: 'u1', name: 'Ada' }) // true
|
|
569
|
+
isPublicUser({ id: 'u1', name: 'Ada', age: 30 }) // false — exact shape, extra key rejected
|
|
570
|
+
|
|
571
|
+
// Remove keys you don't want
|
|
572
|
+
const isUserWithoutAge = objectOf(omitOf(userShape, ['age']))
|
|
573
|
+
isUserWithoutAge({ id: 'u1', name: 'Ada', role: 'admin' }) // true
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Logical Combinators
|
|
579
|
+
|
|
580
|
+
### Refinement — `whereOf`
|
|
581
|
+
|
|
582
|
+
The most common combinator. Refines a base guard with an additional predicate. The predicate receives a typed value (the base guard already narrowed it), so TypeScript provides full autocomplete.
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
import { whereOf, isString, testInput } from '@orkestrel/validator'
|
|
586
|
+
|
|
587
|
+
// Simple predicate
|
|
588
|
+
const isNonEmptyString = whereOf(isString, (value) => value.length > 0)
|
|
589
|
+
|
|
590
|
+
// Embed a rule check inside a guard
|
|
591
|
+
const usernameRules = { required: true, minimum: 3, maximum: 20, alphanumeric: true }
|
|
592
|
+
const isUsername = whereOf(isString, (value) => testInput(value, usernameRules))
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### AND — `andOf`
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
import { andOf, isString } from '@orkestrel/validator'
|
|
599
|
+
|
|
600
|
+
const isNonEmptyString = andOf(isString, (value: string) => value.length > 0)
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### OR — `orOf`
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
import { orOf, isString, isNumber } from '@orkestrel/validator'
|
|
607
|
+
|
|
608
|
+
const isStringOrNumber = orOf(isString, isNumber)
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### NOT — `notOf`
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
import { notOf, isString } from '@orkestrel/validator'
|
|
615
|
+
|
|
616
|
+
const isNotString = notOf(isString)
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Union — `unionOf`
|
|
620
|
+
|
|
621
|
+
Variadic OR across any number of guards:
|
|
622
|
+
|
|
623
|
+
```ts
|
|
624
|
+
import { unionOf, literalOf } from '@orkestrel/validator'
|
|
625
|
+
|
|
626
|
+
const isDirection = unionOf(
|
|
627
|
+
literalOf('north'),
|
|
628
|
+
literalOf('south'),
|
|
629
|
+
literalOf('east'),
|
|
630
|
+
literalOf('west'),
|
|
631
|
+
)
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### Intersection — `intersectionOf` and `composedOf`
|
|
635
|
+
|
|
636
|
+
Both require every guard to pass. Use `intersectionOf` for true type intersections; use `composedOf` when layering constraints on the same base type:
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
import { intersectionOf, composedOf, isString } from '@orkestrel/validator'
|
|
640
|
+
|
|
641
|
+
const isShortString = intersectionOf(
|
|
642
|
+
isString,
|
|
643
|
+
(value: unknown): value is string => isString(value) && value.length < 10,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
// composedOf reads more clearly when all guards share the same type
|
|
647
|
+
const isAlphaCode = composedOf(
|
|
648
|
+
(value: unknown): value is string => isString(value) && /^[A-Za-z]+$/.test(value),
|
|
649
|
+
(value: unknown): value is string => isString(value) && value.length === 2,
|
|
650
|
+
)
|
|
651
|
+
isAlphaCode('en') // true
|
|
652
|
+
isAlphaCode('en1') // false
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### Complement — `complementOf`
|
|
656
|
+
|
|
657
|
+
Subtracts a narrower type from a broader one — passes when the value satisfies the base guard but not the excluded guard:
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
import { complementOf, unionOf, objectOf, literalOf, isNumber } from '@orkestrel/validator'
|
|
661
|
+
|
|
662
|
+
const isCircle = objectOf({ kind: literalOf('circle'), radius: isNumber })
|
|
663
|
+
const isRectangle = objectOf({ kind: literalOf('rectangle'), width: isNumber, height: isNumber })
|
|
664
|
+
const isShape = unionOf(isCircle, isRectangle)
|
|
665
|
+
const isNotCircle = complementOf(isShape, isCircle)
|
|
666
|
+
|
|
667
|
+
isNotCircle({ kind: 'rectangle', width: 2, height: 3 }) // true
|
|
668
|
+
isNotCircle({ kind: 'circle', radius: 3 }) // false
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### Nullable — `nullableOf`
|
|
672
|
+
|
|
673
|
+
Extends any guard to also accept `null`:
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
import { nullableOf, isString } from '@orkestrel/validator'
|
|
677
|
+
|
|
678
|
+
const isMaybeString = nullableOf(isString)
|
|
679
|
+
isMaybeString(null) // true
|
|
680
|
+
isMaybeString('value') // true
|
|
681
|
+
isMaybeString(undefined) // false
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Transform — `transformOf`
|
|
685
|
+
|
|
686
|
+
Validates the original input by projecting it and checking the projected value. The original (not the projection) is what gets narrowed:
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
import { transformOf, isString, isNumber } from '@orkestrel/validator'
|
|
690
|
+
|
|
691
|
+
// Accept strings with a positive character count
|
|
692
|
+
const isNonEmptyString = transformOf(
|
|
693
|
+
isString,
|
|
694
|
+
(value) => value.length,
|
|
695
|
+
(n): n is number => typeof n === 'number' && n > 0,
|
|
696
|
+
)
|
|
697
|
+
isNonEmptyString('abc') // true
|
|
698
|
+
isNonEmptyString('') // false
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### Lazy — `lazyOf`
|
|
702
|
+
|
|
703
|
+
Defers guard creation until first call. Required when building guards for **recursive types**, because the guard variable isn't defined yet at the point of the `objectOf` call:
|
|
704
|
+
|
|
705
|
+
```ts
|
|
706
|
+
import type { Guard } from '@orkestrel/validator'
|
|
707
|
+
import { lazyOf, objectOf, arrayOf, isNumber } from '@orkestrel/validator'
|
|
708
|
+
|
|
709
|
+
interface Tree {
|
|
710
|
+
readonly value: number
|
|
711
|
+
readonly children?: readonly Tree[]
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const isTree: Guard<Tree> = lazyOf(() =>
|
|
715
|
+
objectOf(
|
|
716
|
+
{
|
|
717
|
+
value: isNumber,
|
|
718
|
+
children: arrayOf(isTree), // safe — isTree is resolved at call time
|
|
719
|
+
},
|
|
720
|
+
['children'],
|
|
721
|
+
),
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
isTree({ value: 1, children: [{ value: 2 }] }) // true
|
|
725
|
+
isTree({ value: 'x' }) // false
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## Combining Guards and Validation
|
|
731
|
+
|
|
732
|
+
The two axes compose naturally. Use `whereOf` with `testInput` to embed field-level validation inside a structural guard:
|
|
733
|
+
|
|
734
|
+
```ts
|
|
735
|
+
import {
|
|
736
|
+
objectOf,
|
|
737
|
+
arrayOf,
|
|
738
|
+
literalOf,
|
|
739
|
+
isString,
|
|
740
|
+
whereOf,
|
|
741
|
+
testInput,
|
|
742
|
+
validateInput,
|
|
743
|
+
} from '@orkestrel/validator'
|
|
744
|
+
|
|
745
|
+
const usernameRules = { required: true, minimum: 3, maximum: 20, alphanumeric: true }
|
|
746
|
+
const emailRules = { required: true, email: true }
|
|
747
|
+
|
|
748
|
+
const isSignupForm = objectOf(
|
|
749
|
+
{
|
|
750
|
+
username: whereOf(isString, (v) => testInput(v, usernameRules)),
|
|
751
|
+
email: whereOf(isString, (v) => testInput(v, emailRules)),
|
|
752
|
+
role: literalOf('user', 'admin'),
|
|
753
|
+
tags: arrayOf(whereOf(isString, (v) => v.length > 0)),
|
|
754
|
+
},
|
|
755
|
+
['tags'], // tags is optional
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
const payload: unknown = JSON.parse(request.body)
|
|
759
|
+
|
|
760
|
+
if (isSignupForm(payload)) {
|
|
761
|
+
// payload is fully typed:
|
|
762
|
+
// {
|
|
763
|
+
// username: string
|
|
764
|
+
// email: string
|
|
765
|
+
// role: 'user' | 'admin'
|
|
766
|
+
// tags?: readonly string[]
|
|
767
|
+
// }
|
|
768
|
+
submit(payload)
|
|
769
|
+
} else {
|
|
770
|
+
// Get per-field error details with validateInput
|
|
771
|
+
const usernameResult = validateInput(isString(payload) ? payload : '', usernameRules)
|
|
772
|
+
return { errors: usernameResult.errors }
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
---
|
|
777
|
+
|
|
778
|
+
## TypeScript Types
|
|
779
|
+
|
|
780
|
+
These types are exported for use in your own function signatures.
|
|
781
|
+
|
|
782
|
+
### Guard types
|
|
783
|
+
|
|
784
|
+
```ts
|
|
785
|
+
import type { Guard, GuardType, GuardsShape } from '@orkestrel/validator'
|
|
786
|
+
|
|
787
|
+
// Guard<T> — the type predicate signature
|
|
788
|
+
const isRole: Guard<'user' | 'admin'> = literalOf('user', 'admin')
|
|
789
|
+
|
|
790
|
+
// GuardType — extract T from a Guard<T>
|
|
791
|
+
type Role = GuardType<typeof isRole> // 'user' | 'admin'
|
|
792
|
+
|
|
793
|
+
// GuardsShape — a map of string keys to guards, input to objectOf / recordOf
|
|
794
|
+
const userShape: GuardsShape = { id: isString, age: isNumber }
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### Output types from shapes
|
|
798
|
+
|
|
799
|
+
```ts
|
|
800
|
+
import type { FromGuards, OptionalFromGuards, AllOptionalFromGuards } from '@orkestrel/validator'
|
|
801
|
+
|
|
802
|
+
const userShape = { id: isString, age: isNumber, note: isString }
|
|
803
|
+
|
|
804
|
+
// All keys required
|
|
805
|
+
type User = FromGuards<typeof userShape>
|
|
806
|
+
// Readonly<{ id: string; age: number; note: string }>
|
|
807
|
+
|
|
808
|
+
// Selected keys optional
|
|
809
|
+
type FlexUser = OptionalFromGuards<typeof userShape, ['note']>
|
|
810
|
+
// Readonly<{ id: string; age: number; note?: string }>
|
|
811
|
+
|
|
812
|
+
// All keys optional
|
|
813
|
+
type PartialUser = AllOptionalFromGuards<typeof userShape>
|
|
814
|
+
// Readonly<Partial<{ id: string; age: number; note: string }>>
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
### Validation types
|
|
818
|
+
|
|
819
|
+
```ts
|
|
820
|
+
import type {
|
|
821
|
+
ValidationRules,
|
|
822
|
+
ValidationResult,
|
|
823
|
+
ValidationError,
|
|
824
|
+
ValidationRuleName,
|
|
825
|
+
ValidatorFunction,
|
|
826
|
+
ValidatorErrorCode,
|
|
827
|
+
ValidatorErrorContext,
|
|
828
|
+
} from '@orkestrel/validator'
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Function type aliases
|
|
832
|
+
|
|
833
|
+
```ts
|
|
834
|
+
import type {
|
|
835
|
+
AnyFunction, // (...args: unknown[]) => unknown
|
|
836
|
+
AnyAsyncFunction, // (...args: unknown[]) => Promise<unknown>
|
|
837
|
+
ZeroArgFunction, // () => unknown
|
|
838
|
+
ZeroArgAsyncFunction, // () => Promise<unknown>
|
|
839
|
+
AnyConstructor, // new (...args: never[]) => T
|
|
840
|
+
} from '@orkestrel/validator'
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
## Low-level APIs
|
|
846
|
+
|
|
847
|
+
These are exported for advanced use cases but most consumers won't need them directly.
|
|
848
|
+
|
|
849
|
+
### `evaluateRule`
|
|
850
|
+
|
|
851
|
+
Evaluate a single rule against a string without constructing a full rule set:
|
|
852
|
+
|
|
853
|
+
```ts
|
|
854
|
+
import { evaluateRule } from '@orkestrel/validator'
|
|
855
|
+
|
|
856
|
+
evaluateRule('required', true, '') // 'This field is required'
|
|
857
|
+
evaluateRule('required', true, 'x') // undefined (passed)
|
|
858
|
+
evaluateRule('minimum', 3, 'ab') // 'Must be at least 3 characters'
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### `cloneValidationRules`
|
|
862
|
+
|
|
863
|
+
Shallow-clone a rule object (validates it first):
|
|
864
|
+
|
|
865
|
+
```ts
|
|
866
|
+
import { cloneValidationRules } from '@orkestrel/validator'
|
|
867
|
+
|
|
868
|
+
const copy = cloneValidationRules({ required: true, minimum: 3 })
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
### `createEnumValues`
|
|
872
|
+
|
|
873
|
+
Extract the runtime values from a TypeScript enum. Numeric enum reverse-mapping is handled automatically:
|
|
874
|
+
|
|
875
|
+
```ts
|
|
876
|
+
import { createEnumValues } from '@orkestrel/validator'
|
|
877
|
+
|
|
878
|
+
createEnumValues({ idle: 'IDLE', busy: 'BUSY' }) // ['IDLE', 'BUSY']
|
|
879
|
+
|
|
880
|
+
enum Status {
|
|
881
|
+
Idle,
|
|
882
|
+
Busy,
|
|
883
|
+
}
|
|
884
|
+
createEnumValues(Status) // [0, 1] — not ['Idle', 'Busy']
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### `createShapeGuard`
|
|
888
|
+
|
|
889
|
+
The factory behind `objectOf` and `recordOf`. Prefer those functions in application code:
|
|
890
|
+
|
|
891
|
+
```ts
|
|
892
|
+
import { createShapeGuard } from '@orkestrel/validator'
|
|
893
|
+
|
|
894
|
+
const isUser = createShapeGuard({ id: isString, age: isNumber })
|
|
895
|
+
const isFlexUser = createShapeGuard({ id: isString, note: isString }, ['note'])
|
|
896
|
+
const isPartialUser = createShapeGuard({ id: isString, age: isNumber }, true)
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
### Domain-level type guards
|
|
900
|
+
|
|
901
|
+
Guards for the validation domain types themselves — useful when processing rule objects or results from unknown sources:
|
|
902
|
+
|
|
903
|
+
```ts
|
|
904
|
+
import {
|
|
905
|
+
isValidationRuleName, // 'required' | 'minimum' | ...
|
|
906
|
+
isValidationRules, // ValidationRules
|
|
907
|
+
isValidationError, // ValidationError
|
|
908
|
+
isValidationResult, // ValidationResult
|
|
909
|
+
isValidatorFunction, // ValidatorFunction
|
|
910
|
+
} from '@orkestrel/validator'
|
|
911
|
+
|
|
912
|
+
isValidationRuleName('required') // true
|
|
913
|
+
isValidationRuleName('unknown') // false
|
|
914
|
+
isValidationRules({ required: true, minimum: 3 }) // true
|
|
915
|
+
isValidationRules({ required: 'yes' }) // false
|
|
916
|
+
isValidationError({ rule: 'required', message: 'This field...' }) // true
|
|
917
|
+
isValidationResult({ valid: true, errors: [] }) // true
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
### `recordValue`
|
|
921
|
+
|
|
922
|
+
Read a property from an object by any key type (including symbols):
|
|
923
|
+
|
|
924
|
+
```ts
|
|
925
|
+
import { recordValue } from '@orkestrel/validator'
|
|
926
|
+
|
|
927
|
+
const sym = Symbol('id')
|
|
928
|
+
const obj = { name: 'Ada', [sym]: 42 }
|
|
929
|
+
|
|
930
|
+
recordValue(obj, 'name') // 'Ada'
|
|
931
|
+
recordValue(obj, sym) // 42
|
|
932
|
+
recordValue(obj, 'missing') // undefined
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
### Built-in regex constants
|
|
936
|
+
|
|
937
|
+
```ts
|
|
938
|
+
import {
|
|
939
|
+
EMAIL_PATTERN, // /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
940
|
+
URL_PATTERN, // /^https?:\/\/.+/
|
|
941
|
+
NUMERIC_PATTERN, // /^-?\d+(\.\d+)?$/
|
|
942
|
+
INTEGER_PATTERN, // /^-?\d+$/
|
|
943
|
+
ALPHANUMERIC_PATTERN, // /^[a-zA-Z0-9]+$/
|
|
944
|
+
VALIDATION_RULE_NAMES, // ordered list of all rule names
|
|
945
|
+
} from '@orkestrel/validator'
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
Import these when building custom validators that should match the built-in rule semantics exactly.
|