@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.
- package/dist/constants.d.mts +28 -15
- package/dist/constants.mjs +31 -14
- package/dist/helpers.d.mts +8 -1
- package/dist/helpers.mjs +68 -3
- package/dist/index.d.mts +283 -262
- package/dist/index.mjs +189 -56
- package/dist/models/infer.model.d.mts +66 -0
- package/dist/models/infer.model.mjs +1 -0
- package/dist/models/misc.model.d.mts +153 -0
- package/dist/models/misc.model.mjs +1 -0
- package/dist/models/schema.plain.model.d.mts +92 -0
- package/dist/models/schema.plain.model.mjs +1 -0
- package/dist/models/schema.typed.model.d.mts +96 -0
- package/dist/models/schema.typed.model.mjs +1 -0
- package/dist/models/transform.model.d.mts +59 -0
- package/dist/models/transform.model.mjs +1 -0
- package/dist/models/validation.model.d.mts +81 -0
- package/dist/models/validation.model.mjs +21 -0
- package/dist/schematic.d.mts +20 -6
- package/dist/schematic.mjs +7 -12
- package/dist/validation/property.validation.d.mts +1 -1
- package/dist/validation/property.validation.mjs +21 -17
- package/dist/validation/value.validation.d.mts +2 -2
- package/dist/validation/value.validation.mjs +63 -11
- package/package.json +3 -3
- package/src/constants.ts +84 -18
- package/src/helpers.ts +162 -4
- package/src/index.ts +3 -1
- package/src/models/infer.model.ts +105 -0
- package/src/models/misc.model.ts +212 -0
- package/src/models/schema.plain.model.ts +110 -0
- package/src/models/schema.typed.model.ts +109 -0
- package/src/models/transform.model.ts +85 -0
- package/src/models/validation.model.ts +123 -0
- package/src/schematic.ts +29 -18
- package/src/validation/property.validation.ts +46 -36
- package/src/validation/value.validation.ts +115 -15
- package/dist/models.d.mts +0 -507
- package/dist/models.mjs +0 -18
- package/src/models.ts +0 -691
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type {Constructor} from '@oscarpalmer/atoms/models';
|
|
2
|
+
import type {Schematic} from '../schematic';
|
|
3
|
+
import type {ExtractValueNames, ValueName, Values} from './misc.model';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A generic schema allowing {@link NestedSchema}, {@link SchemaEntry}, or arrays of {@link SchemaEntry} as values
|
|
7
|
+
*/
|
|
8
|
+
export type PlainSchema = {
|
|
9
|
+
[key: string]: PlainSchema | SchemaEntry | SchemaEntry[] | undefined;
|
|
10
|
+
} & {
|
|
11
|
+
$required?: never;
|
|
12
|
+
$type?: never;
|
|
13
|
+
$validators?: never;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A schema for validating objects
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const schema: Schema = {
|
|
22
|
+
* name: 'string',
|
|
23
|
+
* age: 'number',
|
|
24
|
+
* tags: ['string', 'number'],
|
|
25
|
+
* };
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export type Schema = SchemaIndex;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A union of all valid types for a single schema entry
|
|
32
|
+
*
|
|
33
|
+
* Can be a {@link Constructor}, nested {@link Schema}, {@link SchemaProperty}, {@link Schematic}, {@link ValueName} string, or a custom validator function
|
|
34
|
+
*/
|
|
35
|
+
export type SchemaEntry =
|
|
36
|
+
| Constructor
|
|
37
|
+
| PlainSchema
|
|
38
|
+
| SchemaProperty
|
|
39
|
+
| Schematic<unknown>
|
|
40
|
+
| ValueName
|
|
41
|
+
| ((value: unknown) => boolean);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Index signature interface backing {@link Schema}, allowing string-keyed entries of {@link NestedSchema}, {@link SchemaEntry}, or arrays of {@link SchemaEntry}
|
|
45
|
+
*/
|
|
46
|
+
export interface SchemaIndex {
|
|
47
|
+
[key: string]: PlainSchema | SchemaEntry | SchemaEntry[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A property definition with explicit type(s), an optional requirement flag, and optional validators
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const prop: SchemaProperty = {
|
|
56
|
+
* $required: false,
|
|
57
|
+
* $type: ['string', 'number'],
|
|
58
|
+
* $validators: {
|
|
59
|
+
* string: (v) => v.length > 0,
|
|
60
|
+
* number: (v) => v > 0,
|
|
61
|
+
* },
|
|
62
|
+
* };
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export type SchemaProperty = {
|
|
66
|
+
/**
|
|
67
|
+
* Whether the property is required _(defaults to `true`)_
|
|
68
|
+
*/
|
|
69
|
+
$required?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The type(s) the property value must match; a single {@link SchemaPropertyType} or an array
|
|
72
|
+
*/
|
|
73
|
+
$type: SchemaPropertyType | SchemaPropertyType[];
|
|
74
|
+
/**
|
|
75
|
+
* Optional validators keyed by {@link ValueName}, applied during validation
|
|
76
|
+
*/
|
|
77
|
+
$validators?: PropertyValidators<SchemaPropertyType | SchemaPropertyType[]>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A union of valid types for a {@link SchemaProperty}'s `$type` field
|
|
82
|
+
*
|
|
83
|
+
* Can be a {@link Constructor}, {@link PlainSchema}, {@link Schematic}, {@link ValueName} string, or a custom validator function
|
|
84
|
+
*/
|
|
85
|
+
export type SchemaPropertyType =
|
|
86
|
+
| Constructor
|
|
87
|
+
| PlainSchema
|
|
88
|
+
| Schematic<unknown>
|
|
89
|
+
| ValueName
|
|
90
|
+
| ((value: unknown) => boolean);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A map of optional validator functions keyed by {@link ValueName}, used to add custom validation to {@link SchemaProperty} definitions
|
|
94
|
+
*
|
|
95
|
+
* Each key may hold a single validator or an array of validators that receive the typed value
|
|
96
|
+
*
|
|
97
|
+
* @template Value - `$type` value(s) to derive validator keys from
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const validators: PropertyValidators<'string'> = {
|
|
102
|
+
* string: (value) => value.length > 0,
|
|
103
|
+
* };
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export type PropertyValidators<Value> = {
|
|
107
|
+
[Key in ExtractValueNames<Value>]?:
|
|
108
|
+
| ((value: Values[Key]) => boolean)
|
|
109
|
+
| Array<(value: Values[Key]) => boolean>;
|
|
110
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type {PlainObject, Simplify} from '@oscarpalmer/atoms/models';
|
|
2
|
+
import type {Schematic} from '../schematic';
|
|
3
|
+
import type {OptionalKeys, RequiredKeys} from './misc.model';
|
|
4
|
+
import type {PropertyValidators} from './schema.plain.model';
|
|
5
|
+
import type {ToSchemaPropertyType, ToSchemaType} from './transform.model';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A typed optional property definition generated by {@link TypedSchema} for optional keys, with `$required` set to `false` and excludes `undefined` from the type
|
|
9
|
+
*
|
|
10
|
+
* @template Value - Property's type _(including `undefined`)_
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // For `{ name?: string }`, the `name` key produces:
|
|
15
|
+
* // TypedPropertyOptional<string | undefined>
|
|
16
|
+
* // => { $required: false; $type: 'string'; ... }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export type TypedPropertyOptional<Value> = {
|
|
20
|
+
/**
|
|
21
|
+
* The property is not required
|
|
22
|
+
*/
|
|
23
|
+
$required: false;
|
|
24
|
+
/**
|
|
25
|
+
* The type(s) of the property
|
|
26
|
+
*/
|
|
27
|
+
$type: ToSchemaPropertyType<Exclude<Value, undefined>>;
|
|
28
|
+
/**
|
|
29
|
+
* Custom validators for the property and its types
|
|
30
|
+
*/
|
|
31
|
+
$validators?: PropertyValidators<ToSchemaPropertyType<Exclude<Value, undefined>>>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A typed required property definition generated by {@link TypedSchema} for required keys, with `$required` defaulting to `true`
|
|
36
|
+
*
|
|
37
|
+
* @template Value - Property's type
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* // For `{ name: string }`, the `name` key produces:
|
|
42
|
+
* // TypedPropertyRequired<string>
|
|
43
|
+
* // => { $required?: true; $type: 'string'; ... }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export type TypedPropertyRequired<Value> = {
|
|
47
|
+
/**
|
|
48
|
+
* The property is required _(defaults to `true`)_
|
|
49
|
+
*/
|
|
50
|
+
$required?: true;
|
|
51
|
+
/**
|
|
52
|
+
* The type(s) of the property
|
|
53
|
+
*/
|
|
54
|
+
$type: ToSchemaPropertyType<Value>;
|
|
55
|
+
/**
|
|
56
|
+
* Custom validators for the property and its types
|
|
57
|
+
*/
|
|
58
|
+
$validators?: PropertyValidators<ToSchemaPropertyType<Value>>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Creates a schema type constrained to match a TypeScript type
|
|
63
|
+
*
|
|
64
|
+
* 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}
|
|
65
|
+
*
|
|
66
|
+
* @template Model - Object type to generate a schema for
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* type User = { name: string; age: number; bio?: string };
|
|
71
|
+
*
|
|
72
|
+
* const schema: TypedSchema<User> = {
|
|
73
|
+
* name: 'string',
|
|
74
|
+
* age: 'number',
|
|
75
|
+
* bio: { $required: false, $type: 'string' },
|
|
76
|
+
* };
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export type TypedSchema<Model extends PlainObject> = Simplify<
|
|
80
|
+
{
|
|
81
|
+
[Key in RequiredKeys<Model>]: Model[Key] extends PlainObject
|
|
82
|
+
? TypedSchemaRequired<Model[Key]> | Schematic<Model[Key]>
|
|
83
|
+
: ToSchemaType<Model[Key]> | TypedPropertyRequired<Model[Key]>;
|
|
84
|
+
} & {
|
|
85
|
+
[Key in OptionalKeys<Model>]: Exclude<Model[Key], undefined> extends PlainObject
|
|
86
|
+
?
|
|
87
|
+
| TypedSchemaOptional<Exclude<Model[Key], undefined>>
|
|
88
|
+
| Schematic<Exclude<Model[Key], undefined>>
|
|
89
|
+
: TypedPropertyOptional<Model[Key]>;
|
|
90
|
+
}
|
|
91
|
+
>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A {@link TypedSchema} variant for optional nested objects, with `$required` fixed to `false`
|
|
95
|
+
*
|
|
96
|
+
* @template Model - Nested object type
|
|
97
|
+
*/
|
|
98
|
+
type TypedSchemaOptional<Model extends PlainObject> = {
|
|
99
|
+
$required: false;
|
|
100
|
+
} & TypedSchema<Model>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A {@link TypedSchema} variant for required nested objects, with `$required` defaulting to `true`
|
|
104
|
+
*
|
|
105
|
+
* @template Model - Nested object type
|
|
106
|
+
*/
|
|
107
|
+
type TypedSchemaRequired<Model extends PlainObject> = {
|
|
108
|
+
$required?: true;
|
|
109
|
+
} & TypedSchema<Model>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type {PlainObject} from '@oscarpalmer/atoms/models';
|
|
2
|
+
import type {Schematic} from '../schematic';
|
|
3
|
+
import type {DeduplicateTuple, UnionToTuple, UnwrapSingle, Values} from './misc.model';
|
|
4
|
+
import type {TypedSchema} from './schema.typed.model';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Maps each element of a tuple through {@link ToValueType}
|
|
8
|
+
*
|
|
9
|
+
* @template Value - Tuple of types to map
|
|
10
|
+
*/
|
|
11
|
+
export type MapToValueTypes<Value extends unknown[]> = Value extends [infer Head, ...infer Tail]
|
|
12
|
+
? [ToValueType<Head>, ...MapToValueTypes<Tail>]
|
|
13
|
+
: [];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Maps each element of a tuple through {@link ToSchemaPropertyTypeEach}
|
|
17
|
+
*
|
|
18
|
+
* @template Value - Tuple of types to map
|
|
19
|
+
*/
|
|
20
|
+
export type MapToSchemaPropertyTypes<Value extends unknown[]> = Value extends [
|
|
21
|
+
infer Head,
|
|
22
|
+
...infer Tail,
|
|
23
|
+
]
|
|
24
|
+
? [ToSchemaPropertyTypeEach<Head>, ...MapToSchemaPropertyTypes<Tail>]
|
|
25
|
+
: [];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Converts a type into its corresponding {@link SchemaPropertyType}-representation
|
|
29
|
+
*
|
|
30
|
+
* Deduplicates and unwraps single-element tuples via {@link UnwrapSingle}
|
|
31
|
+
*
|
|
32
|
+
* @template Value - type to convert
|
|
33
|
+
*/
|
|
34
|
+
export type ToSchemaPropertyType<Value> = UnwrapSingle<
|
|
35
|
+
DeduplicateTuple<MapToSchemaPropertyTypes<UnionToTuple<Value>>>
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Converts a single type to its schema property equivalent
|
|
40
|
+
*
|
|
41
|
+
* {@link NestedSchema} values have `$required` stripped, plain objects become {@link TypedSchema}, and primitives go through {@link ToValueType}
|
|
42
|
+
*
|
|
43
|
+
* @template Value - type to convert
|
|
44
|
+
*/
|
|
45
|
+
export type ToSchemaPropertyTypeEach<Value> = Value extends PlainObject
|
|
46
|
+
? TypedSchema<Value>
|
|
47
|
+
: ToValueType<Value>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Converts a type into its corresponding {@link ValueName}-representation
|
|
51
|
+
*
|
|
52
|
+
* Deduplicates and unwraps single-element tuples via {@link UnwrapSingle}
|
|
53
|
+
*
|
|
54
|
+
* @template Value - type to convert
|
|
55
|
+
*/
|
|
56
|
+
export type ToSchemaType<Value> = UnwrapSingle<
|
|
57
|
+
DeduplicateTuple<MapToValueTypes<UnionToTuple<Value>>>
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Maps a type to its {@link ValueName} string equivalent
|
|
62
|
+
*
|
|
63
|
+
* 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
|
|
64
|
+
*
|
|
65
|
+
* @template Value - type to map
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* // ToValueType<string> => 'string'
|
|
70
|
+
* // ToValueType<number[]> => 'array'
|
|
71
|
+
* // ToValueType<Date> => 'date'
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export type ToValueType<Value> =
|
|
75
|
+
Value extends Schematic<any>
|
|
76
|
+
? Value
|
|
77
|
+
: {
|
|
78
|
+
[Key in keyof Omit<Values, 'object'>]: Value extends Values[Key] ? Key : never;
|
|
79
|
+
}[keyof Omit<Values, 'object'>] extends infer Match
|
|
80
|
+
? [Match] extends [never]
|
|
81
|
+
? Value extends object
|
|
82
|
+
? 'object' | ((value: unknown) => value is Value)
|
|
83
|
+
: (value: unknown) => value is Value
|
|
84
|
+
: Match
|
|
85
|
+
: never;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type {GenericCallback} from '@oscarpalmer/atoms/models';
|
|
2
|
+
import {join} from '@oscarpalmer/atoms/string';
|
|
3
|
+
import {NAME_ERROR_SCHEMATIC, NAME_ERROR_VALIDATION} from '../constants';
|
|
4
|
+
import type {Schematic} from '../schematic';
|
|
5
|
+
import type {ValueName} from './misc.model';
|
|
6
|
+
|
|
7
|
+
// #region Reporting
|
|
8
|
+
|
|
9
|
+
export type ReportingInformation = Record<ReportingType, boolean>;
|
|
10
|
+
|
|
11
|
+
export type ReportingType = 'all' | 'first' | 'none' | 'throw';
|
|
12
|
+
|
|
13
|
+
// #endregion
|
|
14
|
+
|
|
15
|
+
// #region Schematic validation
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A custom error class for schematic validation failures
|
|
19
|
+
*/
|
|
20
|
+
export class SchematicError extends Error {
|
|
21
|
+
constructor(message: string) {
|
|
22
|
+
super(message);
|
|
23
|
+
|
|
24
|
+
this.name = NAME_ERROR_SCHEMATIC;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// #endregion
|
|
29
|
+
|
|
30
|
+
// #region Validated property
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The runtime representation of a parsed schema property, used internally during validation
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const parsed: ValidatedProperty = {
|
|
38
|
+
* key: 'age',
|
|
39
|
+
* required: true,
|
|
40
|
+
* types: ['number'],
|
|
41
|
+
* validators: { number: [(v) => v > 0] },
|
|
42
|
+
* };
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export type ValidatedProperty = {
|
|
46
|
+
/**
|
|
47
|
+
* The property name in the schema
|
|
48
|
+
*/
|
|
49
|
+
key: ValidatedPropertyKey;
|
|
50
|
+
/**
|
|
51
|
+
* Whether the property is required
|
|
52
|
+
*/
|
|
53
|
+
required: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* The allowed types for this property
|
|
56
|
+
*/
|
|
57
|
+
types: ValidatedPropertyType[];
|
|
58
|
+
/**
|
|
59
|
+
* Custom validators grouped by {@link ValueName}
|
|
60
|
+
*/
|
|
61
|
+
validators: ValidatedPropertyValidators;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Property name in schema
|
|
66
|
+
*/
|
|
67
|
+
export type ValidatedPropertyKey = {
|
|
68
|
+
/**
|
|
69
|
+
* Full property key, including parent keys for nested properties _(e.g., `address.street`)_
|
|
70
|
+
*/
|
|
71
|
+
full: string;
|
|
72
|
+
/**
|
|
73
|
+
* The last segment of the property key _(e.g., `street` for `address.street`)_
|
|
74
|
+
*/
|
|
75
|
+
short: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A union of valid types for a {@link ValidatedProperty}'s `types` array
|
|
80
|
+
*
|
|
81
|
+
* Can be a callback _(custom validator)_, a {@link Schematic}, a nested {@link ValidatedProperty}, or a {@link ValueName} string
|
|
82
|
+
*/
|
|
83
|
+
export type ValidatedPropertyType =
|
|
84
|
+
| GenericCallback
|
|
85
|
+
| ValidatedProperty[]
|
|
86
|
+
| Schematic<unknown>
|
|
87
|
+
| ValueName;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A map of validator functions keyed by {@link ValueName}, used at runtime in {@link ValidatedProperty}
|
|
91
|
+
*
|
|
92
|
+
* Each key holds an array of validator functions that receive an `unknown` value and return a `boolean`
|
|
93
|
+
*/
|
|
94
|
+
export type ValidatedPropertyValidators = {
|
|
95
|
+
[Key in ValueName]?: Array<(value: unknown) => boolean>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// #endregion
|
|
99
|
+
|
|
100
|
+
// #region Property validation
|
|
101
|
+
|
|
102
|
+
export class ValidationError extends Error {
|
|
103
|
+
constructor(readonly information: ValidationInformation[]) {
|
|
104
|
+
super(
|
|
105
|
+
join(
|
|
106
|
+
information.map(item => item.message),
|
|
107
|
+
'; ',
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
this.name = NAME_ERROR_VALIDATION;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type ValidationInformation = {
|
|
116
|
+
key: ValidationInformationKey;
|
|
117
|
+
message: string;
|
|
118
|
+
validator?: GenericCallback;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type ValidationInformationKey = ValidatedPropertyKey;
|
|
122
|
+
|
|
123
|
+
// #endregion
|
package/src/schematic.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import {isPlainObject} from '@oscarpalmer/atoms/is';
|
|
2
2
|
import type {PlainObject} from '@oscarpalmer/atoms/models';
|
|
3
|
-
import {
|
|
4
|
-
import {isSchematic} from './helpers';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type TypedSchema,
|
|
10
|
-
type ValidatedProperty,
|
|
11
|
-
} from './models';
|
|
3
|
+
import {SCHEMATIC_MESSAGE_SCHEMA_INVALID_TYPE, PROPERTY_SCHEMATIC} from './constants';
|
|
4
|
+
import {getReporting, isSchematic} from './helpers';
|
|
5
|
+
import type {Infer} from './models/infer.model';
|
|
6
|
+
import type {Schema} from './models/schema.plain.model';
|
|
7
|
+
import type {TypedSchema} from './models/schema.typed.model';
|
|
8
|
+
import {SchematicError, type ValidatedProperty} from './models/validation.model';
|
|
12
9
|
import {getProperties} from './validation/property.validation';
|
|
13
10
|
import {validateObject} from './validation/value.validation';
|
|
14
11
|
|
|
@@ -21,7 +18,7 @@ export class Schematic<Model> {
|
|
|
21
18
|
#properties: ValidatedProperty[];
|
|
22
19
|
|
|
23
20
|
constructor(properties: ValidatedProperty[]) {
|
|
24
|
-
Object.defineProperty(this,
|
|
21
|
+
Object.defineProperty(this, PROPERTY_SCHEMATIC, {
|
|
25
22
|
value: true,
|
|
26
23
|
});
|
|
27
24
|
|
|
@@ -30,18 +27,32 @@ export class Schematic<Model> {
|
|
|
30
27
|
|
|
31
28
|
/**
|
|
32
29
|
* Does the value match the schema?
|
|
33
|
-
*
|
|
30
|
+
*
|
|
31
|
+
* 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.
|
|
32
|
+
* @param value Value to validate
|
|
33
|
+
* @param errors Throws an error for the first validation failure
|
|
34
|
+
* @returns `true` if the value matches the schema, otherwise throws an error
|
|
35
|
+
*/
|
|
36
|
+
is(value: unknown, errors: 'throw'): asserts value is Model;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Does the value match the schema?
|
|
40
|
+
*
|
|
41
|
+
* Will validate that the value matches the schema and return `true` or `false`, without any validation information for validation failures.
|
|
42
|
+
* @param value Value to validate
|
|
34
43
|
* @returns `true` if the value matches the schema, otherwise `false`
|
|
35
44
|
*/
|
|
36
|
-
is(value: unknown): value is Model
|
|
37
|
-
|
|
45
|
+
is(value: unknown): value is Model;
|
|
46
|
+
|
|
47
|
+
is(value: unknown, errors?: unknown): boolean {
|
|
48
|
+
return validateObject(value, this.#properties, getReporting(errors));
|
|
38
49
|
}
|
|
39
50
|
}
|
|
40
51
|
|
|
41
52
|
/**
|
|
42
53
|
* Create a schematic from a schema
|
|
43
|
-
* @template Model
|
|
44
|
-
* @param schema
|
|
54
|
+
* @template Model Schema type
|
|
55
|
+
* @param schema Schema to create the schematic from
|
|
45
56
|
* @throws Throws {@link SchematicError} if the schema can not be converted into a schematic
|
|
46
57
|
* @returns A schematic for the given schema
|
|
47
58
|
*/
|
|
@@ -49,8 +60,8 @@ export function schematic<Model extends Schema>(schema: Model): Schematic<Infer<
|
|
|
49
60
|
|
|
50
61
|
/**
|
|
51
62
|
* Create a schematic from a typed schema
|
|
52
|
-
* @template Model
|
|
53
|
-
* @param schema
|
|
63
|
+
* @template Model Existing type
|
|
64
|
+
* @param schema Typed schema to create the schematic from
|
|
54
65
|
* @throws Throws {@link SchematicError} if the schema can not be converted into a schematic
|
|
55
66
|
* @returns A schematic for the given typed schema
|
|
56
67
|
*/
|
|
@@ -62,7 +73,7 @@ export function schematic<Model extends Schema>(schema: Model): Schematic<Model>
|
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
if (!isPlainObject(schema)) {
|
|
65
|
-
throw new SchematicError(
|
|
76
|
+
throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_TYPE);
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
return new Schematic<Model>(getProperties(schema));
|
|
@@ -2,33 +2,31 @@ import {isConstructor, isPlainObject} from '@oscarpalmer/atoms/is';
|
|
|
2
2
|
import type {PlainObject} from '@oscarpalmer/atoms/models';
|
|
3
3
|
import {join} from '@oscarpalmer/atoms/string';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
SCHEMATIC_MESSAGE_SCHEMA_INVALID_EMPTY,
|
|
6
|
+
SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED,
|
|
7
|
+
SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE,
|
|
8
|
+
SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED,
|
|
9
|
+
SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE,
|
|
10
|
+
SCHEMATIC_MESSAGE_VALIDATOR_INVALID_KEY,
|
|
11
|
+
SCHEMATIC_MESSAGE_VALIDATOR_INVALID_TYPE,
|
|
12
|
+
SCHEMATIC_MESSAGE_VALIDATOR_INVALID_VALUE,
|
|
13
13
|
PROPERTY_REQUIRED,
|
|
14
14
|
PROPERTY_TYPE,
|
|
15
15
|
PROPERTY_VALIDATORS,
|
|
16
16
|
TEMPLATE_PATTERN,
|
|
17
|
-
TEMPLATE_PATTERN_KEY,
|
|
18
|
-
TEMPLATE_PATTERN_PROPERTY,
|
|
19
17
|
TYPE_ALL,
|
|
20
18
|
TYPE_OBJECT,
|
|
21
19
|
TYPE_UNDEFINED,
|
|
22
20
|
VALIDATABLE_TYPES,
|
|
23
21
|
} from '../constants';
|
|
24
22
|
import {instanceOf, isSchematic} from '../helpers';
|
|
23
|
+
import type {ValueName} from '../models/misc.model';
|
|
25
24
|
import {
|
|
26
25
|
SchematicError,
|
|
27
26
|
type ValidatedProperty,
|
|
28
27
|
type ValidatedPropertyType,
|
|
29
28
|
type ValidatedPropertyValidators,
|
|
30
|
-
|
|
31
|
-
} from '../models';
|
|
29
|
+
} from '../models/validation.model';
|
|
32
30
|
|
|
33
31
|
function getDisallowedProperty(obj: PlainObject): string | undefined {
|
|
34
32
|
if (PROPERTY_REQUIRED in obj) {
|
|
@@ -50,7 +48,7 @@ export function getProperties(
|
|
|
50
48
|
fromType?: boolean,
|
|
51
49
|
): ValidatedProperty[] {
|
|
52
50
|
if (Object.keys(original).length === 0) {
|
|
53
|
-
throw new SchematicError(
|
|
51
|
+
throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_EMPTY);
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
if (fromType ?? false) {
|
|
@@ -58,10 +56,10 @@ export function getProperties(
|
|
|
58
56
|
|
|
59
57
|
if (property != null) {
|
|
60
58
|
throw new SchematicError(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
),
|
|
59
|
+
SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED.replace(
|
|
60
|
+
TEMPLATE_PATTERN,
|
|
61
|
+
prefix!,
|
|
62
|
+
).replace(TEMPLATE_PATTERN, property),
|
|
65
63
|
);
|
|
66
64
|
}
|
|
67
65
|
}
|
|
@@ -74,12 +72,15 @@ export function getProperties(
|
|
|
74
72
|
for (let keyIndex = 0; keyIndex < keysLength; keyIndex += 1) {
|
|
75
73
|
const key = keys[keyIndex];
|
|
76
74
|
|
|
77
|
-
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
75
|
+
const prefixed = join([prefix, key], '.');
|
|
81
76
|
const value = original[key];
|
|
82
77
|
|
|
78
|
+
if (value == null) {
|
|
79
|
+
throw new SchematicError(
|
|
80
|
+
SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE.replace(TEMPLATE_PATTERN, prefixed),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
83
84
|
const types: ValidatedPropertyType[] = [];
|
|
84
85
|
|
|
85
86
|
let required = true;
|
|
@@ -89,11 +90,9 @@ export function getProperties(
|
|
|
89
90
|
required = getRequired(key, value) ?? required;
|
|
90
91
|
validators = getValidators(value[PROPERTY_VALIDATORS]);
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
types.push(TYPE_OBJECT, ...getTypes(key, value, prefix));
|
|
96
|
-
}
|
|
93
|
+
const hasType = PROPERTY_TYPE in value;
|
|
94
|
+
|
|
95
|
+
types.push(...getTypes(key, hasType ? value[PROPERTY_TYPE] : value, prefix, hasType));
|
|
97
96
|
} else {
|
|
98
97
|
types.push(...getTypes(key, value, prefix));
|
|
99
98
|
}
|
|
@@ -103,9 +102,12 @@ export function getProperties(
|
|
|
103
102
|
}
|
|
104
103
|
|
|
105
104
|
properties.push({
|
|
106
|
-
key,
|
|
107
105
|
types,
|
|
108
106
|
validators,
|
|
107
|
+
key: {
|
|
108
|
+
full: prefixed,
|
|
109
|
+
short: key,
|
|
110
|
+
},
|
|
109
111
|
required: required && !types.includes(TYPE_UNDEFINED),
|
|
110
112
|
});
|
|
111
113
|
}
|
|
@@ -120,7 +122,7 @@ function getRequired(key: string, obj: PlainObject): boolean | undefined {
|
|
|
120
122
|
|
|
121
123
|
if (typeof obj[PROPERTY_REQUIRED] !== 'boolean') {
|
|
122
124
|
throw new SchematicError(
|
|
123
|
-
|
|
125
|
+
SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED.replace(TEMPLATE_PATTERN, key),
|
|
124
126
|
);
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -147,7 +149,7 @@ function getTypes(
|
|
|
147
149
|
break;
|
|
148
150
|
|
|
149
151
|
case isPlainObject(value):
|
|
150
|
-
types.push(
|
|
152
|
+
types.push(getProperties(value, join([prefix, key], '.'), fromType));
|
|
151
153
|
break;
|
|
152
154
|
|
|
153
155
|
case isSchematic(value):
|
|
@@ -160,14 +162,20 @@ function getTypes(
|
|
|
160
162
|
|
|
161
163
|
default:
|
|
162
164
|
throw new SchematicError(
|
|
163
|
-
|
|
165
|
+
SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace(
|
|
166
|
+
TEMPLATE_PATTERN,
|
|
167
|
+
join([prefix, key], '.'),
|
|
168
|
+
),
|
|
164
169
|
);
|
|
165
170
|
}
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
if (types.length === 0) {
|
|
169
174
|
throw new SchematicError(
|
|
170
|
-
|
|
175
|
+
SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace(
|
|
176
|
+
TEMPLATE_PATTERN,
|
|
177
|
+
join([prefix, key], '.'),
|
|
178
|
+
),
|
|
171
179
|
);
|
|
172
180
|
}
|
|
173
181
|
|
|
@@ -182,7 +190,7 @@ function getValidators(original: unknown): ValidatedPropertyValidators {
|
|
|
182
190
|
}
|
|
183
191
|
|
|
184
192
|
if (!isPlainObject(original)) {
|
|
185
|
-
throw new TypeError(
|
|
193
|
+
throw new TypeError(SCHEMATIC_MESSAGE_VALIDATOR_INVALID_TYPE);
|
|
186
194
|
}
|
|
187
195
|
|
|
188
196
|
const keys = Object.keys(original);
|
|
@@ -192,17 +200,19 @@ function getValidators(original: unknown): ValidatedPropertyValidators {
|
|
|
192
200
|
const key = keys[index];
|
|
193
201
|
|
|
194
202
|
if (!VALIDATABLE_TYPES.has(key as never)) {
|
|
195
|
-
throw new TypeError(
|
|
203
|
+
throw new TypeError(SCHEMATIC_MESSAGE_VALIDATOR_INVALID_KEY.replace(TEMPLATE_PATTERN, key));
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
const value = (original as PlainObject)[key];
|
|
199
207
|
|
|
200
|
-
validators[key as ValueName] = (Array.isArray(value) ? value : [value]).
|
|
208
|
+
validators[key as ValueName] = (Array.isArray(value) ? value : [value]).map(item => {
|
|
201
209
|
if (typeof item !== 'function') {
|
|
202
|
-
throw new TypeError(
|
|
210
|
+
throw new TypeError(
|
|
211
|
+
SCHEMATIC_MESSAGE_VALIDATOR_INVALID_VALUE.replace(TEMPLATE_PATTERN, key),
|
|
212
|
+
);
|
|
203
213
|
}
|
|
204
214
|
|
|
205
|
-
return
|
|
215
|
+
return item;
|
|
206
216
|
});
|
|
207
217
|
}
|
|
208
218
|
|