@oscarpalmer/jhunal 0.17.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 +284 -240
- package/dist/index.mjs +188 -50
- 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 +15 -1
- 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 +2 -2
- package/src/constants.ts +84 -19
- 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 +24 -13
- package/src/validation/property.validation.ts +41 -36
- package/src/validation/value.validation.ts +115 -15
- package/dist/models.d.mts +0 -484
- package/dist/models.mjs +0 -13
- package/src/models.ts +0 -665
package/dist/index.mjs
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
import { isConstructor, isPlainObject } from "@oscarpalmer/atoms/is";
|
|
2
2
|
import { join } from "@oscarpalmer/atoms/string";
|
|
3
3
|
//#region src/constants.ts
|
|
4
|
-
const ERROR_NAME = "SchematicError";
|
|
5
4
|
const MESSAGE_CONSTRUCTOR = "Expected a constructor function";
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED = "'<>.$required' property must be a boolean";
|
|
10
|
-
const MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE = "'<>' property must be of a valid type";
|
|
11
|
-
const MESSAGE_SCHEMA_INVALID_TYPE = "Schema must be an object";
|
|
12
|
-
const MESSAGE_VALIDATOR_INVALID_KEY = "Validator '<>' does not exist";
|
|
13
|
-
const MESSAGE_VALIDATOR_INVALID_TYPE = "Validators must be an object";
|
|
14
|
-
const MESSAGE_VALIDATOR_INVALID_VALUE = "Validator '<>' must be a function or an array of functions";
|
|
5
|
+
const NAME_SCHEMATIC = "Schematic";
|
|
6
|
+
const NAME_ERROR_SCHEMATIC = "SchematicError";
|
|
7
|
+
const NAME_ERROR_VALIDATION = "ValidationError";
|
|
15
8
|
const PROPERTY_REQUIRED = "$required";
|
|
9
|
+
const PROPERTY_SCHEMATIC = "$schematic";
|
|
16
10
|
const PROPERTY_TYPE = "$type";
|
|
17
11
|
const PROPERTY_VALIDATORS = "$validators";
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
12
|
+
const VALIDATION_MESSAGE_INVALID_INPUT = "Expected 'object' as input but received <>";
|
|
13
|
+
const VALIDATION_MESSAGE_INVALID_REQUIRED = "Expected <> for required property '<>'";
|
|
14
|
+
const VALIDATION_MESSAGE_INVALID_TYPE = "Expected <> for '<>' but received <>";
|
|
15
|
+
const VALIDATION_MESSAGE_INVALID_VALUE = "Value does not satisfy validator for '<>' and type '<>'";
|
|
16
|
+
const VALIDATION_MESSAGE_INVALID_VALUE_SUFFIX = " at index <>";
|
|
17
|
+
const REPORTING_FIRST = "first";
|
|
18
|
+
const REPORTING_NONE = "none";
|
|
19
|
+
const REPORTING_THROW = "throw";
|
|
20
|
+
const REPORTING_TYPES = new Set([
|
|
21
|
+
"all",
|
|
22
|
+
REPORTING_FIRST,
|
|
23
|
+
REPORTING_NONE,
|
|
24
|
+
REPORTING_THROW
|
|
25
|
+
]);
|
|
26
|
+
const SCHEMATIC_MESSAGE_SCHEMA_INVALID_EMPTY = "Schema must have at least one property";
|
|
27
|
+
const SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED = "'<>.<>' property is not allowed for schemas in $type";
|
|
28
|
+
const SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE = "'<>' property must not be 'null' or 'undefined'";
|
|
29
|
+
const SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED = "'<>.$required' property must be a boolean";
|
|
30
|
+
const SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE = "'<>' property must be of a valid type";
|
|
31
|
+
const SCHEMATIC_MESSAGE_SCHEMA_INVALID_TYPE = "Schema must be an object";
|
|
32
|
+
const SCHEMATIC_MESSAGE_VALIDATOR_INVALID_KEY = "Validator '<>' does not exist";
|
|
33
|
+
const SCHEMATIC_MESSAGE_VALIDATOR_INVALID_TYPE = "Validators must be an object";
|
|
34
|
+
const SCHEMATIC_MESSAGE_VALIDATOR_INVALID_VALUE = "Validator '<>' must be a function or an array of functions";
|
|
35
|
+
const TYPE_ARRAY = "array";
|
|
36
|
+
const TYPE_NULL = "null";
|
|
21
37
|
const TYPE_OBJECT = "object";
|
|
22
38
|
const TYPE_UNDEFINED = "undefined";
|
|
23
39
|
const VALIDATABLE_TYPES = new Set([
|
|
@@ -38,6 +54,53 @@ const TYPE_ALL = new Set([
|
|
|
38
54
|
]);
|
|
39
55
|
//#endregion
|
|
40
56
|
//#region src/helpers.ts
|
|
57
|
+
function getInvalidInputMessage(actual) {
|
|
58
|
+
return VALIDATION_MESSAGE_INVALID_INPUT.replace("<>", getValueType(actual));
|
|
59
|
+
}
|
|
60
|
+
function getInvalidMissingMessage(property) {
|
|
61
|
+
let message = VALIDATION_MESSAGE_INVALID_REQUIRED.replace("<>", renderTypes(property.types));
|
|
62
|
+
message = message.replace("<>", property.key.full);
|
|
63
|
+
return message;
|
|
64
|
+
}
|
|
65
|
+
function getInvalidTypeMessage(property, actual) {
|
|
66
|
+
let message = VALIDATION_MESSAGE_INVALID_TYPE.replace("<>", renderTypes(property.types));
|
|
67
|
+
message = message.replace("<>", property.key.full);
|
|
68
|
+
message = message.replace("<>", getValueType(actual));
|
|
69
|
+
return message;
|
|
70
|
+
}
|
|
71
|
+
function getInvalidValidatorMessage(property, type, index, length) {
|
|
72
|
+
let message = VALIDATION_MESSAGE_INVALID_VALUE.replace("<>", property.key.full);
|
|
73
|
+
message = message.replace("<>", type);
|
|
74
|
+
if (length > 1) message += VALIDATION_MESSAGE_INVALID_VALUE_SUFFIX.replace("<>", String(index));
|
|
75
|
+
return message;
|
|
76
|
+
}
|
|
77
|
+
function getPropertyType(original) {
|
|
78
|
+
if (typeof original === "function") return "a validated value";
|
|
79
|
+
if (Array.isArray(original)) return `'${TYPE_OBJECT}'`;
|
|
80
|
+
if (isSchematic(original)) return `a ${NAME_SCHEMATIC}`;
|
|
81
|
+
return `'${String(original)}'`;
|
|
82
|
+
}
|
|
83
|
+
function getReporting(value) {
|
|
84
|
+
const type = REPORTING_TYPES.has(value) ? value : REPORTING_NONE;
|
|
85
|
+
return {
|
|
86
|
+
["all"]: type === "all",
|
|
87
|
+
[REPORTING_FIRST]: type === REPORTING_FIRST,
|
|
88
|
+
[REPORTING_NONE]: type === REPORTING_NONE,
|
|
89
|
+
[REPORTING_THROW]: type === REPORTING_THROW
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function getValueType(value) {
|
|
93
|
+
const valueType = typeof value;
|
|
94
|
+
switch (true) {
|
|
95
|
+
case value === null: return `'${TYPE_NULL}'`;
|
|
96
|
+
case value === void 0: return `'${TYPE_UNDEFINED}'`;
|
|
97
|
+
case valueType !== TYPE_OBJECT: return `'${valueType}'`;
|
|
98
|
+
case Array.isArray(value): return `'${TYPE_ARRAY}'`;
|
|
99
|
+
case isPlainObject(value): return `'${TYPE_OBJECT}'`;
|
|
100
|
+
case isSchematic(value): return `a ${NAME_SCHEMATIC}`;
|
|
101
|
+
default: return value.constructor.name;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
41
104
|
/**
|
|
42
105
|
* Creates a validator function for a given constructor
|
|
43
106
|
* @param constructor - Constructor to check against
|
|
@@ -58,15 +121,40 @@ function instanceOf(constructor) {
|
|
|
58
121
|
function isSchematic(value) {
|
|
59
122
|
return typeof value === "object" && value !== null && "$schematic" in value && value["$schematic"] === true;
|
|
60
123
|
}
|
|
124
|
+
function renderTypes(types) {
|
|
125
|
+
const unique = /* @__PURE__ */ new Set();
|
|
126
|
+
const parts = [];
|
|
127
|
+
for (let index = 0; index < types.length; index += 1) {
|
|
128
|
+
const rendered = getPropertyType(types[index]);
|
|
129
|
+
if (unique.has(rendered)) continue;
|
|
130
|
+
unique.add(rendered);
|
|
131
|
+
parts.push(rendered);
|
|
132
|
+
}
|
|
133
|
+
const { length } = parts;
|
|
134
|
+
let rendered = "";
|
|
135
|
+
for (let index = 0; index < length; index += 1) {
|
|
136
|
+
rendered += parts[index];
|
|
137
|
+
if (index < length - 2) rendered += ", ";
|
|
138
|
+
else if (index === length - 2) rendered += parts.length > 2 ? ", or " : " or ";
|
|
139
|
+
}
|
|
140
|
+
return rendered;
|
|
141
|
+
}
|
|
61
142
|
//#endregion
|
|
62
|
-
//#region src/models.ts
|
|
143
|
+
//#region src/models/validation.model.ts
|
|
63
144
|
/**
|
|
64
145
|
* A custom error class for schematic validation failures
|
|
65
146
|
*/
|
|
66
147
|
var SchematicError = class extends Error {
|
|
67
148
|
constructor(message) {
|
|
68
149
|
super(message);
|
|
69
|
-
this.name =
|
|
150
|
+
this.name = NAME_ERROR_SCHEMATIC;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var ValidationError = class extends Error {
|
|
154
|
+
constructor(information) {
|
|
155
|
+
super(join(information.map((item) => item.message), "; "));
|
|
156
|
+
this.information = information;
|
|
157
|
+
this.name = NAME_ERROR_VALIDATION;
|
|
70
158
|
}
|
|
71
159
|
};
|
|
72
160
|
//#endregion
|
|
@@ -77,32 +165,36 @@ function getDisallowedProperty(obj) {
|
|
|
77
165
|
if ("$validators" in obj) return PROPERTY_VALIDATORS;
|
|
78
166
|
}
|
|
79
167
|
function getProperties(original, prefix, fromType) {
|
|
80
|
-
if (Object.keys(original).length === 0) throw new SchematicError(
|
|
168
|
+
if (Object.keys(original).length === 0) throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_EMPTY);
|
|
81
169
|
if (fromType ?? false) {
|
|
82
170
|
const property = getDisallowedProperty(original);
|
|
83
|
-
if (property != null) throw new SchematicError(
|
|
171
|
+
if (property != null) throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED.replace("<>", prefix).replace("<>", property));
|
|
84
172
|
}
|
|
85
173
|
const keys = Object.keys(original);
|
|
86
174
|
const keysLength = keys.length;
|
|
87
175
|
const properties = [];
|
|
88
176
|
for (let keyIndex = 0; keyIndex < keysLength; keyIndex += 1) {
|
|
89
177
|
const key = keys[keyIndex];
|
|
178
|
+
const prefixed = join([prefix, key], ".");
|
|
90
179
|
const value = original[key];
|
|
91
|
-
if (value == null) throw new SchematicError(
|
|
180
|
+
if (value == null) throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE.replace("<>", prefixed));
|
|
92
181
|
const types = [];
|
|
93
182
|
let required = true;
|
|
94
183
|
let validators = {};
|
|
95
184
|
if (isPlainObject(value)) {
|
|
96
185
|
required = getRequired(key, value) ?? required;
|
|
97
186
|
validators = getValidators(value[PROPERTY_VALIDATORS]);
|
|
98
|
-
|
|
99
|
-
|
|
187
|
+
const hasType = PROPERTY_TYPE in value;
|
|
188
|
+
types.push(...getTypes(key, hasType ? value[PROPERTY_TYPE] : value, prefix, hasType));
|
|
100
189
|
} else types.push(...getTypes(key, value, prefix));
|
|
101
190
|
if (!required && !types.includes("undefined")) types.push(TYPE_UNDEFINED);
|
|
102
191
|
properties.push({
|
|
103
|
-
key,
|
|
104
192
|
types,
|
|
105
193
|
validators,
|
|
194
|
+
key: {
|
|
195
|
+
full: prefixed,
|
|
196
|
+
short: key
|
|
197
|
+
},
|
|
106
198
|
required: required && !types.includes("undefined")
|
|
107
199
|
});
|
|
108
200
|
}
|
|
@@ -110,7 +202,7 @@ function getProperties(original, prefix, fromType) {
|
|
|
110
202
|
}
|
|
111
203
|
function getRequired(key, obj) {
|
|
112
204
|
if (!("$required" in obj)) return;
|
|
113
|
-
if (typeof obj["$required"] !== "boolean") throw new SchematicError(
|
|
205
|
+
if (typeof obj["$required"] !== "boolean") throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED.replace("<>", key));
|
|
114
206
|
return obj[PROPERTY_REQUIRED];
|
|
115
207
|
}
|
|
116
208
|
function getTypes(key, original, prefix, fromType) {
|
|
@@ -124,7 +216,7 @@ function getTypes(key, original, prefix, fromType) {
|
|
|
124
216
|
types.push(isConstructor(value) ? instanceOf(value) : value);
|
|
125
217
|
break;
|
|
126
218
|
case isPlainObject(value):
|
|
127
|
-
types.push(
|
|
219
|
+
types.push(getProperties(value, join([prefix, key], "."), fromType));
|
|
128
220
|
break;
|
|
129
221
|
case isSchematic(value):
|
|
130
222
|
types.push(value);
|
|
@@ -132,55 +224,106 @@ function getTypes(key, original, prefix, fromType) {
|
|
|
132
224
|
case TYPE_ALL.has(value):
|
|
133
225
|
types.push(value);
|
|
134
226
|
break;
|
|
135
|
-
default: throw new SchematicError(
|
|
227
|
+
default: throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace("<>", join([prefix, key], ".")));
|
|
136
228
|
}
|
|
137
229
|
}
|
|
138
|
-
if (types.length === 0) throw new SchematicError(
|
|
230
|
+
if (types.length === 0) throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace("<>", join([prefix, key], ".")));
|
|
139
231
|
return types;
|
|
140
232
|
}
|
|
141
233
|
function getValidators(original) {
|
|
142
234
|
const validators = {};
|
|
143
235
|
if (original == null) return validators;
|
|
144
|
-
if (!isPlainObject(original)) throw new TypeError(
|
|
236
|
+
if (!isPlainObject(original)) throw new TypeError(SCHEMATIC_MESSAGE_VALIDATOR_INVALID_TYPE);
|
|
145
237
|
const keys = Object.keys(original);
|
|
146
238
|
const { length } = keys;
|
|
147
239
|
for (let index = 0; index < length; index += 1) {
|
|
148
240
|
const key = keys[index];
|
|
149
|
-
if (!VALIDATABLE_TYPES.has(key)) throw new TypeError(
|
|
241
|
+
if (!VALIDATABLE_TYPES.has(key)) throw new TypeError(SCHEMATIC_MESSAGE_VALIDATOR_INVALID_KEY.replace("<>", key));
|
|
150
242
|
const value = original[key];
|
|
151
|
-
validators[key] = (Array.isArray(value) ? value : [value]).
|
|
152
|
-
if (typeof item !== "function") throw new TypeError(
|
|
153
|
-
return
|
|
243
|
+
validators[key] = (Array.isArray(value) ? value : [value]).map((item) => {
|
|
244
|
+
if (typeof item !== "function") throw new TypeError(SCHEMATIC_MESSAGE_VALIDATOR_INVALID_VALUE.replace("<>", key));
|
|
245
|
+
return item;
|
|
154
246
|
});
|
|
155
247
|
}
|
|
156
248
|
return validators;
|
|
157
249
|
}
|
|
158
250
|
//#endregion
|
|
159
251
|
//#region src/validation/value.validation.ts
|
|
160
|
-
function
|
|
161
|
-
if (!
|
|
252
|
+
function validateNamed(property, name, value, validation) {
|
|
253
|
+
if (!validators[name](value)) return false;
|
|
254
|
+
const propertyValidators = property.validators[name];
|
|
255
|
+
if (propertyValidators == null || propertyValidators.length === 0) return true;
|
|
256
|
+
const { length } = propertyValidators;
|
|
257
|
+
for (let index = 0; index < length; index += 1) {
|
|
258
|
+
const validator = propertyValidators[index];
|
|
259
|
+
if (!validator(value)) {
|
|
260
|
+
validation.push({
|
|
261
|
+
key: { ...property.key },
|
|
262
|
+
message: getInvalidValidatorMessage(property, name, index, length),
|
|
263
|
+
validator
|
|
264
|
+
});
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
function validateObject(obj, properties, reporting, validation) {
|
|
271
|
+
if (!isPlainObject(obj)) {
|
|
272
|
+
if (reporting.throw && validation == null) throw new ValidationError([{
|
|
273
|
+
key: {
|
|
274
|
+
full: "",
|
|
275
|
+
short: ""
|
|
276
|
+
},
|
|
277
|
+
message: getInvalidInputMessage(obj)
|
|
278
|
+
}]);
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
162
281
|
const propertiesLength = properties.length;
|
|
163
282
|
outer: for (let propertyIndex = 0; propertyIndex < propertiesLength; propertyIndex += 1) {
|
|
164
283
|
const property = properties[propertyIndex];
|
|
165
284
|
const { key, required, types } = property;
|
|
166
|
-
const value = obj[key];
|
|
167
|
-
if (value === void 0 && required)
|
|
285
|
+
const value = obj[key.short];
|
|
286
|
+
if (value === void 0 && required) {
|
|
287
|
+
const information = {
|
|
288
|
+
key: { ...key },
|
|
289
|
+
message: getInvalidMissingMessage(property)
|
|
290
|
+
};
|
|
291
|
+
if (reporting.throw && validation == null) throw new ValidationError([information]);
|
|
292
|
+
if (validation != null) validation.push(information);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
168
295
|
const typesLength = types.length;
|
|
296
|
+
const information = [];
|
|
169
297
|
for (let typeIndex = 0; typeIndex < typesLength; typeIndex += 1) {
|
|
170
298
|
const type = types[typeIndex];
|
|
171
|
-
if (validateValue(type, property, value)) continue outer;
|
|
299
|
+
if (validateValue(type, property, value, reporting, information)) continue outer;
|
|
172
300
|
}
|
|
301
|
+
if (reporting.throw && validation == null) throw new ValidationError(information.length === 0 ? [{
|
|
302
|
+
key: { ...key },
|
|
303
|
+
message: getInvalidTypeMessage(property, value)
|
|
304
|
+
}] : information);
|
|
305
|
+
validation?.push(...information);
|
|
173
306
|
return false;
|
|
174
307
|
}
|
|
175
308
|
return true;
|
|
176
309
|
}
|
|
177
|
-
function validateValue(type, property, value) {
|
|
310
|
+
function validateValue(type, property, value, reporting, validation) {
|
|
311
|
+
let result;
|
|
178
312
|
switch (true) {
|
|
179
|
-
case
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
313
|
+
case typeof type === "function":
|
|
314
|
+
result = type(value);
|
|
315
|
+
break;
|
|
316
|
+
case Array.isArray(type):
|
|
317
|
+
result = validateObject(value, type, reporting, validation);
|
|
318
|
+
break;
|
|
319
|
+
case isSchematic(type):
|
|
320
|
+
result = type.is(value, reporting);
|
|
321
|
+
break;
|
|
322
|
+
default:
|
|
323
|
+
result = validateNamed(property, type, value, validation);
|
|
324
|
+
break;
|
|
183
325
|
}
|
|
326
|
+
return result;
|
|
184
327
|
}
|
|
185
328
|
const validators = {
|
|
186
329
|
array: Array.isArray,
|
|
@@ -203,22 +346,17 @@ const validators = {
|
|
|
203
346
|
var Schematic = class {
|
|
204
347
|
#properties;
|
|
205
348
|
constructor(properties) {
|
|
206
|
-
Object.defineProperty(this,
|
|
349
|
+
Object.defineProperty(this, PROPERTY_SCHEMATIC, { value: true });
|
|
207
350
|
this.#properties = properties;
|
|
208
351
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
* @param value Value to validate
|
|
212
|
-
* @returns `true` if the value matches the schema, otherwise `false`
|
|
213
|
-
*/
|
|
214
|
-
is(value) {
|
|
215
|
-
return validateObject(value, this.#properties);
|
|
352
|
+
is(value, errors) {
|
|
353
|
+
return validateObject(value, this.#properties, getReporting(errors));
|
|
216
354
|
}
|
|
217
355
|
};
|
|
218
356
|
function schematic(schema) {
|
|
219
357
|
if (isSchematic(schema)) return schema;
|
|
220
|
-
if (!isPlainObject(schema)) throw new SchematicError(
|
|
358
|
+
if (!isPlainObject(schema)) throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_TYPE);
|
|
221
359
|
return new Schematic(getProperties(schema));
|
|
222
360
|
}
|
|
223
361
|
//#endregion
|
|
224
|
-
export { SchematicError, instanceOf, isSchematic, schematic };
|
|
362
|
+
export { SchematicError, ValidationError, instanceOf, isSchematic, schematic };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Schematic } from "../schematic.mjs";
|
|
2
|
+
import { PlainSchema, Schema, SchemaProperty } from "./schema.plain.model.mjs";
|
|
3
|
+
import { IsOptionalProperty, ValueName, Values } from "./misc.model.mjs";
|
|
4
|
+
import { Constructor, Simplify } from "@oscarpalmer/atoms/models";
|
|
5
|
+
|
|
6
|
+
//#region src/models/infer.model.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* Infers the TypeScript type from a {@link Schema} definition
|
|
9
|
+
*
|
|
10
|
+
* @template Model - Schema to infer types from
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const userSchema = {
|
|
15
|
+
* name: 'string',
|
|
16
|
+
* age: 'number',
|
|
17
|
+
* address: { $required: false, $type: 'string' },
|
|
18
|
+
* } satisfies Schema;
|
|
19
|
+
*
|
|
20
|
+
* type User = Infer<typeof userSchema>;
|
|
21
|
+
* // { name: string; age: number; address?: string }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
type Infer<Model extends Schema> = Simplify<{ [Key in InferRequiredKeys<Model>]: InferSchemaEntry<Model[Key]> } & { [Key in InferOptionalKeys<Model>]?: InferSchemaEntry<Model[Key]> }>;
|
|
25
|
+
/**
|
|
26
|
+
* Extracts keys from a {@link Schema} whose entries are optional _(i.e., `$required` is `false`)_
|
|
27
|
+
*
|
|
28
|
+
* @template Model - {@link Schema} to extract optional keys from
|
|
29
|
+
*/
|
|
30
|
+
type InferOptionalKeys<Model extends Schema> = keyof { [Key in keyof Model as IsOptionalProperty<Model[Key]> extends true ? Key : never]: never };
|
|
31
|
+
/**
|
|
32
|
+
* Infers the TypeScript type of a {@link SchemaProperty}'s `$type` field, unwrapping arrays to infer their item type
|
|
33
|
+
*
|
|
34
|
+
* @template Value - `$type` value _(single or array)_
|
|
35
|
+
*/
|
|
36
|
+
type InferPropertyType<Value> = Value extends (infer Item)[] ? InferPropertyValue<Item> : InferPropertyValue<Value>;
|
|
37
|
+
/**
|
|
38
|
+
* Maps a single type definition to its TypeScript equivalent
|
|
39
|
+
*
|
|
40
|
+
* Resolves, in order: {@link Constructor} instances, {@link Schematic} models, {@link ValueName} strings, and nested {@link Schema} objects
|
|
41
|
+
*
|
|
42
|
+
* @template Value - single type definition
|
|
43
|
+
*/
|
|
44
|
+
type InferPropertyValue<Value> = Value extends Constructor<infer Instance> ? Instance : Value extends Schematic<infer Model> ? Model : Value extends ValueName ? Values[Value & ValueName] : Value extends Schema ? Infer<Value> : never;
|
|
45
|
+
/**
|
|
46
|
+
* Extracts keys from a {@link Schema} whose entries are required _(i.e., `$required` is not `false`)_
|
|
47
|
+
*
|
|
48
|
+
* @template Model - Schema to extract required keys from
|
|
49
|
+
*/
|
|
50
|
+
type InferRequiredKeys<Model extends Schema> = keyof { [Key in keyof Model as IsOptionalProperty<Model[Key]> extends true ? never : Key]: never };
|
|
51
|
+
/**
|
|
52
|
+
* Infers the type for a top-level {@link Schema} entry, unwrapping arrays to infer their item type
|
|
53
|
+
*
|
|
54
|
+
* @template Value - Schema entry value _(single or array)_
|
|
55
|
+
*/
|
|
56
|
+
type InferSchemaEntry<Value> = Value extends (infer Item)[] ? InferSchemaEntryValue<Item> : InferSchemaEntryValue<Value>;
|
|
57
|
+
/**
|
|
58
|
+
* Resolves a single schema entry to its TypeScript type
|
|
59
|
+
*
|
|
60
|
+
* Handles, in order: {@link Constructor} instances, {@link Schematic} models, {@link SchemaProperty} objects, {@link NestedSchema} objects, {@link ValueName} strings, and plain {@link Schema} objects
|
|
61
|
+
*
|
|
62
|
+
* @template Value - single schema entry
|
|
63
|
+
*/
|
|
64
|
+
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;
|
|
65
|
+
//#endregion
|
|
66
|
+
export { Infer, InferOptionalKeys, InferPropertyType, InferPropertyValue, InferRequiredKeys, InferSchemaEntry, InferSchemaEntryValue };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { SchemaProperty } from "./schema.plain.model.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/models/misc.model.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;
|
|
29
|
+
/**
|
|
30
|
+
* Determines whether a schema entry is optional
|
|
31
|
+
*
|
|
32
|
+
* Returns `true` if the entry is a {@link SchemaProperty} or {@link NestedSchema} with `$required` set to `false`; otherwise returns `false`
|
|
33
|
+
*
|
|
34
|
+
* @template Value - Schema entry to check
|
|
35
|
+
*/
|
|
36
|
+
type IsOptionalProperty<Value> = Value extends SchemaProperty ? Value['$required'] extends false ? true : false : false;
|
|
37
|
+
/**
|
|
38
|
+
* Extracts the last member from a union type by leveraging intersection of function return types
|
|
39
|
+
*
|
|
40
|
+
* @template Value - Union type
|
|
41
|
+
*/
|
|
42
|
+
type LastOfUnion<Value> = UnionToIntersection<Value extends unknown ? () => Value : never> extends (() => infer Item) ? Item : never;
|
|
43
|
+
/**
|
|
44
|
+
* Extracts keys from an object type that are optional
|
|
45
|
+
*
|
|
46
|
+
* @template Value - Object type to inspect
|
|
47
|
+
*/
|
|
48
|
+
type OptionalKeys<Value> = { [Key in keyof Value]-?: {} extends Pick<Value, Key> ? Key : never }[keyof Value];
|
|
49
|
+
/**
|
|
50
|
+
* Extracts keys from an object type that are required _(i.e., not optional)_
|
|
51
|
+
*
|
|
52
|
+
* @template Value - Object type to inspect
|
|
53
|
+
*/
|
|
54
|
+
type RequiredKeys<Value> = Exclude<keyof Value, OptionalKeys<Value>>;
|
|
55
|
+
/**
|
|
56
|
+
* Generates all permutations of a tuple type
|
|
57
|
+
*
|
|
58
|
+
* Used by {@link UnwrapSingle} to allow schema types in any order for small tuples _(length ≤ 5)_
|
|
59
|
+
*
|
|
60
|
+
* @template Tuple - Tuple to permute
|
|
61
|
+
* @template Elput - Accumulator for the current permutation _(internal; name is Tuple backwards)_
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* // TuplePermutations<['string', 'number']>
|
|
66
|
+
* // => ['string', 'number'] | ['number', 'string']
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
type TuplePermutations<Tuple extends unknown[], Elput extends unknown[] = []> = Tuple['length'] extends 0 ? Elput : { [Key in keyof Tuple]: TuplePermutations<TupleRemoveAt<Tuple, Key & `${number}`>, [...Elput, Tuple[Key]]> }[keyof Tuple & `${number}`];
|
|
70
|
+
/**
|
|
71
|
+
* Removes the element at a given index from a tuple
|
|
72
|
+
*
|
|
73
|
+
* Used internally by {@link TuplePermutations}
|
|
74
|
+
*
|
|
75
|
+
* @template Items - Tuple to remove from
|
|
76
|
+
* @template Item - Stringified index to remove
|
|
77
|
+
* @template Prefix - Accumulator for elements before the target _(internal)_
|
|
78
|
+
*/
|
|
79
|
+
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;
|
|
80
|
+
/**
|
|
81
|
+
* Converts a union type into an intersection
|
|
82
|
+
*
|
|
83
|
+
* Uses the contravariance of function parameter types to collapse a union into an intersection
|
|
84
|
+
*
|
|
85
|
+
* @template Value - Union type to convert
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* // UnionToIntersection<{ a: 1 } | { b: 2 }>
|
|
90
|
+
* // => { a: 1 } & { b: 2 }
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
type UnionToIntersection<Value> = (Value extends unknown ? (value: Value) => void : never) extends ((value: infer Item) => void) ? Item : never;
|
|
94
|
+
/**
|
|
95
|
+
* Converts a union type into an ordered tuple
|
|
96
|
+
*
|
|
97
|
+
* Repeatedly extracts the {@link LastOfUnion} member and prepends it to the accumulator
|
|
98
|
+
*
|
|
99
|
+
* @template Value - Union type to convert
|
|
100
|
+
* @template Items - Accumulator for the resulting tuple _(internal)_
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* // UnionToTuple<'a' | 'b' | 'c'>
|
|
105
|
+
* // => ['a', 'b', 'c']
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
type UnionToTuple<Value, Items extends unknown[] = []> = [Value] extends [never] ? Items : UnionToTuple<Exclude<Value, LastOfUnion<Value>>, [LastOfUnion<Value>, ...Items]>;
|
|
109
|
+
/**
|
|
110
|
+
* Unwraps a single-element tuple to its inner type
|
|
111
|
+
*
|
|
112
|
+
* For tuples of length 2–5, returns all {@link TuplePermutations} to allow types in any order. Longer tuples are returned as-is
|
|
113
|
+
*
|
|
114
|
+
* @template Value - Tuple to potentially unwrap
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* // UnwrapSingle<['string']> => 'string'
|
|
119
|
+
* // UnwrapSingle<['string', 'number']> => ['string', 'number'] | ['number', 'string']
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
type UnwrapSingle<Value extends unknown[]> = Value extends [infer Only] ? Only : Value['length'] extends 1 | 2 | 3 | 4 | 5 ? TuplePermutations<Value> : Value;
|
|
123
|
+
/**
|
|
124
|
+
* Basic value types
|
|
125
|
+
*/
|
|
126
|
+
type ValueName = keyof Values;
|
|
127
|
+
/**
|
|
128
|
+
* Maps type name strings to their TypeScript equivalents
|
|
129
|
+
*
|
|
130
|
+
* Used by the type system to resolve {@link ValueName} strings into actual types
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* // Values['string'] => string
|
|
135
|
+
* // Values['date'] => Date
|
|
136
|
+
* // Values['null'] => null
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
type Values = {
|
|
140
|
+
array: unknown[];
|
|
141
|
+
bigint: bigint;
|
|
142
|
+
boolean: boolean;
|
|
143
|
+
date: Date;
|
|
144
|
+
function: Function;
|
|
145
|
+
null: null;
|
|
146
|
+
number: number;
|
|
147
|
+
object: object;
|
|
148
|
+
string: string;
|
|
149
|
+
symbol: symbol;
|
|
150
|
+
undefined: undefined;
|
|
151
|
+
};
|
|
152
|
+
//#endregion
|
|
153
|
+
export { DeduplicateTuple, ExtractValueNames, IsOptionalProperty, LastOfUnion, OptionalKeys, RequiredKeys, TuplePermutations, TupleRemoveAt, UnionToIntersection, UnionToTuple, UnwrapSingle, ValueName, Values };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Schematic } from "../schematic.mjs";
|
|
2
|
+
import { ExtractValueNames, ValueName, Values } from "./misc.model.mjs";
|
|
3
|
+
import { Constructor } from "@oscarpalmer/atoms/models";
|
|
4
|
+
|
|
5
|
+
//#region src/models/schema.plain.model.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* A generic schema allowing {@link NestedSchema}, {@link SchemaEntry}, or arrays of {@link SchemaEntry} as values
|
|
8
|
+
*/
|
|
9
|
+
type PlainSchema = {
|
|
10
|
+
[key: string]: PlainSchema | SchemaEntry | SchemaEntry[] | undefined;
|
|
11
|
+
} & {
|
|
12
|
+
$required?: never;
|
|
13
|
+
$type?: never;
|
|
14
|
+
$validators?: never;
|
|
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
|
+
type Schema = SchemaIndex;
|
|
29
|
+
/**
|
|
30
|
+
* A union of all valid types for a single schema entry
|
|
31
|
+
*
|
|
32
|
+
* Can be a {@link Constructor}, nested {@link Schema}, {@link SchemaProperty}, {@link Schematic}, {@link ValueName} string, or a custom validator function
|
|
33
|
+
*/
|
|
34
|
+
type SchemaEntry = Constructor | PlainSchema | SchemaProperty | Schematic<unknown> | ValueName | ((value: unknown) => boolean);
|
|
35
|
+
/**
|
|
36
|
+
* Index signature interface backing {@link Schema}, allowing string-keyed entries of {@link NestedSchema}, {@link SchemaEntry}, or arrays of {@link SchemaEntry}
|
|
37
|
+
*/
|
|
38
|
+
interface SchemaIndex {
|
|
39
|
+
[key: string]: PlainSchema | SchemaEntry | SchemaEntry[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A property definition with explicit type(s), an optional requirement flag, and optional validators
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const prop: SchemaProperty = {
|
|
47
|
+
* $required: false,
|
|
48
|
+
* $type: ['string', 'number'],
|
|
49
|
+
* $validators: {
|
|
50
|
+
* string: (v) => v.length > 0,
|
|
51
|
+
* number: (v) => v > 0,
|
|
52
|
+
* },
|
|
53
|
+
* };
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
type SchemaProperty = {
|
|
57
|
+
/**
|
|
58
|
+
* Whether the property is required _(defaults to `true`)_
|
|
59
|
+
*/
|
|
60
|
+
$required?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The type(s) the property value must match; a single {@link SchemaPropertyType} or an array
|
|
63
|
+
*/
|
|
64
|
+
$type: SchemaPropertyType | SchemaPropertyType[];
|
|
65
|
+
/**
|
|
66
|
+
* Optional validators keyed by {@link ValueName}, applied during validation
|
|
67
|
+
*/
|
|
68
|
+
$validators?: PropertyValidators<SchemaPropertyType | SchemaPropertyType[]>;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* A union of valid types for a {@link SchemaProperty}'s `$type` field
|
|
72
|
+
*
|
|
73
|
+
* Can be a {@link Constructor}, {@link PlainSchema}, {@link Schematic}, {@link ValueName} string, or a custom validator function
|
|
74
|
+
*/
|
|
75
|
+
type SchemaPropertyType = Constructor | PlainSchema | Schematic<unknown> | ValueName | ((value: unknown) => boolean);
|
|
76
|
+
/**
|
|
77
|
+
* A map of optional validator functions keyed by {@link ValueName}, used to add custom validation to {@link SchemaProperty} definitions
|
|
78
|
+
*
|
|
79
|
+
* Each key may hold a single validator or an array of validators that receive the typed value
|
|
80
|
+
*
|
|
81
|
+
* @template Value - `$type` value(s) to derive validator keys from
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const validators: PropertyValidators<'string'> = {
|
|
86
|
+
* string: (value) => value.length > 0,
|
|
87
|
+
* };
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
type PropertyValidators<Value> = { [Key in ExtractValueNames<Value>]?: ((value: Values[Key]) => boolean) | Array<(value: Values[Key]) => boolean> };
|
|
91
|
+
//#endregion
|
|
92
|
+
export { PlainSchema, PropertyValidators, Schema, SchemaEntry, SchemaIndex, SchemaProperty, SchemaPropertyType };
|