@oscarpalmer/jhunal 0.9.0 → 0.11.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.js +19 -5
- package/dist/index.js +2 -1
- package/dist/is.js +2 -2
- package/dist/jhunal.full.js +75 -32
- package/dist/models.js +8 -0
- package/dist/schematic.js +7 -8
- package/dist/validation/schema.validation.js +45 -18
- package/dist/validation/value.validation.js +5 -5
- package/package.json +4 -4
- package/src/constants.ts +28 -5
- package/src/index.ts +1 -1
- package/src/is.ts +2 -2
- package/src/models.ts +90 -29
- package/src/schematic.ts +19 -12
- package/src/validation/schema.validation.ts +102 -26
- package/src/validation/value.validation.ts +13 -6
- package/types/constants.d.ts +13 -2
- package/types/index.d.ts +1 -1
- package/types/models.d.ts +45 -9
- package/types/schematic.d.ts +2 -3
- package/types/validation/schema.validation.d.ts +2 -2
- package/types/validation/value.validation.d.ts +2 -2
package/dist/constants.js
CHANGED
|
@@ -1,22 +1,36 @@
|
|
|
1
|
+
const ERROR_NAME = "SchematicError";
|
|
1
2
|
const EXPRESSION_HAS_NUMBER = /\d+/;
|
|
2
3
|
const EXPRESSION_INDEX = /\.\d+$/;
|
|
3
|
-
const EXPRESSION_PROPERTY = /\.\$(required|type)(\.|$)/;
|
|
4
|
+
const EXPRESSION_PROPERTY = /\.\$(required|type|validators)(\.|$)/;
|
|
5
|
+
const MESSAGE_CONSTRUCTOR = "Expected a constructor function";
|
|
6
|
+
const MESSAGE_SCHEMA_INVALID_EMPTY = "Schema must have at least one property";
|
|
7
|
+
const MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED = "'<>.$required' property must be a boolean";
|
|
8
|
+
const MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE = "'<>' property must be of a valid type";
|
|
9
|
+
const MESSAGE_SCHEMA_INVALID_TYPE = "Schema must be an object";
|
|
10
|
+
const MESSAGE_VALIDATOR_INVALID_KEY = "Validator '<>' does not exist";
|
|
11
|
+
const MESSAGE_VALIDATOR_INVALID_TYPE = "Validators must be an object";
|
|
12
|
+
const MESSAGE_VALIDATOR_INVALID_VALUE = "Validator '<>' must be a function or an array of functions";
|
|
4
13
|
const PROPERTY_REQUIRED = "$required";
|
|
5
14
|
const PROPERTY_TYPE = "$type";
|
|
15
|
+
const PROPERTY_VALIDATORS = "$validators";
|
|
6
16
|
const SCHEMATIC_NAME = "$schematic";
|
|
17
|
+
const TEMPLATE_PATTERN = "<>";
|
|
7
18
|
const TYPE_OBJECT = "object";
|
|
8
19
|
const TYPE_UNDEFINED = "undefined";
|
|
9
|
-
const
|
|
20
|
+
const VALIDATABLE_TYPES = new Set([
|
|
10
21
|
"array",
|
|
11
22
|
"bigint",
|
|
12
23
|
"boolean",
|
|
13
24
|
"date",
|
|
14
25
|
"function",
|
|
15
|
-
"null",
|
|
16
26
|
"number",
|
|
17
27
|
"string",
|
|
18
28
|
"symbol",
|
|
19
|
-
TYPE_OBJECT
|
|
29
|
+
TYPE_OBJECT
|
|
30
|
+
]);
|
|
31
|
+
const TYPE_ALL = new Set([
|
|
32
|
+
...VALIDATABLE_TYPES,
|
|
33
|
+
"null",
|
|
20
34
|
TYPE_UNDEFINED
|
|
21
35
|
]);
|
|
22
|
-
export { EXPRESSION_HAS_NUMBER, EXPRESSION_INDEX, EXPRESSION_PROPERTY, PROPERTY_REQUIRED, PROPERTY_TYPE, SCHEMATIC_NAME, TYPE_ALL, TYPE_OBJECT, TYPE_UNDEFINED };
|
|
36
|
+
export { ERROR_NAME, EXPRESSION_HAS_NUMBER, EXPRESSION_INDEX, EXPRESSION_PROPERTY, MESSAGE_CONSTRUCTOR, MESSAGE_SCHEMA_INVALID_EMPTY, MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED, MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE, MESSAGE_SCHEMA_INVALID_TYPE, MESSAGE_VALIDATOR_INVALID_KEY, MESSAGE_VALIDATOR_INVALID_TYPE, MESSAGE_VALIDATOR_INVALID_VALUE, PROPERTY_REQUIRED, PROPERTY_TYPE, PROPERTY_VALIDATORS, SCHEMATIC_NAME, TEMPLATE_PATTERN, TYPE_ALL, TYPE_OBJECT, TYPE_UNDEFINED, VALIDATABLE_TYPES };
|
package/dist/index.js
CHANGED
package/dist/is.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import "./constants.js";
|
|
1
|
+
import { MESSAGE_CONSTRUCTOR } from "./constants.js";
|
|
2
2
|
import { isConstructor } from "@oscarpalmer/atoms/is";
|
|
3
3
|
function isInstance(constructor) {
|
|
4
|
-
if (!isConstructor(constructor)) throw new TypeError(
|
|
4
|
+
if (!isConstructor(constructor)) throw new TypeError(MESSAGE_CONSTRUCTOR);
|
|
5
5
|
return (value) => {
|
|
6
6
|
return value instanceof constructor;
|
|
7
7
|
};
|
package/dist/jhunal.full.js
CHANGED
|
@@ -58,29 +58,43 @@ function getString(value) {
|
|
|
58
58
|
function join(value, delimiter) {
|
|
59
59
|
return compact(value).map(getString).join(typeof delimiter === "string" ? delimiter : "");
|
|
60
60
|
}
|
|
61
|
+
const ERROR_NAME = "SchematicError";
|
|
61
62
|
const EXPRESSION_HAS_NUMBER = /\d+/;
|
|
62
63
|
const EXPRESSION_INDEX = /\.\d+$/;
|
|
63
|
-
const EXPRESSION_PROPERTY = /\.\$(required|type)(\.|$)/;
|
|
64
|
+
const EXPRESSION_PROPERTY = /\.\$(required|type|validators)(\.|$)/;
|
|
65
|
+
const MESSAGE_CONSTRUCTOR = "Expected a constructor function";
|
|
66
|
+
const MESSAGE_SCHEMA_INVALID_EMPTY = "Schema must have at least one property";
|
|
67
|
+
const MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED = "'<>.$required' property must be a boolean";
|
|
68
|
+
const MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE = "'<>' property must be of a valid type";
|
|
69
|
+
const MESSAGE_SCHEMA_INVALID_TYPE = "Schema must be an object";
|
|
70
|
+
const MESSAGE_VALIDATOR_INVALID_KEY = "Validator '<>' does not exist";
|
|
71
|
+
const MESSAGE_VALIDATOR_INVALID_TYPE = "Validators must be an object";
|
|
72
|
+
const MESSAGE_VALIDATOR_INVALID_VALUE = "Validator '<>' must be a function or an array of functions";
|
|
64
73
|
const PROPERTY_REQUIRED = "$required";
|
|
65
74
|
const PROPERTY_TYPE = "$type";
|
|
75
|
+
const PROPERTY_VALIDATORS = "$validators";
|
|
66
76
|
const SCHEMATIC_NAME = "$schematic";
|
|
77
|
+
const TEMPLATE_PATTERN = "<>";
|
|
67
78
|
const TYPE_OBJECT = "object";
|
|
68
79
|
const TYPE_UNDEFINED = "undefined";
|
|
69
|
-
const
|
|
80
|
+
const VALIDATABLE_TYPES = new Set([
|
|
70
81
|
"array",
|
|
71
82
|
"bigint",
|
|
72
83
|
"boolean",
|
|
73
84
|
"date",
|
|
74
85
|
"function",
|
|
75
|
-
"null",
|
|
76
86
|
"number",
|
|
77
87
|
"string",
|
|
78
88
|
"symbol",
|
|
79
|
-
TYPE_OBJECT
|
|
89
|
+
TYPE_OBJECT
|
|
90
|
+
]);
|
|
91
|
+
const TYPE_ALL = new Set([
|
|
92
|
+
...VALIDATABLE_TYPES,
|
|
93
|
+
"null",
|
|
80
94
|
TYPE_UNDEFINED
|
|
81
95
|
]);
|
|
82
96
|
function isInstance(constructor) {
|
|
83
|
-
if (!isConstructor(constructor)) throw new TypeError(
|
|
97
|
+
if (!isConstructor(constructor)) throw new TypeError(MESSAGE_CONSTRUCTOR);
|
|
84
98
|
return (value) => {
|
|
85
99
|
return value instanceof constructor;
|
|
86
100
|
};
|
|
@@ -88,6 +102,12 @@ function isInstance(constructor) {
|
|
|
88
102
|
function isSchematic(value) {
|
|
89
103
|
return typeof value === "object" && value !== null && SCHEMATIC_NAME in value && value[SCHEMATIC_NAME] === true;
|
|
90
104
|
}
|
|
105
|
+
var SchematicError = class extends Error {
|
|
106
|
+
constructor(message) {
|
|
107
|
+
super(message);
|
|
108
|
+
this.name = ERROR_NAME;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
91
111
|
function flattenObject(value, depth, smushed, prefix) {
|
|
92
112
|
if (depth >= MAX_DEPTH) return {};
|
|
93
113
|
if (smushed.has(value)) return smushed.get(value);
|
|
@@ -121,30 +141,30 @@ function smush(value) {
|
|
|
121
141
|
return typeof value === "object" && value !== null ? flattenObject(value, 0, /* @__PURE__ */ new WeakMap()) : {};
|
|
122
142
|
}
|
|
123
143
|
var MAX_DEPTH = 100;
|
|
124
|
-
function addPropertyType(to, key, values, required) {
|
|
144
|
+
function addPropertyType(to, key, values, validators, required) {
|
|
125
145
|
if (to.keys.set.has(key)) {
|
|
126
146
|
const property = to.properties[key];
|
|
127
|
-
for (const type of values)
|
|
147
|
+
for (const type of values) property.types.push(type);
|
|
128
148
|
} else {
|
|
129
149
|
to.keys.array.push(key);
|
|
130
150
|
to.keys.set.add(key);
|
|
131
151
|
to.properties[key] = {
|
|
132
152
|
required,
|
|
133
|
-
types: values
|
|
153
|
+
types: values,
|
|
154
|
+
validators: {}
|
|
134
155
|
};
|
|
135
156
|
}
|
|
136
157
|
if (!required && !to.properties[key].types.includes(TYPE_UNDEFINED)) to.properties[key].types.push(TYPE_UNDEFINED);
|
|
158
|
+
to.properties[key].validators = validators;
|
|
137
159
|
}
|
|
138
160
|
function getSchema(schema) {
|
|
139
|
-
|
|
140
|
-
enabled: false,
|
|
161
|
+
return getValidatedSchema(schema, {
|
|
141
162
|
keys: {
|
|
142
163
|
array: [],
|
|
143
164
|
set: /* @__PURE__ */ new Set()
|
|
144
165
|
},
|
|
145
166
|
properties: {}
|
|
146
|
-
};
|
|
147
|
-
return typeof schema === "object" && schema !== null ? getValidatedSchema(schema, validated) : validated;
|
|
167
|
+
});
|
|
148
168
|
}
|
|
149
169
|
function getTypes(value, validated, prefix) {
|
|
150
170
|
const propertyTypes = [];
|
|
@@ -152,23 +172,24 @@ function getTypes(value, validated, prefix) {
|
|
|
152
172
|
const { length } = values;
|
|
153
173
|
for (let index = 0; index < length; index += 1) {
|
|
154
174
|
const type = values[index];
|
|
155
|
-
|
|
156
|
-
if (isSchematic(type) || typeOfType === "string" && TYPE_ALL.has(type)) {
|
|
175
|
+
if (isSchematic(type) || TYPE_ALL.has(type)) {
|
|
157
176
|
propertyTypes.push(type);
|
|
158
177
|
continue;
|
|
159
178
|
}
|
|
160
|
-
if (
|
|
179
|
+
if (typeof type === "function") {
|
|
161
180
|
propertyTypes.push(isConstructor(type) ? isInstance(type) : type);
|
|
162
181
|
continue;
|
|
163
182
|
}
|
|
164
|
-
if (
|
|
183
|
+
if (!isPlainObject(type)) throw new SchematicError(MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace(TEMPLATE_PATTERN, prefix));
|
|
165
184
|
if (PROPERTY_TYPE in type) {
|
|
166
185
|
propertyTypes.push(...getTypes(type[PROPERTY_TYPE], validated, prefix));
|
|
167
186
|
continue;
|
|
168
187
|
}
|
|
169
|
-
|
|
188
|
+
const { [PROPERTY_REQUIRED]: required, ...nested } = type;
|
|
189
|
+
if (PROPERTY_REQUIRED in type && typeof required !== "boolean") throw new SchematicError(MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED.replace(TEMPLATE_PATTERN, prefix));
|
|
190
|
+
addPropertyType(validated, prefix, [TYPE_OBJECT], {}, required !== false);
|
|
170
191
|
propertyTypes.push(TYPE_OBJECT);
|
|
171
|
-
getValidatedSchema(
|
|
192
|
+
getValidatedSchema(nested, validated, prefix);
|
|
172
193
|
}
|
|
173
194
|
return propertyTypes;
|
|
174
195
|
}
|
|
@@ -186,18 +207,43 @@ function getValidatedSchema(schema, validated, prefix) {
|
|
|
186
207
|
if (EXPRESSION_PROPERTY.test(key)) continue;
|
|
187
208
|
if (EXPRESSION_HAS_NUMBER.test(key) && arrayKeys.has(key.replace(EXPRESSION_INDEX, ""))) continue;
|
|
188
209
|
let required = true;
|
|
189
|
-
|
|
210
|
+
let validators = {};
|
|
211
|
+
const isObject = isPlainObject(value);
|
|
212
|
+
if (isObject && PROPERTY_REQUIRED in value) {
|
|
213
|
+
if (typeof value[PROPERTY_REQUIRED] !== "boolean") throw new SchematicError(MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED.replace(TEMPLATE_PATTERN, key));
|
|
214
|
+
required = value[PROPERTY_REQUIRED] === true;
|
|
215
|
+
}
|
|
216
|
+
if (isObject && PROPERTY_VALIDATORS in value) validators = getValidators(value[PROPERTY_VALIDATORS]);
|
|
190
217
|
const prefixedKey = `${prefix}${key}`;
|
|
191
218
|
const types = getTypes(value, validated, prefixedKey);
|
|
192
|
-
if (types.length
|
|
219
|
+
if (types.length === 0) throw new SchematicError(MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace(TEMPLATE_PATTERN, key));
|
|
220
|
+
addPropertyType(validated, prefixedKey, types, validators, required);
|
|
193
221
|
}
|
|
194
222
|
if (noPrefix) validated.keys.array.sort();
|
|
223
|
+
if (noPrefix && validated.keys.array.length === 0) throw new SchematicError(MESSAGE_SCHEMA_INVALID_EMPTY);
|
|
195
224
|
return validated;
|
|
196
225
|
}
|
|
197
|
-
function
|
|
226
|
+
function getValidators(original) {
|
|
227
|
+
const validators = {};
|
|
228
|
+
if (original == null) return validators;
|
|
229
|
+
if (!isPlainObject(original)) throw new TypeError(MESSAGE_VALIDATOR_INVALID_TYPE);
|
|
230
|
+
const keys = Object.keys(original);
|
|
231
|
+
const { length } = keys;
|
|
232
|
+
for (let index = 0; index < length; index += 1) {
|
|
233
|
+
const key = keys[index];
|
|
234
|
+
if (!VALIDATABLE_TYPES.has(key)) throw new TypeError(MESSAGE_VALIDATOR_INVALID_KEY.replace(TEMPLATE_PATTERN, key));
|
|
235
|
+
const value = original[key];
|
|
236
|
+
validators[key] = (Array.isArray(value) ? value : [value]).filter((item) => {
|
|
237
|
+
if (typeof item !== "function") throw new TypeError(MESSAGE_VALIDATOR_INVALID_VALUE.replace(TEMPLATE_PATTERN, key));
|
|
238
|
+
return true;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return validators;
|
|
242
|
+
}
|
|
243
|
+
function validateType(type, property, value) {
|
|
198
244
|
switch (true) {
|
|
199
245
|
case typeof type === "function": return type(value);
|
|
200
|
-
case typeof type === "string": return validators[type](value);
|
|
246
|
+
case typeof type === "string": return validators[type](value) && (property.validators[type]?.every((validator) => validator(value)) ?? true);
|
|
201
247
|
default: return type.is(value);
|
|
202
248
|
}
|
|
203
249
|
}
|
|
@@ -216,12 +262,12 @@ function validateValue(validated, obj) {
|
|
|
216
262
|
if (value === void 0 && property.required && !property.types.includes(TYPE_UNDEFINED)) return false;
|
|
217
263
|
const typesLength = property.types.length;
|
|
218
264
|
if (typesLength === 1) {
|
|
219
|
-
if (!validateType(property.types[0], value)) return false;
|
|
265
|
+
if (!validateType(property.types[0], property, value)) return false;
|
|
220
266
|
continue;
|
|
221
267
|
}
|
|
222
268
|
for (let typeIndex = 0; typeIndex < typesLength; typeIndex += 1) {
|
|
223
269
|
const type = property.types[typeIndex];
|
|
224
|
-
if (validateType(type, value)) {
|
|
270
|
+
if (validateType(type, property, value)) {
|
|
225
271
|
if (type !== "object") ignore.add(key);
|
|
226
272
|
continue outer;
|
|
227
273
|
}
|
|
@@ -249,22 +295,19 @@ const validators = {
|
|
|
249
295
|
*/
|
|
250
296
|
var Schematic = class {
|
|
251
297
|
#schema;
|
|
252
|
-
get enabled() {
|
|
253
|
-
return this.#schema.enabled;
|
|
254
|
-
}
|
|
255
298
|
constructor(schema) {
|
|
256
299
|
Object.defineProperty(this, SCHEMATIC_NAME, { value: true });
|
|
257
|
-
this.#schema =
|
|
258
|
-
this.#schema.enabled = this.#schema.keys.array.length > 0;
|
|
300
|
+
this.#schema = schema;
|
|
259
301
|
}
|
|
260
302
|
/**
|
|
261
303
|
* Does the value match the schema?
|
|
262
304
|
*/
|
|
263
305
|
is(value) {
|
|
264
|
-
return
|
|
306
|
+
return validateValue(this.#schema, value);
|
|
265
307
|
}
|
|
266
308
|
};
|
|
267
309
|
function schematic(schema) {
|
|
268
|
-
|
|
310
|
+
if (!isPlainObject(schema)) throw new SchematicError(MESSAGE_SCHEMA_INVALID_TYPE);
|
|
311
|
+
return new Schematic(getSchema(schema));
|
|
269
312
|
}
|
|
270
|
-
export { isInstance, schematic };
|
|
313
|
+
export { SchematicError, isInstance, schematic };
|
package/dist/models.js
CHANGED
package/dist/schematic.js
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
|
-
import { SCHEMATIC_NAME } from "./constants.js";
|
|
1
|
+
import { MESSAGE_SCHEMA_INVALID_TYPE, SCHEMATIC_NAME } from "./constants.js";
|
|
2
|
+
import { SchematicError } from "./models.js";
|
|
2
3
|
import { getSchema } from "./validation/schema.validation.js";
|
|
3
4
|
import { validateValue } from "./validation/value.validation.js";
|
|
5
|
+
import { isPlainObject } from "@oscarpalmer/atoms/is";
|
|
4
6
|
/**
|
|
5
7
|
* A schematic for validating objects
|
|
6
8
|
*/
|
|
7
9
|
var Schematic = class {
|
|
8
10
|
#schema;
|
|
9
|
-
get enabled() {
|
|
10
|
-
return this.#schema.enabled;
|
|
11
|
-
}
|
|
12
11
|
constructor(schema) {
|
|
13
12
|
Object.defineProperty(this, SCHEMATIC_NAME, { value: true });
|
|
14
|
-
this.#schema =
|
|
15
|
-
this.#schema.enabled = this.#schema.keys.array.length > 0;
|
|
13
|
+
this.#schema = schema;
|
|
16
14
|
}
|
|
17
15
|
/**
|
|
18
16
|
* Does the value match the schema?
|
|
19
17
|
*/
|
|
20
18
|
is(value) {
|
|
21
|
-
return
|
|
19
|
+
return validateValue(this.#schema, value);
|
|
22
20
|
}
|
|
23
21
|
};
|
|
24
22
|
function schematic(schema) {
|
|
25
|
-
|
|
23
|
+
if (!isPlainObject(schema)) throw new SchematicError(MESSAGE_SCHEMA_INVALID_TYPE);
|
|
24
|
+
return new Schematic(getSchema(schema));
|
|
26
25
|
}
|
|
27
26
|
export { Schematic, schematic };
|
|
@@ -1,31 +1,32 @@
|
|
|
1
|
-
import { EXPRESSION_HAS_NUMBER, EXPRESSION_INDEX, EXPRESSION_PROPERTY, PROPERTY_REQUIRED, PROPERTY_TYPE, TYPE_ALL, TYPE_OBJECT, TYPE_UNDEFINED } from "../constants.js";
|
|
1
|
+
import { EXPRESSION_HAS_NUMBER, EXPRESSION_INDEX, EXPRESSION_PROPERTY, MESSAGE_SCHEMA_INVALID_EMPTY, MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED, MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE, MESSAGE_VALIDATOR_INVALID_KEY, MESSAGE_VALIDATOR_INVALID_TYPE, MESSAGE_VALIDATOR_INVALID_VALUE, PROPERTY_REQUIRED, PROPERTY_TYPE, PROPERTY_VALIDATORS, TYPE_ALL, TYPE_OBJECT, TYPE_UNDEFINED, VALIDATABLE_TYPES } from "../constants.js";
|
|
2
2
|
import { isInstance, isSchematic } from "../is.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { SchematicError } from "../models.js";
|
|
4
|
+
import { isConstructor, isPlainObject } from "@oscarpalmer/atoms/is";
|
|
5
|
+
import { smush } from "@oscarpalmer/atoms/value/misc";
|
|
6
|
+
function addPropertyType(to, key, values, validators, required) {
|
|
6
7
|
if (to.keys.set.has(key)) {
|
|
7
8
|
const property = to.properties[key];
|
|
8
|
-
for (const type of values)
|
|
9
|
+
for (const type of values) property.types.push(type);
|
|
9
10
|
} else {
|
|
10
11
|
to.keys.array.push(key);
|
|
11
12
|
to.keys.set.add(key);
|
|
12
13
|
to.properties[key] = {
|
|
13
14
|
required,
|
|
14
|
-
types: values
|
|
15
|
+
types: values,
|
|
16
|
+
validators: {}
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
if (!required && !to.properties[key].types.includes("undefined")) to.properties[key].types.push(TYPE_UNDEFINED);
|
|
20
|
+
to.properties[key].validators = validators;
|
|
18
21
|
}
|
|
19
22
|
function getSchema(schema) {
|
|
20
|
-
|
|
21
|
-
enabled: false,
|
|
23
|
+
return getValidatedSchema(schema, {
|
|
22
24
|
keys: {
|
|
23
25
|
array: [],
|
|
24
26
|
set: /* @__PURE__ */ new Set()
|
|
25
27
|
},
|
|
26
28
|
properties: {}
|
|
27
|
-
};
|
|
28
|
-
return typeof schema === "object" && schema !== null ? getValidatedSchema(schema, validated) : validated;
|
|
29
|
+
});
|
|
29
30
|
}
|
|
30
31
|
function getTypes(value, validated, prefix) {
|
|
31
32
|
const propertyTypes = [];
|
|
@@ -33,23 +34,24 @@ function getTypes(value, validated, prefix) {
|
|
|
33
34
|
const { length } = values;
|
|
34
35
|
for (let index = 0; index < length; index += 1) {
|
|
35
36
|
const type = values[index];
|
|
36
|
-
|
|
37
|
-
if (isSchematic(type) || typeOfType === "string" && TYPE_ALL.has(type)) {
|
|
37
|
+
if (isSchematic(type) || TYPE_ALL.has(type)) {
|
|
38
38
|
propertyTypes.push(type);
|
|
39
39
|
continue;
|
|
40
40
|
}
|
|
41
|
-
if (
|
|
41
|
+
if (typeof type === "function") {
|
|
42
42
|
propertyTypes.push(isConstructor(type) ? isInstance(type) : type);
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
|
-
if (
|
|
45
|
+
if (!isPlainObject(type)) throw new SchematicError(MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace("<>", prefix));
|
|
46
46
|
if ("$type" in type) {
|
|
47
47
|
propertyTypes.push(...getTypes(type[PROPERTY_TYPE], validated, prefix));
|
|
48
48
|
continue;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
const { [PROPERTY_REQUIRED]: required, ...nested } = type;
|
|
51
|
+
if ("$required" in type && typeof required !== "boolean") throw new SchematicError(MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED.replace("<>", prefix));
|
|
52
|
+
addPropertyType(validated, prefix, [TYPE_OBJECT], {}, required !== false);
|
|
51
53
|
propertyTypes.push(TYPE_OBJECT);
|
|
52
|
-
getValidatedSchema(
|
|
54
|
+
getValidatedSchema(nested, validated, prefix);
|
|
53
55
|
}
|
|
54
56
|
return propertyTypes;
|
|
55
57
|
}
|
|
@@ -67,12 +69,37 @@ function getValidatedSchema(schema, validated, prefix) {
|
|
|
67
69
|
if (EXPRESSION_PROPERTY.test(key)) continue;
|
|
68
70
|
if (EXPRESSION_HAS_NUMBER.test(key) && arrayKeys.has(key.replace(EXPRESSION_INDEX, ""))) continue;
|
|
69
71
|
let required = true;
|
|
70
|
-
|
|
72
|
+
let validators = {};
|
|
73
|
+
const isObject = isPlainObject(value);
|
|
74
|
+
if (isObject && "$required" in value) {
|
|
75
|
+
if (typeof value["$required"] !== "boolean") throw new SchematicError(MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED.replace("<>", key));
|
|
76
|
+
required = value[PROPERTY_REQUIRED] === true;
|
|
77
|
+
}
|
|
78
|
+
if (isObject && "$validators" in value) validators = getValidators(value[PROPERTY_VALIDATORS]);
|
|
71
79
|
const prefixedKey = `${prefix}${key}`;
|
|
72
80
|
const types = getTypes(value, validated, prefixedKey);
|
|
73
|
-
if (types.length
|
|
81
|
+
if (types.length === 0) throw new SchematicError(MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace("<>", key));
|
|
82
|
+
addPropertyType(validated, prefixedKey, types, validators, required);
|
|
74
83
|
}
|
|
75
84
|
if (noPrefix) validated.keys.array.sort();
|
|
85
|
+
if (noPrefix && validated.keys.array.length === 0) throw new SchematicError(MESSAGE_SCHEMA_INVALID_EMPTY);
|
|
76
86
|
return validated;
|
|
77
87
|
}
|
|
88
|
+
function getValidators(original) {
|
|
89
|
+
const validators = {};
|
|
90
|
+
if (original == null) return validators;
|
|
91
|
+
if (!isPlainObject(original)) throw new TypeError(MESSAGE_VALIDATOR_INVALID_TYPE);
|
|
92
|
+
const keys = Object.keys(original);
|
|
93
|
+
const { length } = keys;
|
|
94
|
+
for (let index = 0; index < length; index += 1) {
|
|
95
|
+
const key = keys[index];
|
|
96
|
+
if (!VALIDATABLE_TYPES.has(key)) throw new TypeError(MESSAGE_VALIDATOR_INVALID_KEY.replace("<>", key));
|
|
97
|
+
const value = original[key];
|
|
98
|
+
validators[key] = (Array.isArray(value) ? value : [value]).filter((item) => {
|
|
99
|
+
if (typeof item !== "function") throw new TypeError(MESSAGE_VALIDATOR_INVALID_VALUE.replace("<>", key));
|
|
100
|
+
return true;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return validators;
|
|
104
|
+
}
|
|
78
105
|
export { getSchema };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import "../constants.js";
|
|
2
|
-
import { smush } from "@oscarpalmer/atoms/value";
|
|
3
|
-
function validateType(type, value) {
|
|
2
|
+
import { smush } from "@oscarpalmer/atoms/value/misc";
|
|
3
|
+
function validateType(type, property, value) {
|
|
4
4
|
switch (true) {
|
|
5
5
|
case typeof type === "function": return type(value);
|
|
6
|
-
case typeof type === "string": return validators[type](value);
|
|
6
|
+
case typeof type === "string": return validators[type](value) && (property.validators[type]?.every((validator) => validator(value)) ?? true);
|
|
7
7
|
default: return type.is(value);
|
|
8
8
|
}
|
|
9
9
|
}
|
|
@@ -22,12 +22,12 @@ function validateValue(validated, obj) {
|
|
|
22
22
|
if (value === void 0 && property.required && !property.types.includes("undefined")) return false;
|
|
23
23
|
const typesLength = property.types.length;
|
|
24
24
|
if (typesLength === 1) {
|
|
25
|
-
if (!validateType(property.types[0], value)) return false;
|
|
25
|
+
if (!validateType(property.types[0], property, value)) return false;
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
28
|
for (let typeIndex = 0; typeIndex < typesLength; typeIndex += 1) {
|
|
29
29
|
const type = property.types[typeIndex];
|
|
30
|
-
if (validateType(type, value)) {
|
|
30
|
+
if (validateType(type, property, value)) {
|
|
31
31
|
if (type !== "object") ignore.add(key);
|
|
32
32
|
continue outer;
|
|
33
33
|
}
|
package/package.json
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
"url": "https://oscarpalmer.se"
|
|
5
5
|
},
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@oscarpalmer/atoms": "^0.
|
|
7
|
+
"@oscarpalmer/atoms": "^0.141.2"
|
|
8
8
|
},
|
|
9
9
|
"description": "Flies free beneath the glistening moons…",
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"@types/node": "^25.3",
|
|
12
12
|
"@vitest/coverage-istanbul": "^4",
|
|
13
13
|
"jsdom": "^28.1",
|
|
14
|
-
"oxfmt": "^0.
|
|
15
|
-
"oxlint": "^1.
|
|
14
|
+
"oxfmt": "^0.35",
|
|
15
|
+
"oxlint": "^1.50",
|
|
16
16
|
"rolldown": "1.0.0-rc.5",
|
|
17
17
|
"tslib": "^2.8",
|
|
18
18
|
"typescript": "^5.9",
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
},
|
|
46
46
|
"type": "module",
|
|
47
47
|
"types": "./types/index.d.ts",
|
|
48
|
-
"version": "0.
|
|
48
|
+
"version": "0.11.0"
|
|
49
49
|
}
|
package/src/constants.ts
CHANGED
|
@@ -1,31 +1,54 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {ValueName} from './models';
|
|
2
|
+
|
|
3
|
+
export const ERROR_NAME = 'SchematicError';
|
|
2
4
|
|
|
3
5
|
export const EXPRESSION_HAS_NUMBER = /\d+/;
|
|
4
6
|
|
|
5
7
|
export const EXPRESSION_INDEX = /\.\d+$/;
|
|
6
8
|
|
|
7
|
-
export const EXPRESSION_PROPERTY = /\.\$(required|type)(\.|$)/;
|
|
9
|
+
export const EXPRESSION_PROPERTY = /\.\$(required|type|validators)(\.|$)/;
|
|
10
|
+
|
|
11
|
+
export const MESSAGE_CONSTRUCTOR = 'Expected a constructor function';
|
|
12
|
+
|
|
13
|
+
export const MESSAGE_SCHEMA_INVALID_EMPTY = 'Schema must have at least one property';
|
|
14
|
+
|
|
15
|
+
export const MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED = "'<>.$required' property must be a boolean";
|
|
16
|
+
|
|
17
|
+
export const MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE = "'<>' property must be of a valid type";
|
|
18
|
+
|
|
19
|
+
export const MESSAGE_SCHEMA_INVALID_TYPE = 'Schema must be an object';
|
|
20
|
+
|
|
21
|
+
export const MESSAGE_VALIDATOR_INVALID_KEY = "Validator '<>' does not exist";
|
|
22
|
+
|
|
23
|
+
export const MESSAGE_VALIDATOR_INVALID_TYPE = 'Validators must be an object';
|
|
24
|
+
|
|
25
|
+
export const MESSAGE_VALIDATOR_INVALID_VALUE =
|
|
26
|
+
"Validator '<>' must be a function or an array of functions";
|
|
8
27
|
|
|
9
28
|
export const PROPERTY_REQUIRED = '$required';
|
|
10
29
|
|
|
11
30
|
export const PROPERTY_TYPE = '$type';
|
|
12
31
|
|
|
32
|
+
export const PROPERTY_VALIDATORS = '$validators';
|
|
33
|
+
|
|
13
34
|
export const SCHEMATIC_NAME = '$schematic';
|
|
14
35
|
|
|
36
|
+
export const TEMPLATE_PATTERN = '<>';
|
|
37
|
+
|
|
15
38
|
export const TYPE_OBJECT = 'object';
|
|
16
39
|
|
|
17
40
|
export const TYPE_UNDEFINED = 'undefined';
|
|
18
41
|
|
|
19
|
-
export const
|
|
42
|
+
export const VALIDATABLE_TYPES = new Set<ValueName>([
|
|
20
43
|
'array',
|
|
21
44
|
'bigint',
|
|
22
45
|
'boolean',
|
|
23
46
|
'date',
|
|
24
47
|
'function',
|
|
25
|
-
'null',
|
|
26
48
|
'number',
|
|
27
49
|
'string',
|
|
28
50
|
'symbol',
|
|
29
51
|
TYPE_OBJECT,
|
|
30
|
-
TYPE_UNDEFINED,
|
|
31
52
|
]);
|
|
53
|
+
|
|
54
|
+
export const TYPE_ALL = new Set<ValueName>([...VALIDATABLE_TYPES, 'null', TYPE_UNDEFINED]);
|
package/src/index.ts
CHANGED
package/src/is.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {isConstructor} from '@oscarpalmer/atoms/is';
|
|
2
|
-
import {SCHEMATIC_NAME} from './constants';
|
|
2
|
+
import {MESSAGE_CONSTRUCTOR, SCHEMATIC_NAME} from './constants';
|
|
3
3
|
import type {Constructor} from './models';
|
|
4
4
|
import type {Schematic} from './schematic';
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ export function isInstance<Instance>(
|
|
|
7
7
|
constructor: Constructor<Instance>,
|
|
8
8
|
): (value: unknown) => value is Instance {
|
|
9
9
|
if (!isConstructor(constructor)) {
|
|
10
|
-
throw new TypeError(
|
|
10
|
+
throw new TypeError(MESSAGE_CONSTRUCTOR);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
return (value: unknown): value is Instance => {
|
package/src/models.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {PlainObject, Simplify} from '@oscarpalmer/atoms/models';
|
|
2
|
+
import {ERROR_NAME} from './constants';
|
|
2
3
|
import type {Schematic} from './schematic';
|
|
3
4
|
|
|
4
5
|
export type Constructor<Instance = any> = new (...args: any[]) => Instance;
|
|
@@ -12,6 +13,14 @@ type DeduplicateTuple<Value extends unknown[], Seen extends unknown[] = []> = Va
|
|
|
12
13
|
: DeduplicateTuple<Tail, [...Seen, Head]>
|
|
13
14
|
: Seen;
|
|
14
15
|
|
|
16
|
+
type ExtractValueNames<Value> = Value extends ValueName
|
|
17
|
+
? Value
|
|
18
|
+
: Value extends (infer Item)[]
|
|
19
|
+
? ExtractValueNames<Item>
|
|
20
|
+
: Value extends readonly (infer Item)[]
|
|
21
|
+
? ExtractValueNames<Item>
|
|
22
|
+
: never;
|
|
23
|
+
|
|
15
24
|
/**
|
|
16
25
|
* Infer the TypeScript type from a schema definition
|
|
17
26
|
*/
|
|
@@ -57,17 +66,23 @@ type InferSchemaEntryValue<Value> =
|
|
|
57
66
|
? Model
|
|
58
67
|
: Value extends SchemaProperty
|
|
59
68
|
? InferPropertyType<Value['$type']>
|
|
60
|
-
: Value extends
|
|
61
|
-
?
|
|
62
|
-
: Value extends
|
|
63
|
-
?
|
|
64
|
-
:
|
|
69
|
+
: Value extends NestedSchema
|
|
70
|
+
? Infer<Omit<Value, '$required'>>
|
|
71
|
+
: Value extends ValueName
|
|
72
|
+
? Values[Value & ValueName]
|
|
73
|
+
: Value extends Schema
|
|
74
|
+
? Infer<Value>
|
|
75
|
+
: never;
|
|
65
76
|
|
|
66
77
|
type IsOptionalProperty<Value> = Value extends SchemaProperty
|
|
67
78
|
? Value['$required'] extends false
|
|
68
79
|
? true
|
|
69
80
|
: false
|
|
70
|
-
:
|
|
81
|
+
: Value extends {$required?: boolean}
|
|
82
|
+
? Value extends {$required: false}
|
|
83
|
+
? true
|
|
84
|
+
: false
|
|
85
|
+
: false;
|
|
71
86
|
|
|
72
87
|
type LastOfUnion<Value> =
|
|
73
88
|
UnionToIntersection<Value extends unknown ? () => Value : never> extends () => infer Item
|
|
@@ -87,12 +102,19 @@ type MapToSchemaPropertyTypes<Value extends unknown[]> = Value extends [infer He
|
|
|
87
102
|
*/
|
|
88
103
|
export type NestedSchema = {
|
|
89
104
|
$required?: boolean;
|
|
90
|
-
|
|
105
|
+
[key: string]: any;
|
|
106
|
+
};
|
|
91
107
|
|
|
92
108
|
type OptionalKeys<Value> = {
|
|
93
109
|
[Key in keyof Value]-?: {} extends Pick<Value, Key> ? Key : never;
|
|
94
110
|
}[keyof Value];
|
|
95
111
|
|
|
112
|
+
type PropertyValidators<Value> = {
|
|
113
|
+
[Key in ExtractValueNames<Value>]?:
|
|
114
|
+
| ((value: Values[Key]) => boolean)
|
|
115
|
+
| Array<(value: Values[Key]) => boolean>;
|
|
116
|
+
};
|
|
117
|
+
|
|
96
118
|
type RequiredKeys<Value> = Exclude<keyof Value, OptionalKeys<Value>>;
|
|
97
119
|
|
|
98
120
|
/**
|
|
@@ -100,7 +122,7 @@ type RequiredKeys<Value> = Exclude<keyof Value, OptionalKeys<Value>>;
|
|
|
100
122
|
*/
|
|
101
123
|
export type Schema = SchemaIndex;
|
|
102
124
|
|
|
103
|
-
type SchemaEntry = Constructor |
|
|
125
|
+
type SchemaEntry = Constructor | SchemaProperty | Schematic<unknown> | ValueName | NestedSchema;
|
|
104
126
|
|
|
105
127
|
interface SchemaIndex {
|
|
106
128
|
[key: string]: SchemaEntry | SchemaEntry[];
|
|
@@ -112,17 +134,28 @@ interface SchemaIndex {
|
|
|
112
134
|
export type SchemaProperty = {
|
|
113
135
|
$required?: boolean;
|
|
114
136
|
$type: SchemaPropertyType | SchemaPropertyType[];
|
|
137
|
+
$validators?: PropertyValidators<SchemaPropertyType | SchemaPropertyType[]>;
|
|
115
138
|
};
|
|
116
139
|
|
|
117
140
|
type SchemaPropertyType = Constructor | Schema | Schematic<unknown> | ValueName;
|
|
118
141
|
|
|
142
|
+
export class SchematicError extends Error {
|
|
143
|
+
constructor(message: string) {
|
|
144
|
+
super(message);
|
|
145
|
+
|
|
146
|
+
this.name = ERROR_NAME;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
119
150
|
type ToSchemaPropertyType<Value> = UnwrapSingle<
|
|
120
151
|
DeduplicateTuple<MapToSchemaPropertyTypes<UnionToTuple<Value>>>
|
|
121
152
|
>;
|
|
122
153
|
|
|
123
|
-
type ToSchemaPropertyTypeEach<Value> = Value extends
|
|
124
|
-
?
|
|
125
|
-
:
|
|
154
|
+
type ToSchemaPropertyTypeEach<Value> = Value extends NestedSchema
|
|
155
|
+
? Omit<Value, '$required'>
|
|
156
|
+
: Value extends PlainObject
|
|
157
|
+
? TypedSchema<Value>
|
|
158
|
+
: ToValueType<Value>;
|
|
126
159
|
|
|
127
160
|
type ToSchemaType<Value> = UnwrapSingle<DeduplicateTuple<MapToValueTypes<UnionToTuple<Value>>>>;
|
|
128
161
|
|
|
@@ -134,21 +167,23 @@ type ToValueType<Value> = Value extends unknown[]
|
|
|
134
167
|
? 'boolean'
|
|
135
168
|
: Value extends Date
|
|
136
169
|
? 'date'
|
|
137
|
-
: Value extends
|
|
138
|
-
?
|
|
139
|
-
: Value extends
|
|
140
|
-
? '
|
|
141
|
-
: Value extends
|
|
142
|
-
? '
|
|
143
|
-
: Value extends
|
|
144
|
-
? '
|
|
145
|
-
: Value extends
|
|
146
|
-
? '
|
|
147
|
-
: Value extends
|
|
148
|
-
? '
|
|
149
|
-
: Value extends
|
|
150
|
-
? '
|
|
151
|
-
:
|
|
170
|
+
: Value extends Schematic<any>
|
|
171
|
+
? Value
|
|
172
|
+
: Value extends Function
|
|
173
|
+
? 'function'
|
|
174
|
+
: Value extends null
|
|
175
|
+
? 'null'
|
|
176
|
+
: Value extends number
|
|
177
|
+
? 'number'
|
|
178
|
+
: Value extends object
|
|
179
|
+
? 'object' | ((value: unknown) => value is Value)
|
|
180
|
+
: Value extends string
|
|
181
|
+
? 'string'
|
|
182
|
+
: Value extends symbol
|
|
183
|
+
? 'symbol'
|
|
184
|
+
: Value extends undefined
|
|
185
|
+
? 'undefined'
|
|
186
|
+
: (value: unknown) => value is Value;
|
|
152
187
|
|
|
153
188
|
type TuplePermutations<
|
|
154
189
|
Tuple extends unknown[],
|
|
@@ -173,13 +208,33 @@ type TupleRemoveAt<
|
|
|
173
208
|
: Prefix;
|
|
174
209
|
|
|
175
210
|
export type TypedPropertyOptional<Value> = {
|
|
211
|
+
/**
|
|
212
|
+
* The property is not required
|
|
213
|
+
*/
|
|
176
214
|
$required: false;
|
|
215
|
+
/**
|
|
216
|
+
* The type(s) of the property
|
|
217
|
+
*/
|
|
177
218
|
$type: ToSchemaPropertyType<Exclude<Value, undefined>>;
|
|
219
|
+
/**
|
|
220
|
+
* Custom validators for the property and its types
|
|
221
|
+
*/
|
|
222
|
+
$validators?: PropertyValidators<ToSchemaPropertyType<Exclude<Value, undefined>>>;
|
|
178
223
|
};
|
|
179
224
|
|
|
180
225
|
export type TypedPropertyRequired<Value> = {
|
|
226
|
+
/**
|
|
227
|
+
* The property is required _(defaults to `true`)_
|
|
228
|
+
*/
|
|
181
229
|
$required?: true;
|
|
230
|
+
/**
|
|
231
|
+
* The type(s) of the property
|
|
232
|
+
*/
|
|
182
233
|
$type: ToSchemaPropertyType<Value>;
|
|
234
|
+
/**
|
|
235
|
+
* Custom validators for the property and its types
|
|
236
|
+
*/
|
|
237
|
+
$validators?: PropertyValidators<ToSchemaPropertyType<Value>>;
|
|
183
238
|
};
|
|
184
239
|
|
|
185
240
|
/**
|
|
@@ -188,11 +243,13 @@ export type TypedPropertyRequired<Value> = {
|
|
|
188
243
|
export type TypedSchema<Model extends PlainObject> = Simplify<
|
|
189
244
|
{
|
|
190
245
|
[Key in RequiredKeys<Model>]: Model[Key] extends PlainObject
|
|
191
|
-
? TypedSchemaRequired<Model[Key]>
|
|
246
|
+
? TypedSchemaRequired<Model[Key]> | Schematic<Model[Key]>
|
|
192
247
|
: ToSchemaType<Model[Key]> | TypedPropertyRequired<Model[Key]>;
|
|
193
248
|
} & {
|
|
194
249
|
[Key in OptionalKeys<Model>]: Exclude<Model[Key], undefined> extends PlainObject
|
|
195
|
-
?
|
|
250
|
+
?
|
|
251
|
+
| TypedSchemaOptional<Exclude<Model[Key], undefined>>
|
|
252
|
+
| Schematic<Exclude<Model[Key], undefined>>
|
|
196
253
|
: TypedPropertyOptional<Model[Key]>;
|
|
197
254
|
}
|
|
198
255
|
>;
|
|
@@ -224,12 +281,16 @@ type UnwrapSingle<Value extends unknown[]> = Value extends [infer Only]
|
|
|
224
281
|
export type ValidatedProperty = {
|
|
225
282
|
required: boolean;
|
|
226
283
|
types: ValidatedPropertyType[];
|
|
284
|
+
validators: ValidatedPropertyValidators;
|
|
227
285
|
};
|
|
228
286
|
|
|
229
287
|
export type ValidatedPropertyType = Schematic<unknown> | ValueName;
|
|
230
288
|
|
|
289
|
+
export type ValidatedPropertyValidators = {
|
|
290
|
+
[Key in ValueName]?: Array<(value: unknown) => boolean>;
|
|
291
|
+
};
|
|
292
|
+
|
|
231
293
|
export type ValidatedSchema = {
|
|
232
|
-
enabled: boolean;
|
|
233
294
|
keys: {
|
|
234
295
|
array: string[];
|
|
235
296
|
set: Set<string>;
|
package/src/schematic.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import {isPlainObject} from '@oscarpalmer/atoms/is';
|
|
1
2
|
import type {PlainObject} from '@oscarpalmer/atoms/models';
|
|
2
|
-
import {SCHEMATIC_NAME} from './constants';
|
|
3
|
-
import
|
|
3
|
+
import {MESSAGE_SCHEMA_INVALID_TYPE, SCHEMATIC_NAME} from './constants';
|
|
4
|
+
import {
|
|
5
|
+
SchematicError,
|
|
6
|
+
type Infer,
|
|
7
|
+
type Schema,
|
|
8
|
+
type TypedSchema,
|
|
9
|
+
type ValidatedSchema,
|
|
10
|
+
} from './models';
|
|
4
11
|
import {getSchema} from './validation/schema.validation';
|
|
5
12
|
import {validateValue} from './validation/value.validation';
|
|
6
13
|
|
|
@@ -12,25 +19,19 @@ export class Schematic<Model> {
|
|
|
12
19
|
|
|
13
20
|
#schema: ValidatedSchema;
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
return this.#schema.enabled;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
constructor(schema: Model) {
|
|
22
|
+
constructor(schema: ValidatedSchema) {
|
|
20
23
|
Object.defineProperty(this, SCHEMATIC_NAME, {
|
|
21
24
|
value: true,
|
|
22
25
|
});
|
|
23
26
|
|
|
24
|
-
this.#schema =
|
|
25
|
-
|
|
26
|
-
this.#schema.enabled = this.#schema.keys.array.length > 0;
|
|
27
|
+
this.#schema = schema;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Does the value match the schema?
|
|
31
32
|
*/
|
|
32
33
|
is(value: unknown): value is Model {
|
|
33
|
-
return
|
|
34
|
+
return validateValue(this.#schema, value);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -45,5 +46,11 @@ export function schematic<Model extends Schema>(schema: Model): Schematic<Infer<
|
|
|
45
46
|
export function schematic<Model extends PlainObject>(schema: TypedSchema<Model>): Schematic<Model>;
|
|
46
47
|
|
|
47
48
|
export function schematic<Model extends Schema>(schema: Model): Schematic<Model> {
|
|
48
|
-
|
|
49
|
+
if (!isPlainObject(schema)) {
|
|
50
|
+
throw new SchematicError(MESSAGE_SCHEMA_INVALID_TYPE);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const validated = getSchema(schema);
|
|
54
|
+
|
|
55
|
+
return new Schematic<Model>(validated);
|
|
49
56
|
}
|
|
@@ -1,32 +1,47 @@
|
|
|
1
|
-
import {isConstructor} from '@oscarpalmer/atoms/is';
|
|
1
|
+
import {isConstructor, isPlainObject} from '@oscarpalmer/atoms/is';
|
|
2
2
|
import type {PlainObject} from '@oscarpalmer/atoms/models';
|
|
3
|
-
import {smush} from '@oscarpalmer/atoms/value';
|
|
3
|
+
import {smush} from '@oscarpalmer/atoms/value/misc';
|
|
4
4
|
import {
|
|
5
5
|
EXPRESSION_HAS_NUMBER,
|
|
6
6
|
EXPRESSION_INDEX,
|
|
7
7
|
EXPRESSION_PROPERTY,
|
|
8
|
+
MESSAGE_SCHEMA_INVALID_EMPTY,
|
|
9
|
+
MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED,
|
|
10
|
+
MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE,
|
|
11
|
+
MESSAGE_VALIDATOR_INVALID_KEY,
|
|
12
|
+
MESSAGE_VALIDATOR_INVALID_TYPE,
|
|
13
|
+
MESSAGE_VALIDATOR_INVALID_VALUE,
|
|
8
14
|
PROPERTY_REQUIRED,
|
|
9
15
|
PROPERTY_TYPE,
|
|
16
|
+
PROPERTY_VALIDATORS,
|
|
17
|
+
TEMPLATE_PATTERN,
|
|
10
18
|
TYPE_ALL,
|
|
11
19
|
TYPE_OBJECT,
|
|
12
20
|
TYPE_UNDEFINED,
|
|
21
|
+
VALIDATABLE_TYPES,
|
|
13
22
|
} from '../constants';
|
|
14
23
|
import {isInstance, isSchematic} from '../is';
|
|
15
|
-
import
|
|
24
|
+
import {
|
|
25
|
+
SchematicError,
|
|
26
|
+
type Schema,
|
|
27
|
+
type ValidatedPropertyType,
|
|
28
|
+
type ValidatedPropertyValidators,
|
|
29
|
+
type ValidatedSchema,
|
|
30
|
+
type ValueName,
|
|
31
|
+
} from '../models';
|
|
16
32
|
|
|
17
33
|
function addPropertyType(
|
|
18
34
|
to: ValidatedSchema,
|
|
19
35
|
key: string,
|
|
20
36
|
values: ValidatedPropertyType[],
|
|
37
|
+
validators: ValidatedPropertyValidators,
|
|
21
38
|
required: boolean,
|
|
22
39
|
): void {
|
|
23
40
|
if (to.keys.set.has(key)) {
|
|
24
41
|
const property = to.properties[key];
|
|
25
42
|
|
|
26
43
|
for (const type of values) {
|
|
27
|
-
|
|
28
|
-
property.types.push(type);
|
|
29
|
-
}
|
|
44
|
+
property.types.push(type);
|
|
30
45
|
}
|
|
31
46
|
} else {
|
|
32
47
|
to.keys.array.push(key);
|
|
@@ -35,27 +50,25 @@ function addPropertyType(
|
|
|
35
50
|
to.properties[key] = {
|
|
36
51
|
required,
|
|
37
52
|
types: values,
|
|
53
|
+
validators: {},
|
|
38
54
|
};
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
if (!required && !to.properties[key].types.includes(TYPE_UNDEFINED)) {
|
|
42
58
|
to.properties[key].types.push(TYPE_UNDEFINED);
|
|
43
59
|
}
|
|
60
|
+
|
|
61
|
+
to.properties[key].validators = validators;
|
|
44
62
|
}
|
|
45
63
|
|
|
46
|
-
export function getSchema(schema:
|
|
47
|
-
|
|
48
|
-
enabled: false,
|
|
64
|
+
export function getSchema(schema: Schema): ValidatedSchema {
|
|
65
|
+
return getValidatedSchema(schema, {
|
|
49
66
|
keys: {
|
|
50
67
|
array: [],
|
|
51
68
|
set: new Set<string>(),
|
|
52
69
|
},
|
|
53
70
|
properties: {},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
return typeof schema === 'object' && schema !== null
|
|
57
|
-
? getValidatedSchema(schema as Schema, validated)
|
|
58
|
-
: validated;
|
|
71
|
+
});
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
function getTypes(
|
|
@@ -70,22 +83,23 @@ function getTypes(
|
|
|
70
83
|
|
|
71
84
|
for (let index = 0; index < length; index += 1) {
|
|
72
85
|
const type = values[index];
|
|
73
|
-
const typeOfType = typeof type;
|
|
74
86
|
|
|
75
|
-
if (isSchematic(type) ||
|
|
76
|
-
propertyTypes.push(type
|
|
87
|
+
if (isSchematic(type) || TYPE_ALL.has(type as never)) {
|
|
88
|
+
propertyTypes.push(type);
|
|
77
89
|
|
|
78
90
|
continue;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
if (
|
|
93
|
+
if (typeof type === 'function') {
|
|
82
94
|
propertyTypes.push(isConstructor(type) ? isInstance(type) : type);
|
|
83
95
|
|
|
84
96
|
continue;
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
if (
|
|
88
|
-
|
|
99
|
+
if (!isPlainObject(type)) {
|
|
100
|
+
throw new SchematicError(
|
|
101
|
+
MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace(TEMPLATE_PATTERN, prefix),
|
|
102
|
+
);
|
|
89
103
|
}
|
|
90
104
|
|
|
91
105
|
if (PROPERTY_TYPE in type) {
|
|
@@ -94,11 +108,19 @@ function getTypes(
|
|
|
94
108
|
continue;
|
|
95
109
|
}
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
const {[PROPERTY_REQUIRED]: required, ...nested} = type;
|
|
112
|
+
|
|
113
|
+
if (PROPERTY_REQUIRED in type && typeof required !== 'boolean') {
|
|
114
|
+
throw new SchematicError(
|
|
115
|
+
MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED.replace(TEMPLATE_PATTERN, prefix),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addPropertyType(validated, prefix, [TYPE_OBJECT], {}, required !== false);
|
|
98
120
|
|
|
99
121
|
propertyTypes.push(TYPE_OBJECT);
|
|
100
122
|
|
|
101
|
-
getValidatedSchema(
|
|
123
|
+
getValidatedSchema(nested as Schema, validated, prefix);
|
|
102
124
|
}
|
|
103
125
|
|
|
104
126
|
return propertyTypes;
|
|
@@ -135,23 +157,77 @@ function getValidatedSchema(
|
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
let required = true;
|
|
160
|
+
let validators: ValidatedPropertyValidators = {};
|
|
161
|
+
|
|
162
|
+
const isObject = isPlainObject(value);
|
|
138
163
|
|
|
139
|
-
if (
|
|
140
|
-
|
|
164
|
+
if (isObject && PROPERTY_REQUIRED in value) {
|
|
165
|
+
if (typeof value[PROPERTY_REQUIRED] !== 'boolean') {
|
|
166
|
+
throw new SchematicError(
|
|
167
|
+
MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED.replace(TEMPLATE_PATTERN, key),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
required = value[PROPERTY_REQUIRED] === true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (isObject && PROPERTY_VALIDATORS in value) {
|
|
175
|
+
validators = getValidators(value[PROPERTY_VALIDATORS]);
|
|
141
176
|
}
|
|
142
177
|
|
|
143
178
|
const prefixedKey = `${prefix}${key}`;
|
|
144
179
|
|
|
145
180
|
const types = getTypes(value, validated, prefixedKey);
|
|
146
181
|
|
|
147
|
-
if (types.length
|
|
148
|
-
|
|
182
|
+
if (types.length === 0) {
|
|
183
|
+
throw new SchematicError(MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace(TEMPLATE_PATTERN, key));
|
|
149
184
|
}
|
|
185
|
+
|
|
186
|
+
addPropertyType(validated, prefixedKey, types, validators, required);
|
|
150
187
|
}
|
|
151
188
|
|
|
152
189
|
if (noPrefix) {
|
|
153
190
|
validated.keys.array.sort();
|
|
154
191
|
}
|
|
155
192
|
|
|
193
|
+
if (noPrefix && validated.keys.array.length === 0) {
|
|
194
|
+
throw new SchematicError(MESSAGE_SCHEMA_INVALID_EMPTY);
|
|
195
|
+
}
|
|
196
|
+
|
|
156
197
|
return validated;
|
|
157
198
|
}
|
|
199
|
+
|
|
200
|
+
function getValidators(original: unknown): ValidatedPropertyValidators {
|
|
201
|
+
const validators: ValidatedPropertyValidators = {};
|
|
202
|
+
|
|
203
|
+
if (original == null) {
|
|
204
|
+
return validators;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!isPlainObject(original)) {
|
|
208
|
+
throw new TypeError(MESSAGE_VALIDATOR_INVALID_TYPE);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const keys = Object.keys(original);
|
|
212
|
+
const {length} = keys;
|
|
213
|
+
|
|
214
|
+
for (let index = 0; index < length; index += 1) {
|
|
215
|
+
const key = keys[index];
|
|
216
|
+
|
|
217
|
+
if (!VALIDATABLE_TYPES.has(key as never)) {
|
|
218
|
+
throw new TypeError(MESSAGE_VALIDATOR_INVALID_KEY.replace(TEMPLATE_PATTERN, key));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const value = (original as PlainObject)[key];
|
|
222
|
+
|
|
223
|
+
validators[key as ValueName] = (Array.isArray(value) ? value : [value]).filter(item => {
|
|
224
|
+
if (typeof item !== 'function') {
|
|
225
|
+
throw new TypeError(MESSAGE_VALIDATOR_INVALID_VALUE.replace(TEMPLATE_PATTERN, key));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return true;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return validators;
|
|
233
|
+
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import type {PlainObject} from '@oscarpalmer/atoms/models';
|
|
2
|
-
import {smush} from '@oscarpalmer/atoms/value';
|
|
2
|
+
import {smush} from '@oscarpalmer/atoms/value/misc';
|
|
3
3
|
import {TYPE_UNDEFINED} from '../constants';
|
|
4
|
-
import type {ValidatedPropertyType, ValidatedSchema, Values} from '../models';
|
|
4
|
+
import type {ValidatedProperty, ValidatedPropertyType, ValidatedSchema, Values} from '../models';
|
|
5
5
|
|
|
6
|
-
export function validateType(
|
|
6
|
+
export function validateType(
|
|
7
|
+
type: ValidatedPropertyType,
|
|
8
|
+
property: ValidatedProperty,
|
|
9
|
+
value: unknown,
|
|
10
|
+
): boolean {
|
|
7
11
|
switch (true) {
|
|
8
12
|
case typeof type === 'function':
|
|
9
13
|
return (type as any)(value);
|
|
10
14
|
|
|
11
15
|
case typeof type === 'string':
|
|
12
|
-
return
|
|
16
|
+
return (
|
|
17
|
+
validators[type](value) &&
|
|
18
|
+
(property.validators[type]?.every(validator => validator(value)) ?? true)
|
|
19
|
+
);
|
|
13
20
|
|
|
14
21
|
default:
|
|
15
22
|
return type.is(value);
|
|
@@ -47,7 +54,7 @@ export function validateValue(validated: ValidatedSchema, obj: unknown): boolean
|
|
|
47
54
|
const typesLength = property.types.length;
|
|
48
55
|
|
|
49
56
|
if (typesLength === 1) {
|
|
50
|
-
if (!validateType(property.types[0], value)) {
|
|
57
|
+
if (!validateType(property.types[0], property, value)) {
|
|
51
58
|
return false;
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -57,7 +64,7 @@ export function validateValue(validated: ValidatedSchema, obj: unknown): boolean
|
|
|
57
64
|
for (let typeIndex = 0; typeIndex < typesLength; typeIndex += 1) {
|
|
58
65
|
const type = property.types[typeIndex];
|
|
59
66
|
|
|
60
|
-
if (validateType(type, value)) {
|
|
67
|
+
if (validateType(type, property, value)) {
|
|
61
68
|
if ((type as never) !== 'object') {
|
|
62
69
|
ignore.add(key);
|
|
63
70
|
}
|
package/types/constants.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
export declare const ERROR_NAME = "SchematicError";
|
|
2
2
|
export declare const EXPRESSION_HAS_NUMBER: RegExp;
|
|
3
3
|
export declare const EXPRESSION_INDEX: RegExp;
|
|
4
4
|
export declare const EXPRESSION_PROPERTY: RegExp;
|
|
5
|
+
export declare const MESSAGE_CONSTRUCTOR = "Expected a constructor function";
|
|
6
|
+
export declare const MESSAGE_SCHEMA_INVALID_EMPTY = "Schema must have at least one property";
|
|
7
|
+
export declare const MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED = "'<>.$required' property must be a boolean";
|
|
8
|
+
export declare const MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE = "'<>' property must be of a valid type";
|
|
9
|
+
export declare const MESSAGE_SCHEMA_INVALID_TYPE = "Schema must be an object";
|
|
10
|
+
export declare const MESSAGE_VALIDATOR_INVALID_KEY = "Validator '<>' does not exist";
|
|
11
|
+
export declare const MESSAGE_VALIDATOR_INVALID_TYPE = "Validators must be an object";
|
|
12
|
+
export declare const MESSAGE_VALIDATOR_INVALID_VALUE = "Validator '<>' must be a function or an array of functions";
|
|
5
13
|
export declare const PROPERTY_REQUIRED = "$required";
|
|
6
14
|
export declare const PROPERTY_TYPE = "$type";
|
|
15
|
+
export declare const PROPERTY_VALIDATORS = "$validators";
|
|
7
16
|
export declare const SCHEMATIC_NAME = "$schematic";
|
|
17
|
+
export declare const TEMPLATE_PATTERN = "<>";
|
|
8
18
|
export declare const TYPE_OBJECT = "object";
|
|
9
19
|
export declare const TYPE_UNDEFINED = "undefined";
|
|
10
|
-
export declare const
|
|
20
|
+
export declare const VALIDATABLE_TYPES: Set<keyof import("./models").Values>;
|
|
21
|
+
export declare const TYPE_ALL: Set<keyof import("./models").Values>;
|
package/types/index.d.ts
CHANGED
package/types/models.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ type DeduplicateTuple<Value extends unknown[], Seen extends unknown[] = []> = Va
|
|
|
5
5
|
infer Head,
|
|
6
6
|
...infer Tail
|
|
7
7
|
] ? Head extends Seen[number] ? DeduplicateTuple<Tail, Seen> : DeduplicateTuple<Tail, [...Seen, Head]> : Seen;
|
|
8
|
+
type ExtractValueNames<Value> = Value extends ValueName ? Value : Value extends (infer Item)[] ? ExtractValueNames<Item> : Value extends readonly (infer Item)[] ? ExtractValueNames<Item> : never;
|
|
8
9
|
/**
|
|
9
10
|
* Infer the TypeScript type from a schema definition
|
|
10
11
|
*/
|
|
@@ -22,8 +23,12 @@ type InferRequiredKeys<Model extends Schema> = keyof {
|
|
|
22
23
|
[Key in keyof Model as IsOptionalProperty<Model[Key]> extends true ? never : Key]: never;
|
|
23
24
|
};
|
|
24
25
|
type InferSchemaEntry<Value> = Value extends (infer Item)[] ? InferSchemaEntryValue<Item> : InferSchemaEntryValue<Value>;
|
|
25
|
-
type InferSchemaEntryValue<Value> = Value extends Constructor<infer Instance> ? Instance : Value extends Schematic<infer Model> ? Model : Value extends SchemaProperty ? InferPropertyType<Value['$type']> : Value extends ValueName ? Values[Value & ValueName] : Value extends Schema ? Infer<Value> : never;
|
|
26
|
-
type IsOptionalProperty<Value> = Value extends SchemaProperty ? Value['$required'] extends false ? true : false :
|
|
26
|
+
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;
|
|
27
|
+
type IsOptionalProperty<Value> = Value extends SchemaProperty ? Value['$required'] extends false ? true : false : Value extends {
|
|
28
|
+
$required?: boolean;
|
|
29
|
+
} ? Value extends {
|
|
30
|
+
$required: false;
|
|
31
|
+
} ? true : false : false;
|
|
27
32
|
type LastOfUnion<Value> = UnionToIntersection<Value extends unknown ? () => Value : never> extends () => infer Item ? Item : never;
|
|
28
33
|
type MapToValueTypes<Value extends unknown[]> = Value extends [infer Head, ...infer Tail] ? [ToValueType<Head>, ...MapToValueTypes<Tail>] : [];
|
|
29
34
|
type MapToSchemaPropertyTypes<Value extends unknown[]> = Value extends [infer Head, ...infer Tail] ? [ToSchemaPropertyTypeEach<Head>, ...MapToSchemaPropertyTypes<Tail>] : [];
|
|
@@ -32,16 +37,20 @@ type MapToSchemaPropertyTypes<Value extends unknown[]> = Value extends [infer He
|
|
|
32
37
|
*/
|
|
33
38
|
export type NestedSchema = {
|
|
34
39
|
$required?: boolean;
|
|
35
|
-
|
|
40
|
+
[key: string]: any;
|
|
41
|
+
};
|
|
36
42
|
type OptionalKeys<Value> = {
|
|
37
43
|
[Key in keyof Value]-?: {} extends Pick<Value, Key> ? Key : never;
|
|
38
44
|
}[keyof Value];
|
|
45
|
+
type PropertyValidators<Value> = {
|
|
46
|
+
[Key in ExtractValueNames<Value>]?: ((value: Values[Key]) => boolean) | Array<(value: Values[Key]) => boolean>;
|
|
47
|
+
};
|
|
39
48
|
type RequiredKeys<Value> = Exclude<keyof Value, OptionalKeys<Value>>;
|
|
40
49
|
/**
|
|
41
50
|
* A schema for validating objects
|
|
42
51
|
*/
|
|
43
52
|
export type Schema = SchemaIndex;
|
|
44
|
-
type SchemaEntry = Constructor |
|
|
53
|
+
type SchemaEntry = Constructor | SchemaProperty | Schematic<unknown> | ValueName | NestedSchema;
|
|
45
54
|
interface SchemaIndex {
|
|
46
55
|
[key: string]: SchemaEntry | SchemaEntry[];
|
|
47
56
|
}
|
|
@@ -51,12 +60,16 @@ interface SchemaIndex {
|
|
|
51
60
|
export type SchemaProperty = {
|
|
52
61
|
$required?: boolean;
|
|
53
62
|
$type: SchemaPropertyType | SchemaPropertyType[];
|
|
63
|
+
$validators?: PropertyValidators<SchemaPropertyType | SchemaPropertyType[]>;
|
|
54
64
|
};
|
|
55
65
|
type SchemaPropertyType = Constructor | Schema | Schematic<unknown> | ValueName;
|
|
66
|
+
export declare class SchematicError extends Error {
|
|
67
|
+
constructor(message: string);
|
|
68
|
+
}
|
|
56
69
|
type ToSchemaPropertyType<Value> = UnwrapSingle<DeduplicateTuple<MapToSchemaPropertyTypes<UnionToTuple<Value>>>>;
|
|
57
|
-
type ToSchemaPropertyTypeEach<Value> = Value extends PlainObject ? TypedSchema<Value> : ToValueType<Value>;
|
|
70
|
+
type ToSchemaPropertyTypeEach<Value> = Value extends NestedSchema ? Omit<Value, '$required'> : Value extends PlainObject ? TypedSchema<Value> : ToValueType<Value>;
|
|
58
71
|
type ToSchemaType<Value> = UnwrapSingle<DeduplicateTuple<MapToValueTypes<UnionToTuple<Value>>>>;
|
|
59
|
-
type ToValueType<Value> = Value extends unknown[] ? 'array' : Value extends bigint ? 'bigint' : Value extends boolean ? 'boolean' : Value extends Date ? 'date' : Value extends Function ? 'function' : Value extends null ? 'null' : Value extends number ? 'number' : Value extends object ? 'object' | ((value: unknown) => value is Value) : Value extends string ? 'string' : Value extends symbol ? 'symbol' : Value extends undefined ? 'undefined' : (value: unknown) => value is Value;
|
|
72
|
+
type ToValueType<Value> = Value extends unknown[] ? 'array' : Value extends bigint ? 'bigint' : Value extends boolean ? 'boolean' : Value extends Date ? 'date' : Value extends Schematic<any> ? Value : Value extends Function ? 'function' : Value extends null ? 'null' : Value extends number ? 'number' : Value extends object ? 'object' | ((value: unknown) => value is Value) : Value extends string ? 'string' : Value extends symbol ? 'symbol' : Value extends undefined ? 'undefined' : (value: unknown) => value is Value;
|
|
60
73
|
type TuplePermutations<Tuple extends unknown[], Elput extends unknown[] = []> = Tuple['length'] extends 0 ? Elput : {
|
|
61
74
|
[Key in keyof Tuple]: TuplePermutations<TupleRemoveAt<Tuple, Key & `${number}`>, [
|
|
62
75
|
...Elput,
|
|
@@ -65,20 +78,40 @@ type TuplePermutations<Tuple extends unknown[], Elput extends unknown[] = []> =
|
|
|
65
78
|
}[keyof Tuple & `${number}`];
|
|
66
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;
|
|
67
80
|
export type TypedPropertyOptional<Value> = {
|
|
81
|
+
/**
|
|
82
|
+
* The property is not required
|
|
83
|
+
*/
|
|
68
84
|
$required: false;
|
|
85
|
+
/**
|
|
86
|
+
* The type(s) of the property
|
|
87
|
+
*/
|
|
69
88
|
$type: ToSchemaPropertyType<Exclude<Value, undefined>>;
|
|
89
|
+
/**
|
|
90
|
+
* Custom validators for the property and its types
|
|
91
|
+
*/
|
|
92
|
+
$validators?: PropertyValidators<ToSchemaPropertyType<Exclude<Value, undefined>>>;
|
|
70
93
|
};
|
|
71
94
|
export type TypedPropertyRequired<Value> = {
|
|
95
|
+
/**
|
|
96
|
+
* The property is required _(defaults to `true`)_
|
|
97
|
+
*/
|
|
72
98
|
$required?: true;
|
|
99
|
+
/**
|
|
100
|
+
* The type(s) of the property
|
|
101
|
+
*/
|
|
73
102
|
$type: ToSchemaPropertyType<Value>;
|
|
103
|
+
/**
|
|
104
|
+
* Custom validators for the property and its types
|
|
105
|
+
*/
|
|
106
|
+
$validators?: PropertyValidators<ToSchemaPropertyType<Value>>;
|
|
74
107
|
};
|
|
75
108
|
/**
|
|
76
109
|
* Create a schema type constrained to match a TypeScript type
|
|
77
110
|
*/
|
|
78
111
|
export type TypedSchema<Model extends PlainObject> = Simplify<{
|
|
79
|
-
[Key in RequiredKeys<Model>]: Model[Key] extends PlainObject ? TypedSchemaRequired<Model[Key]> : ToSchemaType<Model[Key]> | TypedPropertyRequired<Model[Key]>;
|
|
112
|
+
[Key in RequiredKeys<Model>]: Model[Key] extends PlainObject ? TypedSchemaRequired<Model[Key]> | Schematic<Model[Key]> : ToSchemaType<Model[Key]> | TypedPropertyRequired<Model[Key]>;
|
|
80
113
|
} & {
|
|
81
|
-
[Key in OptionalKeys<Model>]: Exclude<Model[Key], undefined> extends PlainObject ? TypedSchemaOptional<Exclude<Model[Key], undefined>> : TypedPropertyOptional<Model[Key]>;
|
|
114
|
+
[Key in OptionalKeys<Model>]: Exclude<Model[Key], undefined> extends PlainObject ? TypedSchemaOptional<Exclude<Model[Key], undefined>> | Schematic<Exclude<Model[Key], undefined>> : TypedPropertyOptional<Model[Key]>;
|
|
82
115
|
}>;
|
|
83
116
|
type TypedSchemaOptional<Model extends PlainObject> = {
|
|
84
117
|
$required: false;
|
|
@@ -92,10 +125,13 @@ type UnwrapSingle<Value extends unknown[]> = Value extends [infer Only] ? Only :
|
|
|
92
125
|
export type ValidatedProperty = {
|
|
93
126
|
required: boolean;
|
|
94
127
|
types: ValidatedPropertyType[];
|
|
128
|
+
validators: ValidatedPropertyValidators;
|
|
95
129
|
};
|
|
96
130
|
export type ValidatedPropertyType = Schematic<unknown> | ValueName;
|
|
131
|
+
export type ValidatedPropertyValidators = {
|
|
132
|
+
[Key in ValueName]?: Array<(value: unknown) => boolean>;
|
|
133
|
+
};
|
|
97
134
|
export type ValidatedSchema = {
|
|
98
|
-
enabled: boolean;
|
|
99
135
|
keys: {
|
|
100
136
|
array: string[];
|
|
101
137
|
set: Set<string>;
|
package/types/schematic.d.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { PlainObject } from '@oscarpalmer/atoms/models';
|
|
2
|
-
import type
|
|
2
|
+
import { type Infer, type Schema, type TypedSchema, type ValidatedSchema } from './models';
|
|
3
3
|
/**
|
|
4
4
|
* A schematic for validating objects
|
|
5
5
|
*/
|
|
6
6
|
export declare class Schematic<Model> {
|
|
7
7
|
#private;
|
|
8
8
|
private readonly $schematic;
|
|
9
|
-
|
|
10
|
-
constructor(schema: Model);
|
|
9
|
+
constructor(schema: ValidatedSchema);
|
|
11
10
|
/**
|
|
12
11
|
* Does the value match the schema?
|
|
13
12
|
*/
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type
|
|
2
|
-
export declare function getSchema(schema:
|
|
1
|
+
import { type Schema, type ValidatedSchema } from '../models';
|
|
2
|
+
export declare function getSchema(schema: Schema): ValidatedSchema;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ValidatedPropertyType, ValidatedSchema } from '../models';
|
|
2
|
-
export declare function validateType(type: ValidatedPropertyType, value: unknown): boolean;
|
|
1
|
+
import type { ValidatedProperty, ValidatedPropertyType, ValidatedSchema } from '../models';
|
|
2
|
+
export declare function validateType(type: ValidatedPropertyType, property: ValidatedProperty, value: unknown): boolean;
|
|
3
3
|
export declare function validateValue(validated: ValidatedSchema, obj: unknown): boolean;
|