@oscarpalmer/jhunal 0.16.0 → 0.18.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.
Files changed (40) hide show
  1. package/dist/constants.d.mts +28 -15
  2. package/dist/constants.mjs +31 -14
  3. package/dist/helpers.d.mts +8 -1
  4. package/dist/helpers.mjs +68 -3
  5. package/dist/index.d.mts +283 -262
  6. package/dist/index.mjs +189 -56
  7. package/dist/models/infer.model.d.mts +66 -0
  8. package/dist/models/infer.model.mjs +1 -0
  9. package/dist/models/misc.model.d.mts +153 -0
  10. package/dist/models/misc.model.mjs +1 -0
  11. package/dist/models/schema.plain.model.d.mts +92 -0
  12. package/dist/models/schema.plain.model.mjs +1 -0
  13. package/dist/models/schema.typed.model.d.mts +96 -0
  14. package/dist/models/schema.typed.model.mjs +1 -0
  15. package/dist/models/transform.model.d.mts +59 -0
  16. package/dist/models/transform.model.mjs +1 -0
  17. package/dist/models/validation.model.d.mts +81 -0
  18. package/dist/models/validation.model.mjs +21 -0
  19. package/dist/schematic.d.mts +20 -6
  20. package/dist/schematic.mjs +7 -12
  21. package/dist/validation/property.validation.d.mts +1 -1
  22. package/dist/validation/property.validation.mjs +21 -17
  23. package/dist/validation/value.validation.d.mts +2 -2
  24. package/dist/validation/value.validation.mjs +63 -11
  25. package/package.json +3 -3
  26. package/src/constants.ts +84 -18
  27. package/src/helpers.ts +162 -4
  28. package/src/index.ts +3 -1
  29. package/src/models/infer.model.ts +105 -0
  30. package/src/models/misc.model.ts +212 -0
  31. package/src/models/schema.plain.model.ts +110 -0
  32. package/src/models/schema.typed.model.ts +109 -0
  33. package/src/models/transform.model.ts +85 -0
  34. package/src/models/validation.model.ts +123 -0
  35. package/src/schematic.ts +29 -18
  36. package/src/validation/property.validation.ts +46 -36
  37. package/src/validation/value.validation.ts +115 -15
  38. package/dist/models.d.mts +0 -507
  39. package/dist/models.mjs +0 -18
  40. package/src/models.ts +0 -691
package/dist/index.d.mts CHANGED
@@ -1,31 +1,6 @@
1
1
  import { Constructor, GenericCallback, PlainObject, Simplify } from "@oscarpalmer/atoms/models";
2
2
 
3
- //#region src/models.d.ts
4
- /**
5
- * Removes duplicate types from a tuple, preserving first occurrence order
6
- *
7
- * @template Value - Tuple to deduplicate
8
- * @template Seen - Accumulator for already-seen types _(internal)_
9
- *
10
- * @example
11
- * ```ts
12
- * // DeduplicateTuple<['string', 'number', 'string']>
13
- * // => ['string', 'number']
14
- * ```
15
- */
16
- type DeduplicateTuple<Value extends unknown[], Seen extends unknown[] = []> = Value extends [infer Head, ...infer Tail] ? Head extends Seen[number] ? DeduplicateTuple<Tail, Seen> : DeduplicateTuple<Tail, [...Seen, Head]> : Seen;
17
- /**
18
- * Recursively extracts {@link ValueName} strings from a type, unwrapping arrays and readonly arrays
19
- *
20
- * @template Value - Type to extract value names from
21
- *
22
- * @example
23
- * ```ts
24
- * // ExtractValueNames<'string'> => 'string'
25
- * // ExtractValueNames<['string', 'number']> => 'string' | 'number'
26
- * ```
27
- */
28
- type ExtractValueNames<Value> = Value extends ValueName ? Value : Value extends (infer Item)[] ? ExtractValueNames<Item> : Value extends readonly (infer Item)[] ? ExtractValueNames<Item> : never;
3
+ //#region src/models/infer.model.d.ts
29
4
  /**
30
5
  * Infers the TypeScript type from a {@link Schema} definition
31
6
  *
@@ -83,88 +58,278 @@ type InferSchemaEntry<Value> = Value extends (infer Item)[] ? InferSchemaEntryVa
83
58
  *
84
59
  * @template Value - single schema entry
85
60
  */
86
- type InferSchemaEntryValue<Value> = Value extends Constructor<infer Instance> ? Instance : Value extends Schematic<infer Model> ? Model : Value extends SchemaProperty ? InferPropertyType<Value['$type']> : Value extends NestedSchema ? Infer<Omit<Value, '$required'>> : Value extends ValueName ? Values[Value & ValueName] : Value extends Schema ? Infer<Value> : never;
61
+ type InferSchemaEntryValue<Value> = Value extends Constructor<infer Instance> ? Instance : Value extends Schematic<infer Model> ? Model : Value extends SchemaProperty ? InferPropertyType<Value['$type']> : Value extends PlainSchema ? Infer<Value & Schema> : Value extends ValueName ? Values[Value & ValueName] : Value extends Schema ? Infer<Value> : never;
62
+ //#endregion
63
+ //#region src/models/transform.model.d.ts
87
64
  /**
88
- * Determines whether a schema entry is optional
65
+ * Maps each element of a tuple through {@link ToValueType}
89
66
  *
90
- * Returns `true` if the entry is a {@link SchemaProperty} or {@link NestedSchema} with `$required` set to `false`; otherwise returns `false`
67
+ * @template Value - Tuple of types to map
68
+ */
69
+ type MapToValueTypes<Value extends unknown[]> = Value extends [infer Head, ...infer Tail] ? [ToValueType<Head>, ...MapToValueTypes<Tail>] : [];
70
+ /**
71
+ * Maps each element of a tuple through {@link ToSchemaPropertyTypeEach}
91
72
  *
92
- * @template Value - Schema entry to check
73
+ * @template Value - Tuple of types to map
93
74
  */
94
- type IsOptionalProperty<Value> = Value extends SchemaProperty ? Value['$required'] extends false ? true : false : Value extends {
95
- $required?: boolean;
96
- } ? Value extends {
97
- $required: false;
98
- } ? true : false : false;
75
+ type MapToSchemaPropertyTypes<Value extends unknown[]> = Value extends [infer Head, ...infer Tail] ? [ToSchemaPropertyTypeEach<Head>, ...MapToSchemaPropertyTypes<Tail>] : [];
99
76
  /**
100
- * Extracts the last member from a union type by leveraging intersection of function return types
77
+ * Converts a type into its corresponding {@link SchemaPropertyType}-representation
101
78
  *
102
- * @template Value - Union type
79
+ * Deduplicates and unwraps single-element tuples via {@link UnwrapSingle}
80
+ *
81
+ * @template Value - type to convert
103
82
  */
104
- type LastOfUnion<Value> = UnionToIntersection<Value extends unknown ? () => Value : never> extends (() => infer Item) ? Item : never;
83
+ type ToSchemaPropertyType<Value> = UnwrapSingle<DeduplicateTuple<MapToSchemaPropertyTypes<UnionToTuple<Value>>>>;
105
84
  /**
106
- * Maps each element of a tuple through {@link ToValueType}
85
+ * Converts a single type to its schema property equivalent
107
86
  *
108
- * @template Value - Tuple of types to map
87
+ * {@link NestedSchema} values have `$required` stripped, plain objects become {@link TypedSchema}, and primitives go through {@link ToValueType}
88
+ *
89
+ * @template Value - type to convert
109
90
  */
110
- type MapToValueTypes<Value extends unknown[]> = Value extends [infer Head, ...infer Tail] ? [ToValueType<Head>, ...MapToValueTypes<Tail>] : [];
91
+ type ToSchemaPropertyTypeEach<Value> = Value extends PlainObject ? TypedSchema<Value> : ToValueType<Value>;
111
92
  /**
112
- * Maps each element of a tuple through {@link ToSchemaPropertyTypeEach}
93
+ * Converts a type into its corresponding {@link ValueName}-representation
113
94
  *
114
- * @template Value - Tuple of types to map
95
+ * Deduplicates and unwraps single-element tuples via {@link UnwrapSingle}
96
+ *
97
+ * @template Value - type to convert
115
98
  */
116
- type MapToSchemaPropertyTypes<Value extends unknown[]> = Value extends [infer Head, ...infer Tail] ? [ToSchemaPropertyTypeEach<Head>, ...MapToSchemaPropertyTypes<Tail>] : [];
99
+ type ToSchemaType<Value> = UnwrapSingle<DeduplicateTuple<MapToValueTypes<UnionToTuple<Value>>>>;
117
100
  /**
118
- * A nested schema definition that may include a `$required` flag alongside arbitrary string-keyed properties
101
+ * Maps a type to its {@link ValueName} string equivalent
102
+ *
103
+ * Resolves {@link Schematic} types as-is, then performs a reverse-lookup against {@link Values} _(excluding `'object'`)_ to find a matching key. If no match is found, `object` types resolve to `'object'` or a type-guard function, and all other unrecognised types resolve to a type-guard function
104
+ *
105
+ * @template Value - type to map
119
106
  *
120
107
  * @example
121
108
  * ```ts
122
- * const address: NestedSchema = {
123
- * $required: false,
124
- * street: 'string',
125
- * city: 'string',
126
- * };
109
+ * // ToValueType<string> => 'string'
110
+ * // ToValueType<number[]> => 'array'
111
+ * // ToValueType<Date> => 'date'
112
+ * ```
113
+ */
114
+ type ToValueType<Value> = Value extends Schematic<any> ? Value : { [Key in keyof Omit<Values, 'object'>]: Value extends Values[Key] ? Key : never }[keyof Omit<Values, 'object'>] extends infer Match ? [Match] extends [never] ? Value extends object ? 'object' | ((value: unknown) => value is Value) : (value: unknown) => value is Value : Match : never;
115
+ //#endregion
116
+ //#region src/models/schema.typed.model.d.ts
117
+ /**
118
+ * A typed optional property definition generated by {@link TypedSchema} for optional keys, with `$required` set to `false` and excludes `undefined` from the type
119
+ *
120
+ * @template Value - Property's type _(including `undefined`)_
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * // For `{ name?: string }`, the `name` key produces:
125
+ * // TypedPropertyOptional<string | undefined>
126
+ * // => { $required: false; $type: 'string'; ... }
127
127
  * ```
128
128
  */
129
- type NestedSchema = {
129
+ type TypedPropertyOptional<Value> = {
130
130
  /**
131
- * Whether the nested schema is required (defaults to `true`)
131
+ * The property is not required
132
132
  */
133
- $required?: boolean;
134
- } & Schema;
133
+ $required: false;
134
+ /**
135
+ * The type(s) of the property
136
+ */
137
+ $type: ToSchemaPropertyType<Exclude<Value, undefined>>;
138
+ /**
139
+ * Custom validators for the property and its types
140
+ */
141
+ $validators?: PropertyValidators<ToSchemaPropertyType<Exclude<Value, undefined>>>;
142
+ };
135
143
  /**
136
- * Extracts keys from an object type that are optional
144
+ * A typed required property definition generated by {@link TypedSchema} for required keys, with `$required` defaulting to `true`
137
145
  *
138
- * @template Value - Object type to inspect
146
+ * @template Value - Property's type
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * // For `{ name: string }`, the `name` key produces:
151
+ * // TypedPropertyRequired<string>
152
+ * // => { $required?: true; $type: 'string'; ... }
153
+ * ```
139
154
  */
140
- type OptionalKeys<Value> = { [Key in keyof Value]-?: {} extends Pick<Value, Key> ? Key : never }[keyof Value];
155
+ type TypedPropertyRequired<Value> = {
156
+ /**
157
+ * The property is required _(defaults to `true`)_
158
+ */
159
+ $required?: true;
160
+ /**
161
+ * The type(s) of the property
162
+ */
163
+ $type: ToSchemaPropertyType<Value>;
164
+ /**
165
+ * Custom validators for the property and its types
166
+ */
167
+ $validators?: PropertyValidators<ToSchemaPropertyType<Value>>;
168
+ };
141
169
  /**
142
- * A generic schema allowing {@link NestedSchema}, {@link SchemaEntry}, or arrays of {@link SchemaEntry} as values
170
+ * Creates a schema type constrained to match a TypeScript type
171
+ *
172
+ * Required keys map to {@link ToSchemaType} or {@link TypedPropertyRequired}; plain object values may also use {@link Schematic}. Optional keys map to {@link TypedPropertyOptional} or, for plain objects, {@link TypedSchemaOptional}
173
+ *
174
+ * @template Model - Object type to generate a schema for
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * type User = { name: string; age: number; bio?: string };
179
+ *
180
+ * const schema: TypedSchema<User> = {
181
+ * name: 'string',
182
+ * age: 'number',
183
+ * bio: { $required: false, $type: 'string' },
184
+ * };
185
+ * ```
143
186
  */
144
- type PlainSchema = {
145
- [key: string]: NestedSchema | SchemaEntry | SchemaEntry[];
146
- };
187
+ type TypedSchema<Model extends PlainObject> = Simplify<{ [Key in RequiredKeys<Model>]: Model[Key] extends PlainObject ? TypedSchemaRequired<Model[Key]> | Schematic<Model[Key]> : ToSchemaType<Model[Key]> | TypedPropertyRequired<Model[Key]> } & { [Key in OptionalKeys<Model>]: Exclude<Model[Key], undefined> extends PlainObject ? TypedSchemaOptional<Exclude<Model[Key], undefined>> | Schematic<Exclude<Model[Key], undefined>> : TypedPropertyOptional<Model[Key]> }>;
147
188
  /**
148
- * A map of optional validator functions keyed by {@link ValueName}, used to add custom validation to {@link SchemaProperty} definitions
189
+ * A {@link TypedSchema} variant for optional nested objects, with `$required` fixed to `false`
149
190
  *
150
- * Each key may hold a single validator or an array of validators that receive the typed value
191
+ * @template Model - Nested object type
192
+ */
193
+ type TypedSchemaOptional<Model extends PlainObject> = {
194
+ $required: false;
195
+ } & TypedSchema<Model>;
196
+ /**
197
+ * A {@link TypedSchema} variant for required nested objects, with `$required` defaulting to `true`
151
198
  *
152
- * @template Value - `$type` value(s) to derive validator keys from
199
+ * @template Model - Nested object type
200
+ */
201
+ type TypedSchemaRequired<Model extends PlainObject> = {
202
+ $required?: true;
203
+ } & TypedSchema<Model>;
204
+ //#endregion
205
+ //#region src/models/validation.model.d.ts
206
+ /**
207
+ * A custom error class for schematic validation failures
208
+ */
209
+ declare class SchematicError extends Error {
210
+ constructor(message: string);
211
+ }
212
+ /**
213
+ * The runtime representation of a parsed schema property, used internally during validation
153
214
  *
154
215
  * @example
155
216
  * ```ts
156
- * const validators: PropertyValidators<'string'> = {
157
- * string: (value) => value.length > 0,
217
+ * const parsed: ValidatedProperty = {
218
+ * key: 'age',
219
+ * required: true,
220
+ * types: ['number'],
221
+ * validators: { number: [(v) => v > 0] },
158
222
  * };
159
223
  * ```
160
224
  */
161
- type PropertyValidators<Value> = { [Key in ExtractValueNames<Value>]?: ((value: Values[Key]) => boolean) | Array<(value: Values[Key]) => boolean> };
225
+ type ValidatedProperty = {
226
+ /**
227
+ * The property name in the schema
228
+ */
229
+ key: ValidatedPropertyKey;
230
+ /**
231
+ * Whether the property is required
232
+ */
233
+ required: boolean;
234
+ /**
235
+ * The allowed types for this property
236
+ */
237
+ types: ValidatedPropertyType[];
238
+ /**
239
+ * Custom validators grouped by {@link ValueName}
240
+ */
241
+ validators: ValidatedPropertyValidators;
242
+ };
162
243
  /**
163
- * Extracts keys from an object type that are required _(i.e., not optional)_
244
+ * Property name in schema
245
+ */
246
+ type ValidatedPropertyKey = {
247
+ /**
248
+ * Full property key, including parent keys for nested properties _(e.g., `address.street`)_
249
+ */
250
+ full: string;
251
+ /**
252
+ * The last segment of the property key _(e.g., `street` for `address.street`)_
253
+ */
254
+ short: string;
255
+ };
256
+ /**
257
+ * A union of valid types for a {@link ValidatedProperty}'s `types` array
164
258
  *
165
- * @template Value - Object type to inspect
259
+ * Can be a callback _(custom validator)_, a {@link Schematic}, a nested {@link ValidatedProperty}, or a {@link ValueName} string
166
260
  */
167
- type RequiredKeys<Value> = Exclude<keyof Value, OptionalKeys<Value>>;
261
+ type ValidatedPropertyType = GenericCallback | ValidatedProperty[] | Schematic<unknown> | ValueName;
262
+ /**
263
+ * A map of validator functions keyed by {@link ValueName}, used at runtime in {@link ValidatedProperty}
264
+ *
265
+ * Each key holds an array of validator functions that receive an `unknown` value and return a `boolean`
266
+ */
267
+ type ValidatedPropertyValidators = { [Key in ValueName]?: Array<(value: unknown) => boolean> };
268
+ declare class ValidationError extends Error {
269
+ readonly information: ValidationInformation[];
270
+ constructor(information: ValidationInformation[]);
271
+ }
272
+ type ValidationInformation = {
273
+ key: ValidationInformationKey;
274
+ message: string;
275
+ validator?: GenericCallback;
276
+ };
277
+ type ValidationInformationKey = ValidatedPropertyKey;
278
+ //#endregion
279
+ //#region src/schematic.d.ts
280
+ /**
281
+ * A schematic for validating objects
282
+ */
283
+ declare class Schematic<Model> {
284
+ #private;
285
+ private readonly $schematic;
286
+ constructor(properties: ValidatedProperty[]);
287
+ /**
288
+ * Does the value match the schema?
289
+ *
290
+ * Will assert that the values matches the schema and throw an error if it does not. The error will contain all validation information for the first property that fails validation.
291
+ * @param value Value to validate
292
+ * @param errors Throws an error for the first validation failure
293
+ * @returns `true` if the value matches the schema, otherwise throws an error
294
+ */
295
+ is(value: unknown, errors: 'throw'): asserts value is Model;
296
+ /**
297
+ * Does the value match the schema?
298
+ *
299
+ * Will validate that the value matches the schema and return `true` or `false`, without any validation information for validation failures.
300
+ * @param value Value to validate
301
+ * @returns `true` if the value matches the schema, otherwise `false`
302
+ */
303
+ is(value: unknown): value is Model;
304
+ }
305
+ /**
306
+ * Create a schematic from a schema
307
+ * @template Model Schema type
308
+ * @param schema Schema to create the schematic from
309
+ * @throws Throws {@link SchematicError} if the schema can not be converted into a schematic
310
+ * @returns A schematic for the given schema
311
+ */
312
+ declare function schematic<Model extends Schema>(schema: Model): Schematic<Infer<Model>>;
313
+ /**
314
+ * Create a schematic from a typed schema
315
+ * @template Model Existing type
316
+ * @param schema Typed schema to create the schematic from
317
+ * @throws Throws {@link SchematicError} if the schema can not be converted into a schematic
318
+ * @returns A schematic for the given typed schema
319
+ */
320
+ declare function schematic<Model extends PlainObject>(schema: TypedSchema<Model>): Schematic<Model>;
321
+ //#endregion
322
+ //#region src/models/schema.plain.model.d.ts
323
+ /**
324
+ * A generic schema allowing {@link NestedSchema}, {@link SchemaEntry}, or arrays of {@link SchemaEntry} as values
325
+ */
326
+ type PlainSchema = {
327
+ [key: string]: PlainSchema | SchemaEntry | SchemaEntry[] | undefined;
328
+ } & {
329
+ $required?: never;
330
+ $type?: never;
331
+ $validators?: never;
332
+ };
168
333
  /**
169
334
  * A schema for validating objects
170
335
  *
@@ -183,12 +348,12 @@ type Schema = SchemaIndex;
183
348
  *
184
349
  * Can be a {@link Constructor}, nested {@link Schema}, {@link SchemaProperty}, {@link Schematic}, {@link ValueName} string, or a custom validator function
185
350
  */
186
- type SchemaEntry = Constructor | Schema | SchemaProperty | Schematic<unknown> | ValueName | ((value: unknown) => boolean);
351
+ type SchemaEntry = Constructor | PlainSchema | SchemaProperty | Schematic<unknown> | ValueName | ((value: unknown) => boolean);
187
352
  /**
188
353
  * Index signature interface backing {@link Schema}, allowing string-keyed entries of {@link NestedSchema}, {@link SchemaEntry}, or arrays of {@link SchemaEntry}
189
354
  */
190
355
  interface SchemaIndex {
191
- [key: string]: NestedSchema | SchemaEntry | SchemaEntry[];
356
+ [key: string]: PlainSchema | SchemaEntry | SchemaEntry[];
192
357
  }
193
358
  /**
194
359
  * A property definition with explicit type(s), an optional requirement flag, and optional validators
@@ -226,55 +391,73 @@ type SchemaProperty = {
226
391
  */
227
392
  type SchemaPropertyType = Constructor | PlainSchema | Schematic<unknown> | ValueName | ((value: unknown) => boolean);
228
393
  /**
229
- * A custom error class for schema validation failures, with its `name` set to {@link ERROR_NAME}
394
+ * A map of optional validator functions keyed by {@link ValueName}, used to add custom validation to {@link SchemaProperty} definitions
395
+ *
396
+ * Each key may hold a single validator or an array of validators that receive the typed value
397
+ *
398
+ * @template Value - `$type` value(s) to derive validator keys from
230
399
  *
231
400
  * @example
232
401
  * ```ts
233
- * throw new SchematicError('Expected a string, received a number');
402
+ * const validators: PropertyValidators<'string'> = {
403
+ * string: (value) => value.length > 0,
404
+ * };
234
405
  * ```
235
406
  */
236
- declare class SchematicError extends Error {
237
- constructor(message: string);
238
- }
407
+ type PropertyValidators<Value> = { [Key in ExtractValueNames<Value>]?: ((value: Values[Key]) => boolean) | Array<(value: Values[Key]) => boolean> };
408
+ //#endregion
409
+ //#region src/models/misc.model.d.ts
239
410
  /**
240
- * Converts a type into its corresponding {@link SchemaPropertyType}-representation
411
+ * Removes duplicate types from a tuple, preserving first occurrence order
241
412
  *
242
- * Deduplicates and unwraps single-element tuples via {@link UnwrapSingle}
413
+ * @template Value - Tuple to deduplicate
414
+ * @template Seen - Accumulator for already-seen types _(internal)_
243
415
  *
244
- * @template Value - type to convert
416
+ * @example
417
+ * ```ts
418
+ * // DeduplicateTuple<['string', 'number', 'string']>
419
+ * // => ['string', 'number']
420
+ * ```
245
421
  */
246
- type ToSchemaPropertyType<Value> = UnwrapSingle<DeduplicateTuple<MapToSchemaPropertyTypes<UnionToTuple<Value>>>>;
422
+ type DeduplicateTuple<Value extends unknown[], Seen extends unknown[] = []> = Value extends [infer Head, ...infer Tail] ? Head extends Seen[number] ? DeduplicateTuple<Tail, Seen> : DeduplicateTuple<Tail, [...Seen, Head]> : Seen;
247
423
  /**
248
- * Converts a single type to its schema property equivalent
424
+ * Recursively extracts {@link ValueName} strings from a type, unwrapping arrays and readonly arrays
249
425
  *
250
- * {@link NestedSchema} values have `$required` stripped, plain objects become {@link TypedSchema}, and primitives go through {@link ToValueType}
426
+ * @template Value - Type to extract value names from
251
427
  *
252
- * @template Value - type to convert
428
+ * @example
429
+ * ```ts
430
+ * // ExtractValueNames<'string'> => 'string'
431
+ * // ExtractValueNames<['string', 'number']> => 'string' | 'number'
432
+ * ```
253
433
  */
254
- type ToSchemaPropertyTypeEach<Value> = Value extends NestedSchema ? Omit<Value, '$required'> : Value extends PlainObject ? TypedSchema<Value> : ToValueType<Value>;
434
+ type ExtractValueNames<Value> = Value extends ValueName ? Value : Value extends (infer Item)[] ? ExtractValueNames<Item> : Value extends readonly (infer Item)[] ? ExtractValueNames<Item> : never;
255
435
  /**
256
- * Converts a type into its corresponding {@link ValueName}-representation
436
+ * Determines whether a schema entry is optional
257
437
  *
258
- * Deduplicates and unwraps single-element tuples via {@link UnwrapSingle}
438
+ * Returns `true` if the entry is a {@link SchemaProperty} or {@link NestedSchema} with `$required` set to `false`; otherwise returns `false`
259
439
  *
260
- * @template Value - type to convert
440
+ * @template Value - Schema entry to check
261
441
  */
262
- type ToSchemaType<Value> = UnwrapSingle<DeduplicateTuple<MapToValueTypes<UnionToTuple<Value>>>>;
442
+ type IsOptionalProperty<Value> = Value extends SchemaProperty ? Value['$required'] extends false ? true : false : false;
263
443
  /**
264
- * Maps a type to its {@link ValueName} string equivalent
444
+ * Extracts the last member from a union type by leveraging intersection of function return types
265
445
  *
266
- * Resolves {@link Schematic} types as-is, then performs a reverse-lookup against {@link Values} _(excluding `'object'`)_ to find a matching key. If no match is found, `object` types resolve to `'object'` or a type-guard function, and all other unrecognised types resolve to a type-guard function
446
+ * @template Value - Union type
447
+ */
448
+ type LastOfUnion<Value> = UnionToIntersection<Value extends unknown ? () => Value : never> extends (() => infer Item) ? Item : never;
449
+ /**
450
+ * Extracts keys from an object type that are optional
267
451
  *
268
- * @template Value - type to map
452
+ * @template Value - Object type to inspect
453
+ */
454
+ type OptionalKeys<Value> = { [Key in keyof Value]-?: {} extends Pick<Value, Key> ? Key : never }[keyof Value];
455
+ /**
456
+ * Extracts keys from an object type that are required _(i.e., not optional)_
269
457
  *
270
- * @example
271
- * ```ts
272
- * // ToValueType<string> => 'string'
273
- * // ToValueType<number[]> => 'array'
274
- * // ToValueType<Date> => 'date'
275
- * ```
458
+ * @template Value - Object type to inspect
276
459
  */
277
- type ToValueType<Value> = Value extends Schematic<any> ? Value : { [Key in keyof Omit<Values, 'object'>]: Value extends Values[Key] ? Key : never }[keyof Omit<Values, 'object'>] extends infer Match ? [Match] extends [never] ? Value extends object ? 'object' | ((value: unknown) => value is Value) : (value: unknown) => value is Value : Match : never;
460
+ type RequiredKeys<Value> = Exclude<keyof Value, OptionalKeys<Value>>;
278
461
  /**
279
462
  * Generates all permutations of a tuple type
280
463
  *
@@ -300,93 +483,6 @@ type TuplePermutations<Tuple extends unknown[], Elput extends unknown[] = []> =
300
483
  * @template Prefix - Accumulator for elements before the target _(internal)_
301
484
  */
302
485
  type TupleRemoveAt<Items extends unknown[], Item extends string, Prefix extends unknown[] = []> = Items extends [infer Head, ...infer Tail] ? `${Prefix['length']}` extends Item ? [...Prefix, ...Tail] : TupleRemoveAt<Tail, Item, [...Prefix, Head]> : Prefix;
303
- /**
304
- * A typed optional property definition generated by {@link TypedSchema} for optional keys, with `$required` set to `false` and excludes `undefined` from the type
305
- *
306
- * @template Value - Property's type _(including `undefined`)_
307
- *
308
- * @example
309
- * ```ts
310
- * // For `{ name?: string }`, the `name` key produces:
311
- * // TypedPropertyOptional<string | undefined>
312
- * // => { $required: false; $type: 'string'; ... }
313
- * ```
314
- */
315
- type TypedPropertyOptional<Value> = {
316
- /**
317
- * The property is not required
318
- */
319
- $required: false;
320
- /**
321
- * The type(s) of the property
322
- */
323
- $type: ToSchemaPropertyType<Exclude<Value, undefined>>;
324
- /**
325
- * Custom validators for the property and its types
326
- */
327
- $validators?: PropertyValidators<ToSchemaPropertyType<Exclude<Value, undefined>>>;
328
- };
329
- /**
330
- * A typed required property definition generated by {@link TypedSchema} for required keys, with `$required` defaulting to `true`
331
- *
332
- * @template Value - Property's type
333
- *
334
- * @example
335
- * ```ts
336
- * // For `{ name: string }`, the `name` key produces:
337
- * // TypedPropertyRequired<string>
338
- * // => { $required?: true; $type: 'string'; ... }
339
- * ```
340
- */
341
- type TypedPropertyRequired<Value> = {
342
- /**
343
- * The property is required _(defaults to `true`)_
344
- */
345
- $required?: true;
346
- /**
347
- * The type(s) of the property
348
- */
349
- $type: ToSchemaPropertyType<Value>;
350
- /**
351
- * Custom validators for the property and its types
352
- */
353
- $validators?: PropertyValidators<ToSchemaPropertyType<Value>>;
354
- };
355
- /**
356
- * Creates a schema type constrained to match a TypeScript type
357
- *
358
- * Required keys map to {@link ToSchemaType} or {@link TypedPropertyRequired}; plain object values may also use {@link Schematic}. Optional keys map to {@link TypedPropertyOptional} or, for plain objects, {@link TypedSchemaOptional}
359
- *
360
- * @template Model - Object type to generate a schema for
361
- *
362
- * @example
363
- * ```ts
364
- * type User = { name: string; age: number; bio?: string };
365
- *
366
- * const schema: TypedSchema<User> = {
367
- * name: 'string',
368
- * age: 'number',
369
- * bio: { $required: false, $type: 'string' },
370
- * };
371
- * ```
372
- */
373
- type TypedSchema<Model extends PlainObject> = Simplify<{ [Key in RequiredKeys<Model>]: Model[Key] extends PlainObject ? TypedSchemaRequired<Model[Key]> | Schematic<Model[Key]> : ToSchemaType<Model[Key]> | TypedPropertyRequired<Model[Key]> } & { [Key in OptionalKeys<Model>]: Exclude<Model[Key], undefined> extends PlainObject ? TypedSchemaOptional<Exclude<Model[Key], undefined>> | Schematic<Exclude<Model[Key], undefined>> : TypedPropertyOptional<Model[Key]> }>;
374
- /**
375
- * A {@link TypedSchema} variant for optional nested objects, with `$required` fixed to `false`
376
- *
377
- * @template Model - Nested object type
378
- */
379
- type TypedSchemaOptional<Model extends PlainObject> = {
380
- $required: false;
381
- } & TypedSchema<Model>;
382
- /**
383
- * A {@link TypedSchema} variant for required nested objects, with `$required` defaulting to `true`
384
- *
385
- * @template Model - Nested object type
386
- */
387
- type TypedSchemaRequired<Model extends PlainObject> = {
388
- $required?: true;
389
- } & TypedSchema<Model>;
390
486
  /**
391
487
  * Converts a union type into an intersection
392
488
  *
@@ -430,49 +526,6 @@ type UnionToTuple<Value, Items extends unknown[] = []> = [Value] extends [never]
430
526
  * ```
431
527
  */
432
528
  type UnwrapSingle<Value extends unknown[]> = Value extends [infer Only] ? Only : Value['length'] extends 1 | 2 | 3 | 4 | 5 ? TuplePermutations<Value> : Value;
433
- /**
434
- * The runtime representation of a parsed schema property, used internally during validation
435
- *
436
- * @example
437
- * ```ts
438
- * const parsed: ValidatedProperty = {
439
- * key: 'age',
440
- * required: true,
441
- * types: ['number'],
442
- * validators: { number: [(v) => v > 0] },
443
- * };
444
- * ```
445
- */
446
- type ValidatedProperty = {
447
- /**
448
- * The property name in the schema
449
- */
450
- key: string;
451
- /**
452
- * Whether the property is required
453
- */
454
- required: boolean;
455
- /**
456
- * The allowed types for this property
457
- */
458
- types: ValidatedPropertyType[];
459
- /**
460
- * Custom validators grouped by {@link ValueName}
461
- */
462
- validators: ValidatedPropertyValidators;
463
- };
464
- /**
465
- * A union of valid types for a {@link ValidatedProperty}'s `types` array
466
- *
467
- * Can be a callback _(custom validator)_, a {@link Schematic}, a nested {@link ValidatedProperty}, or a {@link ValueName} string
468
- */
469
- type ValidatedPropertyType = GenericCallback | Schematic<unknown> | ValidatedProperty | ValueName;
470
- /**
471
- * A map of validator functions keyed by {@link ValueName}, used at runtime in {@link ValidatedProperty}
472
- *
473
- * Each key holds an array of validator functions that receive an `unknown` value and return a `boolean`
474
- */
475
- type ValidatedPropertyValidators = { [Key in ValueName]?: Array<(value: unknown) => boolean> };
476
529
  /**
477
530
  * Basic value types
478
531
  */
@@ -503,38 +556,6 @@ type Values = {
503
556
  undefined: undefined;
504
557
  };
505
558
  //#endregion
506
- //#region src/schematic.d.ts
507
- /**
508
- * A schematic for validating objects
509
- */
510
- declare class Schematic<Model> {
511
- #private;
512
- private readonly $schematic;
513
- constructor(properties: ValidatedProperty[]);
514
- /**
515
- * Does the value match the schema?
516
- * @param value - Value to validate
517
- * @returns `true` if the value matches the schema, otherwise `false`
518
- */
519
- is(value: unknown): value is Model;
520
- }
521
- /**
522
- * Create a schematic from a schema
523
- * @template Model - Schema type
524
- * @param schema - Schema to create the schematic from
525
- * @throws Throws {@link SchematicError} if the schema can not be converted into a schematic
526
- * @returns A schematic for the given schema
527
- */
528
- declare function schematic<Model extends Schema>(schema: Model): Schematic<Infer<Model>>;
529
- /**
530
- * Create a schematic from a typed schema
531
- * @template Model - Existing type
532
- * @param schema - Typed schema to create the schematic from
533
- * @throws Throws {@link SchematicError} if the schema can not be converted into a schematic
534
- * @returns A schematic for the given typed schema
535
- */
536
- declare function schematic<Model extends PlainObject>(schema: TypedSchema<Model>): Schematic<Model>;
537
- //#endregion
538
559
  //#region src/helpers.d.ts
539
560
  /**
540
561
  * Creates a validator function for a given constructor
@@ -550,4 +571,4 @@ declare function instanceOf<Instance>(constructor: Constructor<Instance>): (valu
550
571
  */
551
572
  declare function isSchematic(value: unknown): value is Schematic<never>;
552
573
  //#endregion
553
- export { type Schema, type Schematic, SchematicError, type TypedSchema, instanceOf, isSchematic, schematic };
574
+ export { type Schema, type Schematic, SchematicError, type TypedSchema, ValidationError, instanceOf, isSchematic, schematic };