@oscarpalmer/jhunal 0.9.0 → 0.10.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 +3 -2
- package/dist/jhunal.full.js +25 -10
- package/dist/node_modules/@oscarpalmer/atoms/dist/internal/array/compact.js +12 -0
- package/dist/node_modules/@oscarpalmer/atoms/dist/internal/is.js +20 -0
- package/dist/node_modules/@oscarpalmer/atoms/dist/internal/string.js +24 -0
- package/dist/node_modules/@oscarpalmer/atoms/dist/value/smush.js +36 -0
- package/dist/validation/schema.validation.js +21 -7
- package/dist/validation/value.validation.js +5 -5
- package/package.json +4 -4
- package/src/constants.ts +3 -1
- package/src/models.ts +40 -0
- package/src/validation/schema.validation.ts +40 -5
- package/src/validation/value.validation.ts +13 -6
- package/types/constants.d.ts +1 -0
- package/types/models.d.ts +29 -0
- package/types/validation/value.validation.d.ts +2 -2
package/dist/constants.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const EXPRESSION_HAS_NUMBER = /\d+/;
|
|
2
2
|
const EXPRESSION_INDEX = /\.\d+$/;
|
|
3
|
-
const EXPRESSION_PROPERTY = /\.\$(required|type)(\.|$)/;
|
|
3
|
+
const EXPRESSION_PROPERTY = /\.\$(required|type|validators)(\.|$)/;
|
|
4
4
|
const PROPERTY_REQUIRED = "$required";
|
|
5
5
|
const PROPERTY_TYPE = "$type";
|
|
6
|
+
const PROPERTY_VALIDATORS = "$validators";
|
|
6
7
|
const SCHEMATIC_NAME = "$schematic";
|
|
7
8
|
const TYPE_OBJECT = "object";
|
|
8
9
|
const TYPE_UNDEFINED = "undefined";
|
|
@@ -19,4 +20,4 @@ const TYPE_ALL = new Set([
|
|
|
19
20
|
TYPE_OBJECT,
|
|
20
21
|
TYPE_UNDEFINED
|
|
21
22
|
]);
|
|
22
|
-
export { EXPRESSION_HAS_NUMBER, EXPRESSION_INDEX, EXPRESSION_PROPERTY, PROPERTY_REQUIRED, PROPERTY_TYPE, SCHEMATIC_NAME, TYPE_ALL, TYPE_OBJECT, TYPE_UNDEFINED };
|
|
23
|
+
export { EXPRESSION_HAS_NUMBER, EXPRESSION_INDEX, EXPRESSION_PROPERTY, PROPERTY_REQUIRED, PROPERTY_TYPE, PROPERTY_VALIDATORS, SCHEMATIC_NAME, TYPE_ALL, TYPE_OBJECT, TYPE_UNDEFINED };
|
package/dist/jhunal.full.js
CHANGED
|
@@ -60,9 +60,10 @@ function join(value, delimiter) {
|
|
|
60
60
|
}
|
|
61
61
|
const EXPRESSION_HAS_NUMBER = /\d+/;
|
|
62
62
|
const EXPRESSION_INDEX = /\.\d+$/;
|
|
63
|
-
const EXPRESSION_PROPERTY = /\.\$(required|type)(\.|$)/;
|
|
63
|
+
const EXPRESSION_PROPERTY = /\.\$(required|type|validators)(\.|$)/;
|
|
64
64
|
const PROPERTY_REQUIRED = "$required";
|
|
65
65
|
const PROPERTY_TYPE = "$type";
|
|
66
|
+
const PROPERTY_VALIDATORS = "$validators";
|
|
66
67
|
const SCHEMATIC_NAME = "$schematic";
|
|
67
68
|
const TYPE_OBJECT = "object";
|
|
68
69
|
const TYPE_UNDEFINED = "undefined";
|
|
@@ -121,7 +122,7 @@ function smush(value) {
|
|
|
121
122
|
return typeof value === "object" && value !== null ? flattenObject(value, 0, /* @__PURE__ */ new WeakMap()) : {};
|
|
122
123
|
}
|
|
123
124
|
var MAX_DEPTH = 100;
|
|
124
|
-
function addPropertyType(to, key, values, required) {
|
|
125
|
+
function addPropertyType(to, key, values, validators, required) {
|
|
125
126
|
if (to.keys.set.has(key)) {
|
|
126
127
|
const property = to.properties[key];
|
|
127
128
|
for (const type of values) if (!property.types.includes(type)) property.types.push(type);
|
|
@@ -130,10 +131,12 @@ function addPropertyType(to, key, values, required) {
|
|
|
130
131
|
to.keys.set.add(key);
|
|
131
132
|
to.properties[key] = {
|
|
132
133
|
required,
|
|
133
|
-
types: values
|
|
134
|
+
types: values,
|
|
135
|
+
validators: {}
|
|
134
136
|
};
|
|
135
137
|
}
|
|
136
138
|
if (!required && !to.properties[key].types.includes(TYPE_UNDEFINED)) to.properties[key].types.push(TYPE_UNDEFINED);
|
|
139
|
+
to.properties[key].validators = validators;
|
|
137
140
|
}
|
|
138
141
|
function getSchema(schema) {
|
|
139
142
|
const validated = {
|
|
@@ -166,7 +169,7 @@ function getTypes(value, validated, prefix) {
|
|
|
166
169
|
propertyTypes.push(...getTypes(type[PROPERTY_TYPE], validated, prefix));
|
|
167
170
|
continue;
|
|
168
171
|
}
|
|
169
|
-
addPropertyType(validated, prefix, [TYPE_OBJECT], type[PROPERTY_REQUIRED] !== false);
|
|
172
|
+
addPropertyType(validated, prefix, [TYPE_OBJECT], {}, type[PROPERTY_REQUIRED] !== false);
|
|
170
173
|
propertyTypes.push(TYPE_OBJECT);
|
|
171
174
|
getValidatedSchema(type, validated, prefix);
|
|
172
175
|
}
|
|
@@ -186,18 +189,30 @@ function getValidatedSchema(schema, validated, prefix) {
|
|
|
186
189
|
if (EXPRESSION_PROPERTY.test(key)) continue;
|
|
187
190
|
if (EXPRESSION_HAS_NUMBER.test(key) && arrayKeys.has(key.replace(EXPRESSION_INDEX, ""))) continue;
|
|
188
191
|
let required = true;
|
|
189
|
-
|
|
192
|
+
let validators = {};
|
|
193
|
+
const isObject = typeof value === "object" && value !== null;
|
|
194
|
+
if (isObject && PROPERTY_REQUIRED in value) required = typeof value[PROPERTY_REQUIRED] === "boolean" ? value[PROPERTY_REQUIRED] : true;
|
|
195
|
+
if (isObject && PROPERTY_VALIDATORS in value) validators = getValidators(value[PROPERTY_VALIDATORS]);
|
|
190
196
|
const prefixedKey = `${prefix}${key}`;
|
|
191
197
|
const types = getTypes(value, validated, prefixedKey);
|
|
192
|
-
if (types.length > 0) addPropertyType(validated, prefixedKey, types, required);
|
|
198
|
+
if (types.length > 0) addPropertyType(validated, prefixedKey, types, validators, required);
|
|
193
199
|
}
|
|
194
200
|
if (noPrefix) validated.keys.array.sort();
|
|
195
201
|
return validated;
|
|
196
202
|
}
|
|
197
|
-
function
|
|
203
|
+
function getValidators(original) {
|
|
204
|
+
const validators = {};
|
|
205
|
+
if (typeof original !== "object" || original === null) return validators;
|
|
206
|
+
for (const type of TYPE_ALL) {
|
|
207
|
+
const value = original[type];
|
|
208
|
+
validators[type] = (Array.isArray(value) ? value : [value]).filter((validator) => typeof validator === "function");
|
|
209
|
+
}
|
|
210
|
+
return validators;
|
|
211
|
+
}
|
|
212
|
+
function validateType(type, property, value) {
|
|
198
213
|
switch (true) {
|
|
199
214
|
case typeof type === "function": return type(value);
|
|
200
|
-
case typeof type === "string": return validators[type](value);
|
|
215
|
+
case typeof type === "string": return validators[type](value) && (property.validators[type]?.every((validator) => validator(value)) ?? true);
|
|
201
216
|
default: return type.is(value);
|
|
202
217
|
}
|
|
203
218
|
}
|
|
@@ -216,12 +231,12 @@ function validateValue(validated, obj) {
|
|
|
216
231
|
if (value === void 0 && property.required && !property.types.includes(TYPE_UNDEFINED)) return false;
|
|
217
232
|
const typesLength = property.types.length;
|
|
218
233
|
if (typesLength === 1) {
|
|
219
|
-
if (!validateType(property.types[0], value)) return false;
|
|
234
|
+
if (!validateType(property.types[0], property, value)) return false;
|
|
220
235
|
continue;
|
|
221
236
|
}
|
|
222
237
|
for (let typeIndex = 0; typeIndex < typesLength; typeIndex += 1) {
|
|
223
238
|
const type = property.types[typeIndex];
|
|
224
|
-
if (validateType(type, value)) {
|
|
239
|
+
if (validateType(type, property, value)) {
|
|
225
240
|
if (type !== "object") ignore.add(key);
|
|
226
241
|
continue outer;
|
|
227
242
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function compact(array, strict) {
|
|
2
|
+
if (!Array.isArray(array)) return [];
|
|
3
|
+
if (strict === true) return array.filter(Boolean);
|
|
4
|
+
const { length } = array;
|
|
5
|
+
const compacted = [];
|
|
6
|
+
for (let index = 0; index < length; index += 1) {
|
|
7
|
+
const item = array[index];
|
|
8
|
+
if (item != null) compacted.push(item);
|
|
9
|
+
}
|
|
10
|
+
return compacted;
|
|
11
|
+
}
|
|
12
|
+
export { compact };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the value an array or a record?
|
|
3
|
+
* @param value Value to check
|
|
4
|
+
* @returns `true` if the value is an array or a record, otherwise `false`
|
|
5
|
+
*/
|
|
6
|
+
function isArrayOrPlainObject(value) {
|
|
7
|
+
return Array.isArray(value) || isPlainObject(value);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Is the value a plain object?
|
|
11
|
+
* @param value Value to check
|
|
12
|
+
* @returns `true` if the value is a plain object, otherwise `false`
|
|
13
|
+
*/
|
|
14
|
+
function isPlainObject(value) {
|
|
15
|
+
if (value === null || typeof value !== "object") return false;
|
|
16
|
+
if (Symbol.toStringTag in value || Symbol.iterator in value) return false;
|
|
17
|
+
const prototype = Object.getPrototypeOf(value);
|
|
18
|
+
return prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null;
|
|
19
|
+
}
|
|
20
|
+
export { isArrayOrPlainObject };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { compact } from "./array/compact.js";
|
|
2
|
+
/**
|
|
3
|
+
* Get the string value from any value
|
|
4
|
+
* @param value Original value
|
|
5
|
+
* @returns String representation of the value
|
|
6
|
+
*/
|
|
7
|
+
function getString(value) {
|
|
8
|
+
if (typeof value === "string") return value;
|
|
9
|
+
if (value == null) return "";
|
|
10
|
+
if (typeof value === "function") return getString(value());
|
|
11
|
+
if (typeof value !== "object") return String(value);
|
|
12
|
+
const asString = String(value.valueOf?.() ?? value);
|
|
13
|
+
return asString.startsWith("[object ") ? JSON.stringify(value) : asString;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Join an array of values into a string
|
|
17
|
+
* @param value Array of values
|
|
18
|
+
* @param delimiter Delimiter to use between values
|
|
19
|
+
* @returns Joined string
|
|
20
|
+
*/
|
|
21
|
+
function join(value, delimiter) {
|
|
22
|
+
return compact(value).map(getString).join(typeof delimiter === "string" ? delimiter : "");
|
|
23
|
+
}
|
|
24
|
+
export { join };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isArrayOrPlainObject } from "../internal/is.js";
|
|
2
|
+
import { join } from "../internal/string.js";
|
|
3
|
+
function flattenObject(value, depth, smushed, prefix) {
|
|
4
|
+
if (depth >= MAX_DEPTH) return {};
|
|
5
|
+
if (smushed.has(value)) return smushed.get(value);
|
|
6
|
+
const keys = Object.keys(value);
|
|
7
|
+
const { length } = keys;
|
|
8
|
+
const flattened = {};
|
|
9
|
+
for (let index = 0; index < length; index += 1) {
|
|
10
|
+
const key = keys[index];
|
|
11
|
+
const val = value[key];
|
|
12
|
+
if (isArrayOrPlainObject(val)) {
|
|
13
|
+
const prefixedKey = join([prefix, key], ".");
|
|
14
|
+
flattened[prefixedKey] = Array.isArray(val) ? [...val] : { ...val };
|
|
15
|
+
const nested = flattenObject(val, depth + 1, smushed, prefixedKey);
|
|
16
|
+
const nestedKeys = Object.keys(nested);
|
|
17
|
+
const nestedLength = nestedKeys.length;
|
|
18
|
+
for (let nestedIndex = 0; nestedIndex < nestedLength; nestedIndex += 1) {
|
|
19
|
+
const nestedKey = nestedKeys[nestedIndex];
|
|
20
|
+
flattened[nestedKey] = nested[nestedKey];
|
|
21
|
+
}
|
|
22
|
+
} else flattened[join([prefix, key], ".")] = val;
|
|
23
|
+
}
|
|
24
|
+
smushed.set(value, flattened);
|
|
25
|
+
return flattened;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Smush an object into a flat object that uses dot notation keys
|
|
29
|
+
* @param value Object to smush
|
|
30
|
+
* @returns Smushed object with dot notation keys
|
|
31
|
+
*/
|
|
32
|
+
function smush(value) {
|
|
33
|
+
return typeof value === "object" && value !== null ? flattenObject(value, 0, /* @__PURE__ */ new WeakMap()) : {};
|
|
34
|
+
}
|
|
35
|
+
var MAX_DEPTH = 100;
|
|
36
|
+
export { smush };
|
|
@@ -1,8 +1,8 @@
|
|
|
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, PROPERTY_REQUIRED, PROPERTY_TYPE, PROPERTY_VALIDATORS, TYPE_ALL, TYPE_OBJECT, TYPE_UNDEFINED } from "../constants.js";
|
|
2
2
|
import { isInstance, isSchematic } from "../is.js";
|
|
3
|
+
import { smush } from "../node_modules/@oscarpalmer/atoms/dist/value/smush.js";
|
|
3
4
|
import { isConstructor } from "@oscarpalmer/atoms/is";
|
|
4
|
-
|
|
5
|
-
function addPropertyType(to, key, values, required) {
|
|
5
|
+
function addPropertyType(to, key, values, validators, required) {
|
|
6
6
|
if (to.keys.set.has(key)) {
|
|
7
7
|
const property = to.properties[key];
|
|
8
8
|
for (const type of values) if (!property.types.includes(type)) property.types.push(type);
|
|
@@ -11,10 +11,12 @@ function addPropertyType(to, key, values, required) {
|
|
|
11
11
|
to.keys.set.add(key);
|
|
12
12
|
to.properties[key] = {
|
|
13
13
|
required,
|
|
14
|
-
types: values
|
|
14
|
+
types: values,
|
|
15
|
+
validators: {}
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
if (!required && !to.properties[key].types.includes("undefined")) to.properties[key].types.push(TYPE_UNDEFINED);
|
|
19
|
+
to.properties[key].validators = validators;
|
|
18
20
|
}
|
|
19
21
|
function getSchema(schema) {
|
|
20
22
|
const validated = {
|
|
@@ -47,7 +49,7 @@ function getTypes(value, validated, prefix) {
|
|
|
47
49
|
propertyTypes.push(...getTypes(type[PROPERTY_TYPE], validated, prefix));
|
|
48
50
|
continue;
|
|
49
51
|
}
|
|
50
|
-
addPropertyType(validated, prefix, [TYPE_OBJECT], type[PROPERTY_REQUIRED] !== false);
|
|
52
|
+
addPropertyType(validated, prefix, [TYPE_OBJECT], {}, type[PROPERTY_REQUIRED] !== false);
|
|
51
53
|
propertyTypes.push(TYPE_OBJECT);
|
|
52
54
|
getValidatedSchema(type, validated, prefix);
|
|
53
55
|
}
|
|
@@ -67,12 +69,24 @@ 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 = typeof value === "object" && value !== null;
|
|
74
|
+
if (isObject && "$required" in value) required = typeof value["$required"] === "boolean" ? value[PROPERTY_REQUIRED] : true;
|
|
75
|
+
if (isObject && "$validators" in value) validators = getValidators(value[PROPERTY_VALIDATORS]);
|
|
71
76
|
const prefixedKey = `${prefix}${key}`;
|
|
72
77
|
const types = getTypes(value, validated, prefixedKey);
|
|
73
|
-
if (types.length > 0) addPropertyType(validated, prefixedKey, types, required);
|
|
78
|
+
if (types.length > 0) addPropertyType(validated, prefixedKey, types, validators, required);
|
|
74
79
|
}
|
|
75
80
|
if (noPrefix) validated.keys.array.sort();
|
|
76
81
|
return validated;
|
|
77
82
|
}
|
|
83
|
+
function getValidators(original) {
|
|
84
|
+
const validators = {};
|
|
85
|
+
if (typeof original !== "object" || original === null) return validators;
|
|
86
|
+
for (const type of TYPE_ALL) {
|
|
87
|
+
const value = original[type];
|
|
88
|
+
validators[type] = (Array.isArray(value) ? value : [value]).filter((validator) => typeof validator === "function");
|
|
89
|
+
}
|
|
90
|
+
return validators;
|
|
91
|
+
}
|
|
78
92
|
export { getSchema };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import "../constants.js";
|
|
2
|
-
import { smush } from "
|
|
3
|
-
function validateType(type, value) {
|
|
2
|
+
import { smush } from "../node_modules/@oscarpalmer/atoms/dist/value/smush.js";
|
|
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.10.0"
|
|
49
49
|
}
|
package/src/constants.ts
CHANGED
|
@@ -4,12 +4,14 @@ export const EXPRESSION_HAS_NUMBER = /\d+/;
|
|
|
4
4
|
|
|
5
5
|
export const EXPRESSION_INDEX = /\.\d+$/;
|
|
6
6
|
|
|
7
|
-
export const EXPRESSION_PROPERTY = /\.\$(required|type)(\.|$)/;
|
|
7
|
+
export const EXPRESSION_PROPERTY = /\.\$(required|type|validators)(\.|$)/;
|
|
8
8
|
|
|
9
9
|
export const PROPERTY_REQUIRED = '$required';
|
|
10
10
|
|
|
11
11
|
export const PROPERTY_TYPE = '$type';
|
|
12
12
|
|
|
13
|
+
export const PROPERTY_VALIDATORS = '$validators';
|
|
14
|
+
|
|
13
15
|
export const SCHEMATIC_NAME = '$schematic';
|
|
14
16
|
|
|
15
17
|
export const TYPE_OBJECT = 'object';
|
package/src/models.ts
CHANGED
|
@@ -12,6 +12,14 @@ type DeduplicateTuple<Value extends unknown[], Seen extends unknown[] = []> = Va
|
|
|
12
12
|
: DeduplicateTuple<Tail, [...Seen, Head]>
|
|
13
13
|
: Seen;
|
|
14
14
|
|
|
15
|
+
type ExtractValueNames<Value> = Value extends ValueName
|
|
16
|
+
? Value
|
|
17
|
+
: Value extends (infer Item)[]
|
|
18
|
+
? ExtractValueNames<Item>
|
|
19
|
+
: Value extends readonly (infer Item)[]
|
|
20
|
+
? ExtractValueNames<Item>
|
|
21
|
+
: never;
|
|
22
|
+
|
|
15
23
|
/**
|
|
16
24
|
* Infer the TypeScript type from a schema definition
|
|
17
25
|
*/
|
|
@@ -93,6 +101,12 @@ type OptionalKeys<Value> = {
|
|
|
93
101
|
[Key in keyof Value]-?: {} extends Pick<Value, Key> ? Key : never;
|
|
94
102
|
}[keyof Value];
|
|
95
103
|
|
|
104
|
+
type PropertyValidators<Value> = {
|
|
105
|
+
[Key in ExtractValueNames<Value>]?:
|
|
106
|
+
| ((value: Values[Key]) => boolean)
|
|
107
|
+
| Array<(value: Values[Key]) => boolean>;
|
|
108
|
+
};
|
|
109
|
+
|
|
96
110
|
type RequiredKeys<Value> = Exclude<keyof Value, OptionalKeys<Value>>;
|
|
97
111
|
|
|
98
112
|
/**
|
|
@@ -112,6 +126,7 @@ interface SchemaIndex {
|
|
|
112
126
|
export type SchemaProperty = {
|
|
113
127
|
$required?: boolean;
|
|
114
128
|
$type: SchemaPropertyType | SchemaPropertyType[];
|
|
129
|
+
$validators?: PropertyValidators<SchemaPropertyType | SchemaPropertyType[]>;
|
|
115
130
|
};
|
|
116
131
|
|
|
117
132
|
type SchemaPropertyType = Constructor | Schema | Schematic<unknown> | ValueName;
|
|
@@ -173,13 +188,33 @@ type TupleRemoveAt<
|
|
|
173
188
|
: Prefix;
|
|
174
189
|
|
|
175
190
|
export type TypedPropertyOptional<Value> = {
|
|
191
|
+
/**
|
|
192
|
+
* The property is not required
|
|
193
|
+
*/
|
|
176
194
|
$required: false;
|
|
195
|
+
/**
|
|
196
|
+
* The type(s) of the property
|
|
197
|
+
*/
|
|
177
198
|
$type: ToSchemaPropertyType<Exclude<Value, undefined>>;
|
|
199
|
+
/**
|
|
200
|
+
* Custom validators for the property and its types
|
|
201
|
+
*/
|
|
202
|
+
$validators?: PropertyValidators<ToSchemaPropertyType<Exclude<Value, undefined>>>;
|
|
178
203
|
};
|
|
179
204
|
|
|
180
205
|
export type TypedPropertyRequired<Value> = {
|
|
206
|
+
/**
|
|
207
|
+
* The property is required _(defaults to `true`)_
|
|
208
|
+
*/
|
|
181
209
|
$required?: true;
|
|
210
|
+
/**
|
|
211
|
+
* The type(s) of the property
|
|
212
|
+
*/
|
|
182
213
|
$type: ToSchemaPropertyType<Value>;
|
|
214
|
+
/**
|
|
215
|
+
* Custom validators for the property and its types
|
|
216
|
+
*/
|
|
217
|
+
$validators?: PropertyValidators<ToSchemaPropertyType<Value>>;
|
|
183
218
|
};
|
|
184
219
|
|
|
185
220
|
/**
|
|
@@ -224,10 +259,15 @@ type UnwrapSingle<Value extends unknown[]> = Value extends [infer Only]
|
|
|
224
259
|
export type ValidatedProperty = {
|
|
225
260
|
required: boolean;
|
|
226
261
|
types: ValidatedPropertyType[];
|
|
262
|
+
validators: ValidatedPropertyValidators;
|
|
227
263
|
};
|
|
228
264
|
|
|
229
265
|
export type ValidatedPropertyType = Schematic<unknown> | ValueName;
|
|
230
266
|
|
|
267
|
+
export type ValidatedPropertyValidators = {
|
|
268
|
+
[Key in ValueName]?: Array<(value: unknown) => boolean>;
|
|
269
|
+
};
|
|
270
|
+
|
|
231
271
|
export type ValidatedSchema = {
|
|
232
272
|
enabled: boolean;
|
|
233
273
|
keys: {
|
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import {isConstructor} 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
8
|
PROPERTY_REQUIRED,
|
|
9
9
|
PROPERTY_TYPE,
|
|
10
|
+
PROPERTY_VALIDATORS,
|
|
10
11
|
TYPE_ALL,
|
|
11
12
|
TYPE_OBJECT,
|
|
12
13
|
TYPE_UNDEFINED,
|
|
13
14
|
} from '../constants';
|
|
14
15
|
import {isInstance, isSchematic} from '../is';
|
|
15
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
Schema,
|
|
18
|
+
ValidatedPropertyType,
|
|
19
|
+
ValidatedPropertyValidators,
|
|
20
|
+
ValidatedSchema,
|
|
21
|
+
} from '../models';
|
|
16
22
|
|
|
17
23
|
function addPropertyType(
|
|
18
24
|
to: ValidatedSchema,
|
|
19
25
|
key: string,
|
|
20
26
|
values: ValidatedPropertyType[],
|
|
27
|
+
validators: ValidatedPropertyValidators,
|
|
21
28
|
required: boolean,
|
|
22
29
|
): void {
|
|
23
30
|
if (to.keys.set.has(key)) {
|
|
@@ -35,12 +42,15 @@ function addPropertyType(
|
|
|
35
42
|
to.properties[key] = {
|
|
36
43
|
required,
|
|
37
44
|
types: values,
|
|
45
|
+
validators: {},
|
|
38
46
|
};
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
if (!required && !to.properties[key].types.includes(TYPE_UNDEFINED)) {
|
|
42
50
|
to.properties[key].types.push(TYPE_UNDEFINED);
|
|
43
51
|
}
|
|
52
|
+
|
|
53
|
+
to.properties[key].validators = validators;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
export function getSchema(schema: unknown): ValidatedSchema {
|
|
@@ -94,7 +104,7 @@ function getTypes(
|
|
|
94
104
|
continue;
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
addPropertyType(validated, prefix, [TYPE_OBJECT], type[PROPERTY_REQUIRED] !== false);
|
|
107
|
+
addPropertyType(validated, prefix, [TYPE_OBJECT], {}, type[PROPERTY_REQUIRED] !== false);
|
|
98
108
|
|
|
99
109
|
propertyTypes.push(TYPE_OBJECT);
|
|
100
110
|
|
|
@@ -135,17 +145,24 @@ function getValidatedSchema(
|
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
let required = true;
|
|
148
|
+
let validators: ValidatedPropertyValidators = {};
|
|
149
|
+
|
|
150
|
+
const isObject = typeof value === 'object' && value !== null;
|
|
138
151
|
|
|
139
|
-
if (
|
|
152
|
+
if (isObject && PROPERTY_REQUIRED in value) {
|
|
140
153
|
required = typeof value[PROPERTY_REQUIRED] === 'boolean' ? value[PROPERTY_REQUIRED] : true;
|
|
141
154
|
}
|
|
142
155
|
|
|
156
|
+
if (isObject && PROPERTY_VALIDATORS in value) {
|
|
157
|
+
validators = getValidators(value[PROPERTY_VALIDATORS]);
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
const prefixedKey = `${prefix}${key}`;
|
|
144
161
|
|
|
145
162
|
const types = getTypes(value, validated, prefixedKey);
|
|
146
163
|
|
|
147
164
|
if (types.length > 0) {
|
|
148
|
-
addPropertyType(validated, prefixedKey, types, required);
|
|
165
|
+
addPropertyType(validated, prefixedKey, types, validators, required);
|
|
149
166
|
}
|
|
150
167
|
}
|
|
151
168
|
|
|
@@ -155,3 +172,21 @@ function getValidatedSchema(
|
|
|
155
172
|
|
|
156
173
|
return validated;
|
|
157
174
|
}
|
|
175
|
+
|
|
176
|
+
function getValidators(original: unknown): ValidatedPropertyValidators {
|
|
177
|
+
const validators: ValidatedPropertyValidators = {};
|
|
178
|
+
|
|
179
|
+
if (typeof original !== 'object' || original === null) {
|
|
180
|
+
return validators;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const type of TYPE_ALL) {
|
|
184
|
+
const value = (original as PlainObject)[type];
|
|
185
|
+
|
|
186
|
+
validators[type] = (Array.isArray(value) ? value : [value]).filter(
|
|
187
|
+
validator => typeof validator === 'function',
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return validators;
|
|
192
|
+
}
|
|
@@ -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
|
@@ -4,6 +4,7 @@ export declare const EXPRESSION_INDEX: RegExp;
|
|
|
4
4
|
export declare const EXPRESSION_PROPERTY: RegExp;
|
|
5
5
|
export declare const PROPERTY_REQUIRED = "$required";
|
|
6
6
|
export declare const PROPERTY_TYPE = "$type";
|
|
7
|
+
export declare const PROPERTY_VALIDATORS = "$validators";
|
|
7
8
|
export declare const SCHEMATIC_NAME = "$schematic";
|
|
8
9
|
export declare const TYPE_OBJECT = "object";
|
|
9
10
|
export declare const TYPE_UNDEFINED = "undefined";
|
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
|
*/
|
|
@@ -36,6 +37,9 @@ export type NestedSchema = {
|
|
|
36
37
|
type OptionalKeys<Value> = {
|
|
37
38
|
[Key in keyof Value]-?: {} extends Pick<Value, Key> ? Key : never;
|
|
38
39
|
}[keyof Value];
|
|
40
|
+
type PropertyValidators<Value> = {
|
|
41
|
+
[Key in ExtractValueNames<Value>]?: ((value: Values[Key]) => boolean) | Array<(value: Values[Key]) => boolean>;
|
|
42
|
+
};
|
|
39
43
|
type RequiredKeys<Value> = Exclude<keyof Value, OptionalKeys<Value>>;
|
|
40
44
|
/**
|
|
41
45
|
* A schema for validating objects
|
|
@@ -51,6 +55,7 @@ interface SchemaIndex {
|
|
|
51
55
|
export type SchemaProperty = {
|
|
52
56
|
$required?: boolean;
|
|
53
57
|
$type: SchemaPropertyType | SchemaPropertyType[];
|
|
58
|
+
$validators?: PropertyValidators<SchemaPropertyType | SchemaPropertyType[]>;
|
|
54
59
|
};
|
|
55
60
|
type SchemaPropertyType = Constructor | Schema | Schematic<unknown> | ValueName;
|
|
56
61
|
type ToSchemaPropertyType<Value> = UnwrapSingle<DeduplicateTuple<MapToSchemaPropertyTypes<UnionToTuple<Value>>>>;
|
|
@@ -65,12 +70,32 @@ type TuplePermutations<Tuple extends unknown[], Elput extends unknown[] = []> =
|
|
|
65
70
|
}[keyof Tuple & `${number}`];
|
|
66
71
|
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
72
|
export type TypedPropertyOptional<Value> = {
|
|
73
|
+
/**
|
|
74
|
+
* The property is not required
|
|
75
|
+
*/
|
|
68
76
|
$required: false;
|
|
77
|
+
/**
|
|
78
|
+
* The type(s) of the property
|
|
79
|
+
*/
|
|
69
80
|
$type: ToSchemaPropertyType<Exclude<Value, undefined>>;
|
|
81
|
+
/**
|
|
82
|
+
* Custom validators for the property and its types
|
|
83
|
+
*/
|
|
84
|
+
$validators?: PropertyValidators<ToSchemaPropertyType<Exclude<Value, undefined>>>;
|
|
70
85
|
};
|
|
71
86
|
export type TypedPropertyRequired<Value> = {
|
|
87
|
+
/**
|
|
88
|
+
* The property is required _(defaults to `true`)_
|
|
89
|
+
*/
|
|
72
90
|
$required?: true;
|
|
91
|
+
/**
|
|
92
|
+
* The type(s) of the property
|
|
93
|
+
*/
|
|
73
94
|
$type: ToSchemaPropertyType<Value>;
|
|
95
|
+
/**
|
|
96
|
+
* Custom validators for the property and its types
|
|
97
|
+
*/
|
|
98
|
+
$validators?: PropertyValidators<ToSchemaPropertyType<Value>>;
|
|
74
99
|
};
|
|
75
100
|
/**
|
|
76
101
|
* Create a schema type constrained to match a TypeScript type
|
|
@@ -92,8 +117,12 @@ type UnwrapSingle<Value extends unknown[]> = Value extends [infer Only] ? Only :
|
|
|
92
117
|
export type ValidatedProperty = {
|
|
93
118
|
required: boolean;
|
|
94
119
|
types: ValidatedPropertyType[];
|
|
120
|
+
validators: ValidatedPropertyValidators;
|
|
95
121
|
};
|
|
96
122
|
export type ValidatedPropertyType = Schematic<unknown> | ValueName;
|
|
123
|
+
export type ValidatedPropertyValidators = {
|
|
124
|
+
[Key in ValueName]?: Array<(value: unknown) => boolean>;
|
|
125
|
+
};
|
|
97
126
|
export type ValidatedSchema = {
|
|
98
127
|
enabled: boolean;
|
|
99
128
|
keys: {
|
|
@@ -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;
|