ascertain 2.0.88 → 3.0.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/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Ascertain
2
2
 
3
- ### Ascertain what data is not suitable for your library
4
-
5
- 0-Deps, simple, blazing fast, for browser and Node.js object schema validator
3
+ Zero-dependency, high-performance schema validator for Node.js and browsers.
6
4
 
7
5
  [![Coverage Status][codecov-image]][codecov-url]
8
6
  [![Build Status][github-image]][github-url]
@@ -10,182 +8,354 @@
10
8
  [![Downloads][downloads-image]][npm-url]
11
9
  [![Snyk][snyk-image]][snyk-url]
12
10
 
13
-
14
11
  [Documentation](https://3axap4ehko.github.io/ascertain/)
15
12
 
16
13
  ## Features
17
14
 
18
- - Type-safe validation: Ensures your data conforms to predefined schemas.
19
- - Composite schemas: Supports logical AND, OR, and optional schemas.
20
- - Type casting: Automatically parses and casts strings to other types.
21
- - Error handling: Provides detailed error messages for invalid data.
15
+ - **Zero dependencies** - Minimal footprint, no external dependencies
16
+ - **High performance** - Compiles schemas to optimized JS functions (~6x faster than dynamic validation)
17
+ - **Type-safe** - Full TypeScript support with type inference
18
+ - **Flexible schemas** - AND, OR, optional, tuple, discriminated operators
19
+ - **Type casting** - Built-in parsers for numbers (hex, octal, binary), dates, JSON, base64
20
+ - **Object validation** - Validate keys/values with `$keys`, `$values`, `$strict`
21
+ - **Partial validation** - `createValidator` validates subsets with type narrowing
22
+ - **Detailed errors** - Clear error messages with paths for debugging
23
+ - **Standard Schema v1** - Interoperable with tRPC, TanStack Form, and other ecosystem tools
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install ascertain
29
+ ```
22
30
 
23
- ## Schema description
31
+ ## Quick Start
24
32
 
25
- - Primitive Values: Any primitive value (e.g., string, number, bigint, boolean, undefined, symbol, null) is used as an expected constant to match against.
26
- - Function Types: Functions are used as constructors for non-objects and instance types for object types.
27
- - Array Values: Arrays are used to represent an expected array type, where every item in the array must match the specified type (acting as an "and" operator).
28
- - Regular Expressions: Regular expressions are used to validate that a value matches a specified string pattern.
29
- - Object Types: Non-null objects are used as templates for expected properties, where each property of the object must match the corresponding schema definition.
33
+ ```typescript
34
+ import { ascertain, or, optional } from 'ascertain';
30
35
 
36
+ ascertain({
37
+ name: String,
38
+ age: Number,
39
+ role: or('admin', 'user'),
40
+ email: optional(String),
41
+ }, userData);
42
+ ```
31
43
 
44
+ ## Performance
32
45
 
33
- ## Usage Example
46
+ Ascertain compiles schemas into optimized JavaScript functions. Compiled validators run **~6x faster** than dynamic validation.
34
47
 
35
- ### Schema compilation
36
48
  ```typescript
37
- import { compile, optional, and, or, $keys, $values, Schema, as } from 'ascertain';
38
-
39
- const validate = compile({
40
- number: Number,
41
- string: String,
42
- boolean: Boolean,
43
- function: Function,
44
- array: Array,
45
- object: Object,
46
- date: and(Date, { toJSON: Function }),
47
- regexp: /regexp/,
48
- oneOfValue: or(1, 2, 3),
49
- arrayOfNumbers: [Number],
50
- objectSchema: {
51
- number: Number,
52
- },
53
- optional: optional({
54
- number: Number,
55
- }),
56
- keyValue: {
57
- [$keys]: /^key[A-Z]/,
58
- [$values]: Number
59
- },
60
- parsedNumber: Number,
61
- parsedString: String,
62
- parsedBoolean: Boolean,
63
- parsedArray: [String],
64
- parsedJSON: {
65
- number: 1,
66
- },
67
- parsedBase64: String,
68
- parsedTime: 2 * 60 * 1000, // two minutes
69
- parsedDate: Date,
49
+ import { compile } from 'ascertain';
50
+
51
+ // Compile once
52
+ const validateUser = compile(userSchema);
53
+
54
+ // Validate many (no recompilation)
55
+ validateUser(user1);
56
+ validateUser(user2);
57
+ ```
58
+
59
+ | When to use | Function | Speed |
60
+ |-------------|----------|-------|
61
+ | Repeated validation (API handlers, loops) | `compile()` | Fastest |
62
+ | One-off validation | `ascertain()` | Convenient |
63
+
64
+ ### Benchmark
65
+
66
+ | Library | Mode | Valid (ops/s) | Invalid (ops/s) |
67
+ |---------|------|---------------|-----------------|
68
+ | **Ascertain** | first-error | 80M | 41M |
69
+ | **Ascertain** | all-errors | 80M | 25M |
70
+ | AJV | first-error | 52M | 35M |
71
+ | AJV | all-errors | 52M | 20M |
72
+ | Zod | all-errors | 34M | 77K |
73
+
74
+ ## Schema Reference
75
+
76
+ | Schema | Validates | Example |
77
+ |--------|-----------|---------|
78
+ | `String`, `Number`, `Boolean` | Type check | `{ age: Number }` |
79
+ | `Date`, `Array`, `Object` | Instance check | `{ created: Date }` |
80
+ | `Function` | Any callable | `{ handler: Function }` |
81
+ | Primitives | Exact value | `{ status: 'active' }` |
82
+ | RegExp | Pattern match | `{ email: /^.+@.+$/ }` |
83
+ | `[Schema]` | Array of type | `{ tags: [String] }` |
84
+ | `{ key: Schema }` | Object shape | `{ user: { name: String } }` |
85
+ | `or(a, b, ...)` | Any match | `or(String, Number)` |
86
+ | `and(a, b, ...)` | All match | `and(Date, { toJSON: Function })` |
87
+ | `optional(s)` | Nullable | `optional(String)` |
88
+ | `tuple(a, b)` | Fixed array | `tuple(Number, Number)` |
89
+ | `discriminated(schemas, key)` | Tagged union | `discriminated([{ type: 'a' }, { type: 'b' }], 'type')` |
90
+
91
+ ### Special Symbols
92
+
93
+ ```typescript
94
+ import { $keys, $values, $strict } from 'ascertain';
95
+
96
+ const schema = {
97
+ [$keys]: /^[a-z]+$/, // Validate all keys
98
+ [$values]: Number, // Validate all values
99
+ [$strict]: true, // No extra properties
100
+ };
101
+ ```
102
+
103
+ ## Type Casting
104
+
105
+ Parse strings into typed values (environment variables, query params):
106
+
107
+ ```typescript
108
+ import { as } from 'ascertain';
109
+
110
+ as.number('42') // 42
111
+ as.number('3.14') // 3.14
112
+ as.number('0xFF') // 255 (hex)
113
+ as.number('0o77') // 63 (octal)
114
+ as.number('0b1010') // 10 (binary)
115
+ as.number('1e10') // 10000000000
116
+
117
+ as.boolean('true') // true
118
+ as.boolean('1') // true
119
+
120
+ as.time('500ms') // 500
121
+ as.time('30s') // 30000
122
+ as.time('5m') // 300000
123
+ as.time('2h') // 7200000
124
+ as.time('1d') // 86400000
125
+
126
+ as.date('2024-12-31') // Date object
127
+ as.array('a,b,c', ',') // ['a', 'b', 'c']
128
+ as.json('{"x":1}') // { x: 1 }
129
+ as.base64('dGVzdA==') // 'test'
130
+ ```
131
+
132
+ Invalid values return `TypeError` for deferred validation:
133
+
134
+ ```typescript
135
+ const config = {
136
+ port: as.number(process.env.PORT), // TypeError if invalid
137
+ host: as.string(process.env.HOST),
138
+ };
139
+
140
+ // Errors surface with clear paths
141
+ ascertain({ port: Number, host: String }, config);
142
+ // → TypeError: "Invalid value undefined, expected non-nullable"
143
+ ```
144
+
145
+ ## Patterns
146
+
147
+ ### Batch Validation
148
+
149
+ Compile once, validate many:
150
+
151
+ ```typescript
152
+ const validateUser = compile(userSchema);
153
+
154
+ const results = users.map((user, i) => {
155
+ if (validateUser(user)) {
156
+ return { index: i, valid: true };
157
+ }
158
+ return { index: i, valid: false, error: validateUser.issues[0].message };
70
159
  });
160
+ ```
161
+
162
+ ### Discriminated Unions
163
+
164
+ Use `discriminated()` for efficient tagged union validation. Instead of trying each variant like `or()`, it checks the discriminant field first and only validates the matching variant:
165
+
166
+ ```typescript
167
+ import { compile, discriminated } from 'ascertain';
168
+
169
+ const messageSchema = discriminated([
170
+ { type: 'email', address: String },
171
+ { type: 'sms', phone: String },
172
+ { type: 'push', token: String },
173
+ ], 'type');
71
174
 
175
+ const validate = compile(messageSchema);
176
+
177
+ validate({ type: 'email', address: 'user@example.com' }); // true
178
+ validate({ type: 'sms', phone: '123456' }); // true
179
+ validate({ type: 'push', token: 123 }); // false
180
+ validate({ type: 'unknown' }); // false
72
181
  ```
73
182
 
74
- ### Runtime validation
75
- Create data ascertain
183
+ Discriminant values must be string, number, or boolean literals.
184
+
185
+ ### Conditional Rules
186
+
187
+ Use `or()` and `and()` for complex conditions:
188
+
76
189
  ```typescript
77
- import { ascertain, optional, and, or, $keys, $values, Schema, as } from 'ascertain';
78
-
79
- // create data sample
80
- const data = {
81
- number: 1,
82
- string: 'string',
83
- boolean: true,
84
- function: () => {},
85
- array: [],
86
- object: {},
87
- date: new Date,
88
- regexp: 'regexp',
89
- oneOfValue: 1,
90
- arrayOfNumbers: [1,2,3,4,5],
91
- objectSchema: {
92
- number: 1,
93
- },
94
- optional: null,
95
- keyValue: {
96
- keyOne: 1,
97
- keyTwo: 2,
98
- keyThree: 3,
99
- },
100
- // fault tolernat type casting
101
- parsedNumber: as.number('1'),
102
- parsedString: as.string('string'),
103
- parsedBoolean: as.boolean('false'),
104
- parsedArray: as.array('1,2,3,4,5', ','),
105
- parsedJSON: as.json('{ "number": 1 }'),
106
- parsedBase64: as.base64('dGVzdA=='),
107
- parsedTime: as.time('2m'),
108
- parsedDate: as.date('31-12-2024'),
190
+ const schema = {
191
+ type: or('email', 'sms'),
192
+ // Conditional: email requires address, sms requires phone
193
+ contact: or(
194
+ and({ type: 'email' }, { address: String }),
195
+ and({ type: 'sms' }, { phone: String }),
196
+ ),
109
197
  };
198
+ ```
110
199
 
111
- // create data schema
112
- const schema: Schema<typeof data> = {
113
- number: Number,
114
- string: String,
115
- boolean: Boolean,
116
- function: Function,
117
- array: Array,
118
- object: Object,
119
- date: and(Date, { toJSON: Function }),
120
- regexp: /regexp/,
121
- oneOfValue: or(1, 2, 3),
122
- arrayOfNumbers: [Number],
123
- objectSchema: {
124
- number: Number,
125
- },
126
- optional: optional({
127
- number: Number,
128
- }),
129
- keyValue: {
130
- [$keys]: /^key[A-Z]/,
131
- [$values]: Number
132
- },
133
- parsedNumber: Number,
134
- parsedString: String,
135
- parsedBoolean: Boolean,
136
- parsedArray: [String],
137
- parsedJSON: {
138
- number: 1,
139
- },
140
- parsedBase64: String,
141
- parsedTime: 2 * 60 * 1000, // two minutes
142
- parsedDate: Date,
200
+ ### Schema Composition
201
+
202
+ Build schemas from reusable parts:
203
+
204
+ ```typescript
205
+ const addressSchema = {
206
+ street: String,
207
+ city: String,
208
+ zip: /^\d{5}$/,
209
+ };
210
+
211
+ const personSchema = {
212
+ name: String,
213
+ address: addressSchema,
143
214
  };
144
215
 
145
- // validate
146
- const validate = ascertain<typeof data>(schema, data, '[DATA]');
216
+ const companySchema = {
217
+ name: String,
218
+ headquarters: addressSchema,
219
+ employees: [personSchema],
220
+ };
147
221
  ```
148
222
 
149
- ### Benchmark VS zod and ajv
223
+ ### Versioned Schemas
224
+
225
+ Version schemas as modules:
226
+
227
+ ```typescript
228
+ // schemas/user.v1.ts
229
+ export const userSchemaV1 = { name: String, email: String };
230
+
231
+ // schemas/user.v2.ts
232
+ export const userSchemaV2 = {
233
+ ...userSchemaV1,
234
+ phone: optional(String),
235
+ createdAt: Date,
236
+ };
237
+
238
+ // api/handler.ts
239
+ import { userSchemaV2 } from './schemas/user.v2';
240
+ const validate = compile(userSchemaV2);
150
241
  ```
151
- ⭐ Script ajv-vs-zod-vs-ascertain.js
152
- Suite ajv vs zod vs ascertain
153
- ➤ Perform benchmark
154
- Measure 500000 zod static schema validation
155
- ┌──────────┬──────────┬──────────┬──────────┬────────────┬────────┐
156
- │ (index) │ med │ p95 │ p99 │ total │ count │
157
- ├──────────┼──────────┼──────────┼──────────┼────────────┼────────┤
158
- │ 0.000699 │ 0.000788 │ 0.000996 │ 0.001624 │ 462.602373 │ 500000 │
159
- └──────────┴──────────┴──────────┴──────────┴────────────┴────────┘
160
- Measure 500000 zod dynamic schema validation
161
- ┌──────────┬──────────┬──────────┬──────────┬─────────────┬────────┐
162
- │ (index) med │ p95 │ p99 │ total │ count │
163
- ├──────────┼──────────┼──────────┼──────────┼─────────────┼────────┤
164
- │ 0.006248 │ 0.006918 │ 0.007524 │ 0.016948 │ 3780.465563 │ 500000 │
165
- └──────────┴──────────┴──────────┴──────────┴─────────────┴────────┘
166
- ✓ Measure 500000 ascertain static schema validation
167
- ┌──────────┬──────────┬──────────┬──────────┬───────────┬────────┐
168
- (index) │ med │ p95 │ p99 │ total │ count │
169
- ├──────────┼──────────┼──────────┼──────────┼───────────┼────────┤
170
- │ 0.000063 │ 0.000071 │ 0.000098 │ 0.000267 │ 41.673271 │ 500000 │
171
- └──────────┴──────────┴──────────┴──────────┴───────────┴────────┘
172
- Measure 500000 ascertain dynamic schema validation
173
- ┌──────────┬──────────┬──────────┬──────────┬────────────┬────────┐
174
- (index) med │ p95 │ p99 │ total │ count │
175
- ├──────────┼──────────┼──────────┼──────────┼────────────┼────────┤
176
- 0.000367 0.000415 0.000525 │ 0.001055 │ 239.078129 │ 500000 │
177
- └──────────┴──────────┴──────────┴──────────┴────────────┴────────┘
178
- ✓ Measure 500000 ajv compiled schema validation
179
- ┌──────────┬──────────┬──────────┬──────────┬───────────┬────────┐
180
- (index) │ med │ p95 │ p99 │ total │ count │
181
- ├──────────┼──────────┼──────────┼──────────┼───────────┼────────┤
182
- 0.000063 0.000072 0.000124 0.000307 44.542936 500000
183
- └──────────┴──────────┴──────────┴──────────┴───────────┴────────┘
242
+
243
+ ### Config Validation
244
+
245
+ Validate only what each module needs:
246
+
247
+ ```typescript
248
+ import { createValidator, as } from 'ascertain';
249
+
250
+ const config = {
251
+ app: { name: as.string(process.env.APP_NAME), port: as.number(process.env.PORT) },
252
+ db: { host: as.string(process.env.DB_HOST), pool: as.number(process.env.DB_POOL) },
253
+ cache: { ttl: as.time(process.env.CACHE_TTL) },
254
+ };
255
+
256
+ const validate = createValidator(config);
257
+
258
+ // Each module validates only what it needs
259
+ const { db } = validate({
260
+ db: { host: String, pool: Number },
261
+ });
262
+
263
+ db.host; // string - validated and typed
264
+ db.pool; // number - validated and typed
265
+ // db.xxx // TypeScript error - property doesn't exist
266
+
267
+ // cache not validated = not accessible
268
+ // cache.ttl // TypeScript error - cache not in returned type
269
+ ```
270
+
271
+ ## Compile Options
272
+
273
+ By default `compile()` stops at the first validation error (fastest for invalid data). Pass `{ allErrors: true }` to collect all errors:
274
+
275
+ ```typescript
276
+ import { compile } from 'ascertain';
277
+
278
+ const schema = { name: String, age: Number, active: Boolean };
279
+
280
+ // First-error mode (default) - stops at first failure
281
+ const validate = compile(schema);
282
+ if (!validate({ name: 123, age: 'bad', active: 'no' })) {
283
+ console.log(validate.issues.length); // 1
284
+ console.log(validate.issues[0].path); // ['name']
285
+ }
286
+
287
+ // All-errors mode - collects every failure
288
+ const validateAll = compile(schema, { allErrors: true });
289
+ if (!validateAll({ name: 123, age: 'bad', active: 'no' })) {
290
+ console.log(validateAll.issues.length); // 3
291
+ }
292
+ ```
293
+
294
+ ## Standard Schema
295
+
296
+ Wrap a schema for [Standard Schema v1](https://standardschema.dev/) compliance, enabling interoperability with tRPC, TanStack Form, and other ecosystem libraries:
297
+
298
+ ```typescript
299
+ import { standardSchema, or, optional } from 'ascertain';
300
+
301
+ const userValidator = standardSchema({
302
+ name: String,
303
+ age: Number,
304
+ role: or('admin', 'user'),
305
+ email: optional(String),
306
+ });
307
+
308
+ // Use as regular validator (throws on error)
309
+ userValidator({ name: 'Alice', age: 30, role: 'admin' });
310
+
311
+ // Use Standard Schema interface (returns result object)
312
+ const result = userValidator['~standard'].validate(unknownData);
313
+ if (result.issues) {
314
+ console.log(result.issues);
315
+ } else {
316
+ console.log(result.value);
317
+ }
318
+ ```
319
+
320
+ ## Complete Example
321
+
322
+ ```typescript
323
+ import { compile, or, optional, and, tuple, $keys, $values, $strict, as } from 'ascertain';
324
+
325
+ const schema = {
326
+ id: Number,
327
+ name: String,
328
+ email: /^[^@]+@[^@]+$/,
329
+ status: or('active', 'inactive', 'pending'),
330
+ role: optional(or('admin', 'user')),
331
+
332
+ profile: {
333
+ bio: optional(String),
334
+ avatar: optional(String),
335
+ },
336
+
337
+ settings: {
338
+ [$keys]: /^[a-z_]+$/,
339
+ [$values]: or(String, Number, Boolean),
340
+ [$strict]: true,
341
+ },
342
+
343
+ coordinates: optional(tuple(Number, Number)),
344
+ createdAt: and(Date, { toISOString: Function }),
345
+
346
+ retries: as.number(process.env.MAX_RETRIES),
347
+ timeout: as.time(process.env.TIMEOUT),
348
+ };
349
+
350
+ const validate = compile(schema);
351
+ if (!validate(data)) {
352
+ console.error(validate.issues);
353
+ }
184
354
  ```
185
355
 
186
356
  ## License
187
- License [The MIT License](http://opensource.org/licenses/MIT)
188
- Copyright (c) 2019-2025 Ivan Zakharchanka
357
+
358
+ [MIT](http://opensource.org/licenses/MIT) - Ivan Zakharchanka
189
359
 
190
360
  [npm-url]: https://www.npmjs.com/package/ascertain
191
361
  [downloads-image]: https://img.shields.io/npm/dw/ascertain.svg?maxAge=43200