@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oscarpalmer/jhunal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Flies free beneath the glistening moons…",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"schema",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"watch": "npx vite build --watch"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@oscarpalmer/atoms": "^0.
|
|
41
|
+
"@oscarpalmer/atoms": "^0.169"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/node": "^25.5",
|
|
@@ -51,4 +51,4 @@
|
|
|
51
51
|
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
|
52
52
|
},
|
|
53
53
|
"packageManager": "npm@11.11.1"
|
|
54
|
-
}
|
|
54
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -1,42 +1,106 @@
|
|
|
1
|
-
import type {ValueName} from './models';
|
|
1
|
+
import type {ValueName} from './models/misc.model';
|
|
2
|
+
import type {ReportingType} from './models/validation.model';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export const EXPRESSION_PROPERTY = /(^|\.)\$(required|type|validators)(\.|$)/;
|
|
4
|
+
// #region Misc.
|
|
6
5
|
|
|
7
6
|
export const MESSAGE_CONSTRUCTOR = 'Expected a constructor function';
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export const MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED =
|
|
12
|
-
"'<key>.<property>' property is not allowed for schemas in $type";
|
|
8
|
+
// #endregion
|
|
13
9
|
|
|
14
|
-
|
|
10
|
+
// #region Names
|
|
15
11
|
|
|
16
|
-
export const
|
|
12
|
+
export const NAME_SCHEMATIC = 'Schematic';
|
|
17
13
|
|
|
18
|
-
export const
|
|
14
|
+
export const NAME_ERROR_SCHEMATIC = 'SchematicError';
|
|
19
15
|
|
|
20
|
-
export const
|
|
16
|
+
export const NAME_ERROR_VALIDATION = 'ValidationError';
|
|
21
17
|
|
|
22
|
-
|
|
18
|
+
// #endregion
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
"Validator '<>' must be a function or an array of functions";
|
|
20
|
+
// #region Properties
|
|
26
21
|
|
|
27
22
|
export const PROPERTY_REQUIRED = '$required';
|
|
28
23
|
|
|
24
|
+
export const PROPERTY_SCHEMATIC = '$schematic';
|
|
25
|
+
|
|
29
26
|
export const PROPERTY_TYPE = '$type';
|
|
30
27
|
|
|
31
28
|
export const PROPERTY_VALIDATORS = '$validators';
|
|
32
29
|
|
|
33
|
-
|
|
30
|
+
// #endregion
|
|
31
|
+
|
|
32
|
+
// #region Property validation
|
|
33
|
+
|
|
34
|
+
export const VALIDATION_MESSAGE_INVALID_INPUT = "Expected 'object' as input but received <>";
|
|
35
|
+
|
|
36
|
+
export const VALIDATION_MESSAGE_INVALID_REQUIRED = "Expected <> for required property '<>'";
|
|
37
|
+
|
|
38
|
+
export const VALIDATION_MESSAGE_INVALID_TYPE = "Expected <> for '<>' but received <>";
|
|
39
|
+
|
|
40
|
+
export const VALIDATION_MESSAGE_INVALID_VALUE =
|
|
41
|
+
"Value does not satisfy validator for '<>' and type '<>'";
|
|
42
|
+
|
|
43
|
+
export const VALIDATION_MESSAGE_INVALID_VALUE_SUFFIX = ' at index <>';
|
|
44
|
+
|
|
45
|
+
// #endregion
|
|
46
|
+
|
|
47
|
+
// #region Reporting
|
|
48
|
+
|
|
49
|
+
export const REPORTING_ALL: ReportingType = 'all';
|
|
50
|
+
|
|
51
|
+
export const REPORTING_FIRST: ReportingType = 'first';
|
|
52
|
+
|
|
53
|
+
export const REPORTING_NONE: ReportingType = 'none';
|
|
54
|
+
|
|
55
|
+
export const REPORTING_THROW: ReportingType = 'throw';
|
|
56
|
+
|
|
57
|
+
export const REPORTING_TYPES = new Set<ReportingType>([
|
|
58
|
+
REPORTING_ALL,
|
|
59
|
+
REPORTING_FIRST,
|
|
60
|
+
REPORTING_NONE,
|
|
61
|
+
REPORTING_THROW,
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
// #endregion
|
|
65
|
+
|
|
66
|
+
// #region Schematic validation
|
|
67
|
+
|
|
68
|
+
export const SCHEMATIC_MESSAGE_SCHEMA_INVALID_EMPTY = 'Schema must have at least one property';
|
|
69
|
+
|
|
70
|
+
export const SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED =
|
|
71
|
+
"'<>.<>' property is not allowed for schemas in $type";
|
|
72
|
+
|
|
73
|
+
export const SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE =
|
|
74
|
+
"'<>' property must not be 'null' or 'undefined'";
|
|
75
|
+
|
|
76
|
+
export const SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED =
|
|
77
|
+
"'<>.$required' property must be a boolean";
|
|
78
|
+
|
|
79
|
+
export const SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE =
|
|
80
|
+
"'<>' property must be of a valid type";
|
|
81
|
+
|
|
82
|
+
export const SCHEMATIC_MESSAGE_SCHEMA_INVALID_TYPE = 'Schema must be an object';
|
|
83
|
+
|
|
84
|
+
export const SCHEMATIC_MESSAGE_VALIDATOR_INVALID_KEY = "Validator '<>' does not exist";
|
|
85
|
+
|
|
86
|
+
export const SCHEMATIC_MESSAGE_VALIDATOR_INVALID_TYPE = 'Validators must be an object';
|
|
87
|
+
|
|
88
|
+
export const SCHEMATIC_MESSAGE_VALIDATOR_INVALID_VALUE =
|
|
89
|
+
"Validator '<>' must be a function or an array of functions";
|
|
90
|
+
|
|
91
|
+
// #endregion
|
|
92
|
+
|
|
93
|
+
// #region Templates
|
|
34
94
|
|
|
35
95
|
export const TEMPLATE_PATTERN = '<>';
|
|
36
96
|
|
|
37
|
-
|
|
97
|
+
// #endregion
|
|
98
|
+
|
|
99
|
+
// #region Types
|
|
38
100
|
|
|
39
|
-
export const
|
|
101
|
+
export const TYPE_ARRAY = 'array';
|
|
102
|
+
|
|
103
|
+
export const TYPE_NULL = 'null';
|
|
40
104
|
|
|
41
105
|
export const TYPE_OBJECT = 'object';
|
|
42
106
|
|
|
@@ -55,3 +119,5 @@ export const VALIDATABLE_TYPES = new Set<ValueName>([
|
|
|
55
119
|
]);
|
|
56
120
|
|
|
57
121
|
export const TYPE_ALL = new Set<ValueName>([...VALIDATABLE_TYPES, 'null', TYPE_UNDEFINED]);
|
|
122
|
+
|
|
123
|
+
// #endregion
|
package/src/helpers.ts
CHANGED
|
@@ -1,8 +1,134 @@
|
|
|
1
|
-
import {isConstructor} from '@oscarpalmer/atoms/is';
|
|
1
|
+
import {isConstructor, isPlainObject} from '@oscarpalmer/atoms/is';
|
|
2
2
|
import type {Constructor} from '@oscarpalmer/atoms/models';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
MESSAGE_CONSTRUCTOR,
|
|
5
|
+
NAME_SCHEMATIC,
|
|
6
|
+
PROPERTY_SCHEMATIC,
|
|
7
|
+
REPORTING_ALL,
|
|
8
|
+
REPORTING_FIRST,
|
|
9
|
+
REPORTING_NONE,
|
|
10
|
+
REPORTING_THROW,
|
|
11
|
+
REPORTING_TYPES,
|
|
12
|
+
TEMPLATE_PATTERN,
|
|
13
|
+
TYPE_ARRAY,
|
|
14
|
+
TYPE_NULL,
|
|
15
|
+
TYPE_OBJECT,
|
|
16
|
+
TYPE_UNDEFINED,
|
|
17
|
+
VALIDATION_MESSAGE_INVALID_INPUT,
|
|
18
|
+
VALIDATION_MESSAGE_INVALID_REQUIRED,
|
|
19
|
+
VALIDATION_MESSAGE_INVALID_TYPE,
|
|
20
|
+
VALIDATION_MESSAGE_INVALID_VALUE,
|
|
21
|
+
VALIDATION_MESSAGE_INVALID_VALUE_SUFFIX,
|
|
22
|
+
} from './constants';
|
|
23
|
+
import type {ValueName} from './models/misc.model';
|
|
24
|
+
import type {
|
|
25
|
+
ReportingInformation,
|
|
26
|
+
ReportingType,
|
|
27
|
+
ValidatedProperty,
|
|
28
|
+
ValidatedPropertyType,
|
|
29
|
+
} from './models/validation.model';
|
|
4
30
|
import type {Schematic} from './schematic';
|
|
5
31
|
|
|
32
|
+
export function getInvalidInputMessage(actual: unknown): string {
|
|
33
|
+
return VALIDATION_MESSAGE_INVALID_INPUT.replace(TEMPLATE_PATTERN, getValueType(actual));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getInvalidMissingMessage(property: ValidatedProperty): string {
|
|
37
|
+
let message = VALIDATION_MESSAGE_INVALID_REQUIRED.replace(
|
|
38
|
+
TEMPLATE_PATTERN,
|
|
39
|
+
renderTypes(property.types),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
message = message.replace(TEMPLATE_PATTERN, property.key.full);
|
|
43
|
+
|
|
44
|
+
return message;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getInvalidTypeMessage(property: ValidatedProperty, actual: unknown): string {
|
|
48
|
+
let message = VALIDATION_MESSAGE_INVALID_TYPE.replace(
|
|
49
|
+
TEMPLATE_PATTERN,
|
|
50
|
+
renderTypes(property.types),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
message = message.replace(TEMPLATE_PATTERN, property.key.full);
|
|
54
|
+
message = message.replace(TEMPLATE_PATTERN, getValueType(actual));
|
|
55
|
+
|
|
56
|
+
return message;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getInvalidValidatorMessage(
|
|
60
|
+
property: ValidatedProperty,
|
|
61
|
+
type: ValueName,
|
|
62
|
+
index: number,
|
|
63
|
+
length: number,
|
|
64
|
+
): string {
|
|
65
|
+
let message = VALIDATION_MESSAGE_INVALID_VALUE.replace(TEMPLATE_PATTERN, property.key.full);
|
|
66
|
+
|
|
67
|
+
message = message.replace(TEMPLATE_PATTERN, type);
|
|
68
|
+
|
|
69
|
+
if (length > 1) {
|
|
70
|
+
message += VALIDATION_MESSAGE_INVALID_VALUE_SUFFIX.replace(TEMPLATE_PATTERN, String(index));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return message;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getPropertyType(original: ValidatedPropertyType): string {
|
|
77
|
+
if (typeof original === 'function') {
|
|
78
|
+
return 'a validated value';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(original)) {
|
|
82
|
+
return `'${TYPE_OBJECT}'`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (isSchematic(original)) {
|
|
86
|
+
return `a ${NAME_SCHEMATIC}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return `'${String(original)}'`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getReporting(value: unknown): ReportingInformation {
|
|
93
|
+
const type = REPORTING_TYPES.has(value as ReportingType)
|
|
94
|
+
? (value as ReportingType)
|
|
95
|
+
: REPORTING_NONE;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
[REPORTING_ALL]: type === REPORTING_ALL,
|
|
99
|
+
[REPORTING_FIRST]: type === REPORTING_FIRST,
|
|
100
|
+
[REPORTING_NONE]: type === REPORTING_NONE,
|
|
101
|
+
[REPORTING_THROW]: type === REPORTING_THROW,
|
|
102
|
+
} as ReportingInformation;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getValueType(value: unknown): string {
|
|
106
|
+
const valueType = typeof value;
|
|
107
|
+
|
|
108
|
+
switch (true) {
|
|
109
|
+
case value === null:
|
|
110
|
+
return `'${TYPE_NULL}'`;
|
|
111
|
+
|
|
112
|
+
case value === undefined:
|
|
113
|
+
return `'${TYPE_UNDEFINED}'`;
|
|
114
|
+
|
|
115
|
+
case valueType !== TYPE_OBJECT:
|
|
116
|
+
return `'${valueType}'`;
|
|
117
|
+
|
|
118
|
+
case Array.isArray(value):
|
|
119
|
+
return `'${TYPE_ARRAY}'`;
|
|
120
|
+
|
|
121
|
+
case isPlainObject(value):
|
|
122
|
+
return `'${TYPE_OBJECT}'`;
|
|
123
|
+
|
|
124
|
+
case isSchematic(value):
|
|
125
|
+
return `a ${NAME_SCHEMATIC}`;
|
|
126
|
+
|
|
127
|
+
default:
|
|
128
|
+
return value.constructor.name;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
6
132
|
/**
|
|
7
133
|
* Creates a validator function for a given constructor
|
|
8
134
|
* @param constructor - Constructor to check against
|
|
@@ -30,7 +156,39 @@ export function isSchematic(value: unknown): value is Schematic<never> {
|
|
|
30
156
|
return (
|
|
31
157
|
typeof value === 'object' &&
|
|
32
158
|
value !== null &&
|
|
33
|
-
|
|
34
|
-
value[
|
|
159
|
+
PROPERTY_SCHEMATIC in value &&
|
|
160
|
+
value[PROPERTY_SCHEMATIC] === true
|
|
35
161
|
);
|
|
36
162
|
}
|
|
163
|
+
|
|
164
|
+
function renderTypes(types: ValidatedPropertyType[]): string {
|
|
165
|
+
const unique = new Set<string>();
|
|
166
|
+
const parts: string[] = [];
|
|
167
|
+
|
|
168
|
+
for (let index = 0; index < types.length; index += 1) {
|
|
169
|
+
const rendered = getPropertyType(types[index]);
|
|
170
|
+
|
|
171
|
+
if (unique.has(rendered)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
unique.add(rendered);
|
|
176
|
+
parts.push(rendered);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const {length} = parts;
|
|
180
|
+
|
|
181
|
+
let rendered = '';
|
|
182
|
+
|
|
183
|
+
for (let index = 0; index < length; index += 1) {
|
|
184
|
+
rendered += parts[index];
|
|
185
|
+
|
|
186
|
+
if (index < length - 2) {
|
|
187
|
+
rendered += ', ';
|
|
188
|
+
} else if (index === length - 2) {
|
|
189
|
+
rendered += parts.length > 2 ? ', or ' : ' or ';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return rendered;
|
|
194
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export {instanceOf, isSchematic} from './helpers';
|
|
2
|
-
export
|
|
2
|
+
export type {Schema} from './models/schema.plain.model';
|
|
3
|
+
export type {TypedSchema} from './models/schema.typed.model';
|
|
4
|
+
export {SchematicError, ValidationError} from './models/validation.model';
|
|
3
5
|
export {schematic, type Schematic} from './schematic';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type {Constructor, Simplify} from '@oscarpalmer/atoms/models';
|
|
2
|
+
import type {Schematic} from '../schematic';
|
|
3
|
+
import type {IsOptionalProperty, ValueName, Values} from './misc.model';
|
|
4
|
+
import type {PlainSchema, Schema, SchemaProperty} from './schema.plain.model';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Infers the TypeScript type from a {@link Schema} definition
|
|
8
|
+
*
|
|
9
|
+
* @template Model - Schema to infer types from
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const userSchema = {
|
|
14
|
+
* name: 'string',
|
|
15
|
+
* age: 'number',
|
|
16
|
+
* address: { $required: false, $type: 'string' },
|
|
17
|
+
* } satisfies Schema;
|
|
18
|
+
*
|
|
19
|
+
* type User = Infer<typeof userSchema>;
|
|
20
|
+
* // { name: string; age: number; address?: string }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export type Infer<Model extends Schema> = Simplify<
|
|
24
|
+
{
|
|
25
|
+
[Key in InferRequiredKeys<Model>]: InferSchemaEntry<Model[Key]>;
|
|
26
|
+
} & {
|
|
27
|
+
[Key in InferOptionalKeys<Model>]?: InferSchemaEntry<Model[Key]>;
|
|
28
|
+
}
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extracts keys from a {@link Schema} whose entries are optional _(i.e., `$required` is `false`)_
|
|
33
|
+
*
|
|
34
|
+
* @template Model - {@link Schema} to extract optional keys from
|
|
35
|
+
*/
|
|
36
|
+
export type InferOptionalKeys<Model extends Schema> = keyof {
|
|
37
|
+
[Key in keyof Model as IsOptionalProperty<Model[Key]> extends true ? Key : never]: never;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Infers the TypeScript type of a {@link SchemaProperty}'s `$type` field, unwrapping arrays to infer their item type
|
|
42
|
+
*
|
|
43
|
+
* @template Value - `$type` value _(single or array)_
|
|
44
|
+
*/
|
|
45
|
+
export type InferPropertyType<Value> = Value extends (infer Item)[]
|
|
46
|
+
? InferPropertyValue<Item>
|
|
47
|
+
: InferPropertyValue<Value>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Maps a single type definition to its TypeScript equivalent
|
|
51
|
+
*
|
|
52
|
+
* Resolves, in order: {@link Constructor} instances, {@link Schematic} models, {@link ValueName} strings, and nested {@link Schema} objects
|
|
53
|
+
*
|
|
54
|
+
* @template Value - single type definition
|
|
55
|
+
*/
|
|
56
|
+
export type InferPropertyValue<Value> =
|
|
57
|
+
Value extends Constructor<infer Instance>
|
|
58
|
+
? Instance
|
|
59
|
+
: Value extends Schematic<infer Model>
|
|
60
|
+
? Model
|
|
61
|
+
: Value extends ValueName
|
|
62
|
+
? Values[Value & ValueName]
|
|
63
|
+
: Value extends Schema
|
|
64
|
+
? Infer<Value>
|
|
65
|
+
: never;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extracts keys from a {@link Schema} whose entries are required _(i.e., `$required` is not `false`)_
|
|
69
|
+
*
|
|
70
|
+
* @template Model - Schema to extract required keys from
|
|
71
|
+
*/
|
|
72
|
+
export type InferRequiredKeys<Model extends Schema> = keyof {
|
|
73
|
+
[Key in keyof Model as IsOptionalProperty<Model[Key]> extends true ? never : Key]: never;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Infers the type for a top-level {@link Schema} entry, unwrapping arrays to infer their item type
|
|
78
|
+
*
|
|
79
|
+
* @template Value - Schema entry value _(single or array)_
|
|
80
|
+
*/
|
|
81
|
+
export type InferSchemaEntry<Value> = Value extends (infer Item)[]
|
|
82
|
+
? InferSchemaEntryValue<Item>
|
|
83
|
+
: InferSchemaEntryValue<Value>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolves a single schema entry to its TypeScript type
|
|
87
|
+
*
|
|
88
|
+
* Handles, in order: {@link Constructor} instances, {@link Schematic} models, {@link SchemaProperty} objects, {@link NestedSchema} objects, {@link ValueName} strings, and plain {@link Schema} objects
|
|
89
|
+
*
|
|
90
|
+
* @template Value - single schema entry
|
|
91
|
+
*/
|
|
92
|
+
export type InferSchemaEntryValue<Value> =
|
|
93
|
+
Value extends Constructor<infer Instance>
|
|
94
|
+
? Instance
|
|
95
|
+
: Value extends Schematic<infer Model>
|
|
96
|
+
? Model
|
|
97
|
+
: Value extends SchemaProperty
|
|
98
|
+
? InferPropertyType<Value['$type']>
|
|
99
|
+
: Value extends PlainSchema
|
|
100
|
+
? Infer<Value & Schema>
|
|
101
|
+
: Value extends ValueName
|
|
102
|
+
? Values[Value & ValueName]
|
|
103
|
+
: Value extends Schema
|
|
104
|
+
? Infer<Value>
|
|
105
|
+
: never;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type {SchemaProperty} from './schema.plain.model';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Removes duplicate types from a tuple, preserving first occurrence order
|
|
5
|
+
*
|
|
6
|
+
* @template Value - Tuple to deduplicate
|
|
7
|
+
* @template Seen - Accumulator for already-seen types _(internal)_
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* // DeduplicateTuple<['string', 'number', 'string']>
|
|
12
|
+
* // => ['string', 'number']
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export type DeduplicateTuple<Value extends unknown[], Seen extends unknown[] = []> = Value extends [
|
|
16
|
+
infer Head,
|
|
17
|
+
...infer Tail,
|
|
18
|
+
]
|
|
19
|
+
? Head extends Seen[number]
|
|
20
|
+
? DeduplicateTuple<Tail, Seen>
|
|
21
|
+
: DeduplicateTuple<Tail, [...Seen, Head]>
|
|
22
|
+
: Seen;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Recursively extracts {@link ValueName} strings from a type, unwrapping arrays and readonly arrays
|
|
26
|
+
*
|
|
27
|
+
* @template Value - Type to extract value names from
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* // ExtractValueNames<'string'> => 'string'
|
|
32
|
+
* // ExtractValueNames<['string', 'number']> => 'string' | 'number'
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export type ExtractValueNames<Value> = Value extends ValueName
|
|
36
|
+
? Value
|
|
37
|
+
: Value extends (infer Item)[]
|
|
38
|
+
? ExtractValueNames<Item>
|
|
39
|
+
: Value extends readonly (infer Item)[]
|
|
40
|
+
? ExtractValueNames<Item>
|
|
41
|
+
: never;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Determines whether a schema entry is optional
|
|
45
|
+
*
|
|
46
|
+
* Returns `true` if the entry is a {@link SchemaProperty} or {@link NestedSchema} with `$required` set to `false`; otherwise returns `false`
|
|
47
|
+
*
|
|
48
|
+
* @template Value - Schema entry to check
|
|
49
|
+
*/
|
|
50
|
+
export type IsOptionalProperty<Value> = Value extends SchemaProperty
|
|
51
|
+
? Value['$required'] extends false
|
|
52
|
+
? true
|
|
53
|
+
: false
|
|
54
|
+
: false;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extracts the last member from a union type by leveraging intersection of function return types
|
|
58
|
+
*
|
|
59
|
+
* @template Value - Union type
|
|
60
|
+
*/
|
|
61
|
+
export type LastOfUnion<Value> =
|
|
62
|
+
UnionToIntersection<Value extends unknown ? () => Value : never> extends () => infer Item
|
|
63
|
+
? Item
|
|
64
|
+
: never;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Extracts keys from an object type that are optional
|
|
68
|
+
*
|
|
69
|
+
* @template Value - Object type to inspect
|
|
70
|
+
*/
|
|
71
|
+
export type OptionalKeys<Value> = {
|
|
72
|
+
[Key in keyof Value]-?: {} extends Pick<Value, Key> ? Key : never;
|
|
73
|
+
}[keyof Value];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extracts keys from an object type that are required _(i.e., not optional)_
|
|
77
|
+
*
|
|
78
|
+
* @template Value - Object type to inspect
|
|
79
|
+
*/
|
|
80
|
+
export type RequiredKeys<Value> = Exclude<keyof Value, OptionalKeys<Value>>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generates all permutations of a tuple type
|
|
84
|
+
*
|
|
85
|
+
* Used by {@link UnwrapSingle} to allow schema types in any order for small tuples _(length ≤ 5)_
|
|
86
|
+
*
|
|
87
|
+
* @template Tuple - Tuple to permute
|
|
88
|
+
* @template Elput - Accumulator for the current permutation _(internal; name is Tuple backwards)_
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* // TuplePermutations<['string', 'number']>
|
|
93
|
+
* // => ['string', 'number'] | ['number', 'string']
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export type TuplePermutations<
|
|
97
|
+
Tuple extends unknown[],
|
|
98
|
+
Elput extends unknown[] = [],
|
|
99
|
+
> = Tuple['length'] extends 0
|
|
100
|
+
? Elput
|
|
101
|
+
: {
|
|
102
|
+
[Key in keyof Tuple]: TuplePermutations<
|
|
103
|
+
TupleRemoveAt<Tuple, Key & `${number}`>,
|
|
104
|
+
[...Elput, Tuple[Key]]
|
|
105
|
+
>;
|
|
106
|
+
}[keyof Tuple & `${number}`];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Removes the element at a given index from a tuple
|
|
110
|
+
*
|
|
111
|
+
* Used internally by {@link TuplePermutations}
|
|
112
|
+
*
|
|
113
|
+
* @template Items - Tuple to remove from
|
|
114
|
+
* @template Item - Stringified index to remove
|
|
115
|
+
* @template Prefix - Accumulator for elements before the target _(internal)_
|
|
116
|
+
*/
|
|
117
|
+
export type TupleRemoveAt<
|
|
118
|
+
Items extends unknown[],
|
|
119
|
+
Item extends string,
|
|
120
|
+
Prefix extends unknown[] = [],
|
|
121
|
+
> = Items extends [infer Head, ...infer Tail]
|
|
122
|
+
? `${Prefix['length']}` extends Item
|
|
123
|
+
? [...Prefix, ...Tail]
|
|
124
|
+
: TupleRemoveAt<Tail, Item, [...Prefix, Head]>
|
|
125
|
+
: Prefix;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Converts a union type into an intersection
|
|
129
|
+
*
|
|
130
|
+
* Uses the contravariance of function parameter types to collapse a union into an intersection
|
|
131
|
+
*
|
|
132
|
+
* @template Value - Union type to convert
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* // UnionToIntersection<{ a: 1 } | { b: 2 }>
|
|
137
|
+
* // => { a: 1 } & { b: 2 }
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export type UnionToIntersection<Value> = (
|
|
141
|
+
Value extends unknown ? (value: Value) => void : never
|
|
142
|
+
) extends (value: infer Item) => void
|
|
143
|
+
? Item
|
|
144
|
+
: never;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Converts a union type into an ordered tuple
|
|
148
|
+
*
|
|
149
|
+
* Repeatedly extracts the {@link LastOfUnion} member and prepends it to the accumulator
|
|
150
|
+
*
|
|
151
|
+
* @template Value - Union type to convert
|
|
152
|
+
* @template Items - Accumulator for the resulting tuple _(internal)_
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```ts
|
|
156
|
+
* // UnionToTuple<'a' | 'b' | 'c'>
|
|
157
|
+
* // => ['a', 'b', 'c']
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export type UnionToTuple<Value, Items extends unknown[] = []> = [Value] extends [never]
|
|
161
|
+
? Items
|
|
162
|
+
: UnionToTuple<Exclude<Value, LastOfUnion<Value>>, [LastOfUnion<Value>, ...Items]>;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Unwraps a single-element tuple to its inner type
|
|
166
|
+
*
|
|
167
|
+
* For tuples of length 2–5, returns all {@link TuplePermutations} to allow types in any order. Longer tuples are returned as-is
|
|
168
|
+
*
|
|
169
|
+
* @template Value - Tuple to potentially unwrap
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* // UnwrapSingle<['string']> => 'string'
|
|
174
|
+
* // UnwrapSingle<['string', 'number']> => ['string', 'number'] | ['number', 'string']
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
export type UnwrapSingle<Value extends unknown[]> = Value extends [infer Only]
|
|
178
|
+
? Only
|
|
179
|
+
: Value['length'] extends 1 | 2 | 3 | 4 | 5
|
|
180
|
+
? TuplePermutations<Value>
|
|
181
|
+
: Value;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Basic value types
|
|
185
|
+
*/
|
|
186
|
+
export type ValueName = keyof Values;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Maps type name strings to their TypeScript equivalents
|
|
190
|
+
*
|
|
191
|
+
* Used by the type system to resolve {@link ValueName} strings into actual types
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* // Values['string'] => string
|
|
196
|
+
* // Values['date'] => Date
|
|
197
|
+
* // Values['null'] => null
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export type Values = {
|
|
201
|
+
array: unknown[];
|
|
202
|
+
bigint: bigint;
|
|
203
|
+
boolean: boolean;
|
|
204
|
+
date: Date;
|
|
205
|
+
function: Function;
|
|
206
|
+
null: null;
|
|
207
|
+
number: number;
|
|
208
|
+
object: object;
|
|
209
|
+
string: string;
|
|
210
|
+
symbol: symbol;
|
|
211
|
+
undefined: undefined;
|
|
212
|
+
};
|