@oscarpalmer/jhunal 0.8.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/is.js +2 -4
- package/dist/jhunal.full.js +76 -609
- 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 +23 -8
- package/dist/validation/value.validation.js +5 -5
- package/package.json +6 -6
- package/src/constants.ts +3 -1
- package/src/is.ts +1 -4
- package/src/models.ts +40 -0
- package/src/validation/schema.validation.ts +42 -6
- package/src/validation/value.validation.ts +13 -6
- package/types/constants.d.ts +1 -0
- package/types/is.d.ts +0 -1
- package/types/models.d.ts +29 -0
- package/types/validation/value.validation.d.ts +2 -2
|
@@ -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,7 +1,8 @@
|
|
|
1
|
-
import { EXPRESSION_HAS_NUMBER, EXPRESSION_INDEX, EXPRESSION_PROPERTY, PROPERTY_REQUIRED, PROPERTY_TYPE, TYPE_ALL, TYPE_OBJECT, TYPE_UNDEFINED } from "../constants.js";
|
|
2
|
-
import {
|
|
3
|
-
import { smush } from "
|
|
4
|
-
|
|
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
|
+
import { isInstance, isSchematic } from "../is.js";
|
|
3
|
+
import { smush } from "../node_modules/@oscarpalmer/atoms/dist/value/smush.js";
|
|
4
|
+
import { isConstructor } from "@oscarpalmer/atoms/is";
|
|
5
|
+
function addPropertyType(to, key, values, validators, required) {
|
|
5
6
|
if (to.keys.set.has(key)) {
|
|
6
7
|
const property = to.properties[key];
|
|
7
8
|
for (const type of values) if (!property.types.includes(type)) property.types.push(type);
|
|
@@ -10,10 +11,12 @@ function addPropertyType(to, key, values, required) {
|
|
|
10
11
|
to.keys.set.add(key);
|
|
11
12
|
to.properties[key] = {
|
|
12
13
|
required,
|
|
13
|
-
types: values
|
|
14
|
+
types: values,
|
|
15
|
+
validators: {}
|
|
14
16
|
};
|
|
15
17
|
}
|
|
16
18
|
if (!required && !to.properties[key].types.includes("undefined")) to.properties[key].types.push(TYPE_UNDEFINED);
|
|
19
|
+
to.properties[key].validators = validators;
|
|
17
20
|
}
|
|
18
21
|
function getSchema(schema) {
|
|
19
22
|
const validated = {
|
|
@@ -46,7 +49,7 @@ function getTypes(value, validated, prefix) {
|
|
|
46
49
|
propertyTypes.push(...getTypes(type[PROPERTY_TYPE], validated, prefix));
|
|
47
50
|
continue;
|
|
48
51
|
}
|
|
49
|
-
addPropertyType(validated, prefix, [TYPE_OBJECT], type[PROPERTY_REQUIRED] !== false);
|
|
52
|
+
addPropertyType(validated, prefix, [TYPE_OBJECT], {}, type[PROPERTY_REQUIRED] !== false);
|
|
50
53
|
propertyTypes.push(TYPE_OBJECT);
|
|
51
54
|
getValidatedSchema(type, validated, prefix);
|
|
52
55
|
}
|
|
@@ -66,12 +69,24 @@ function getValidatedSchema(schema, validated, prefix) {
|
|
|
66
69
|
if (EXPRESSION_PROPERTY.test(key)) continue;
|
|
67
70
|
if (EXPRESSION_HAS_NUMBER.test(key) && arrayKeys.has(key.replace(EXPRESSION_INDEX, ""))) continue;
|
|
68
71
|
let required = true;
|
|
69
|
-
|
|
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]);
|
|
70
76
|
const prefixedKey = `${prefix}${key}`;
|
|
71
77
|
const types = getTypes(value, validated, prefixedKey);
|
|
72
|
-
if (types.length > 0) addPropertyType(validated, prefixedKey, types, required);
|
|
78
|
+
if (types.length > 0) addPropertyType(validated, prefixedKey, types, validators, required);
|
|
73
79
|
}
|
|
74
80
|
if (noPrefix) validated.keys.array.sort();
|
|
75
81
|
return validated;
|
|
76
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
|
+
}
|
|
77
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",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"exports": {
|
|
23
23
|
"./package.json": "./package.json",
|
|
24
24
|
".": {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
"types": "./types/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"files": ["dist", "src", "types"],
|
|
@@ -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/is.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
+
import {isConstructor} from '@oscarpalmer/atoms/is';
|
|
1
2
|
import {SCHEMATIC_NAME} from './constants';
|
|
2
3
|
import type {Constructor} from './models';
|
|
3
4
|
import type {Schematic} from './schematic';
|
|
4
5
|
|
|
5
|
-
export function isConstructor(value: unknown): value is Constructor<unknown> {
|
|
6
|
-
return typeof value === 'function' && value.prototype !== undefined;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
6
|
export function isInstance<Instance>(
|
|
10
7
|
constructor: Constructor<Instance>,
|
|
11
8
|
): (value: unknown) => value is Instance {
|
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,22 +1,30 @@
|
|
|
1
|
+
import {isConstructor} from '@oscarpalmer/atoms/is';
|
|
1
2
|
import type {PlainObject} from '@oscarpalmer/atoms/models';
|
|
2
|
-
import {smush} from '@oscarpalmer/atoms/value';
|
|
3
|
+
import {smush} from '@oscarpalmer/atoms/value/misc';
|
|
3
4
|
import {
|
|
4
5
|
EXPRESSION_HAS_NUMBER,
|
|
5
6
|
EXPRESSION_INDEX,
|
|
6
7
|
EXPRESSION_PROPERTY,
|
|
7
8
|
PROPERTY_REQUIRED,
|
|
8
9
|
PROPERTY_TYPE,
|
|
10
|
+
PROPERTY_VALIDATORS,
|
|
9
11
|
TYPE_ALL,
|
|
10
12
|
TYPE_OBJECT,
|
|
11
13
|
TYPE_UNDEFINED,
|
|
12
14
|
} from '../constants';
|
|
13
|
-
import {
|
|
14
|
-
import type {
|
|
15
|
+
import {isInstance, isSchematic} from '../is';
|
|
16
|
+
import type {
|
|
17
|
+
Schema,
|
|
18
|
+
ValidatedPropertyType,
|
|
19
|
+
ValidatedPropertyValidators,
|
|
20
|
+
ValidatedSchema,
|
|
21
|
+
} from '../models';
|
|
15
22
|
|
|
16
23
|
function addPropertyType(
|
|
17
24
|
to: ValidatedSchema,
|
|
18
25
|
key: string,
|
|
19
26
|
values: ValidatedPropertyType[],
|
|
27
|
+
validators: ValidatedPropertyValidators,
|
|
20
28
|
required: boolean,
|
|
21
29
|
): void {
|
|
22
30
|
if (to.keys.set.has(key)) {
|
|
@@ -34,12 +42,15 @@ function addPropertyType(
|
|
|
34
42
|
to.properties[key] = {
|
|
35
43
|
required,
|
|
36
44
|
types: values,
|
|
45
|
+
validators: {},
|
|
37
46
|
};
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
if (!required && !to.properties[key].types.includes(TYPE_UNDEFINED)) {
|
|
41
50
|
to.properties[key].types.push(TYPE_UNDEFINED);
|
|
42
51
|
}
|
|
52
|
+
|
|
53
|
+
to.properties[key].validators = validators;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
export function getSchema(schema: unknown): ValidatedSchema {
|
|
@@ -93,7 +104,7 @@ function getTypes(
|
|
|
93
104
|
continue;
|
|
94
105
|
}
|
|
95
106
|
|
|
96
|
-
addPropertyType(validated, prefix, [TYPE_OBJECT], type[PROPERTY_REQUIRED] !== false);
|
|
107
|
+
addPropertyType(validated, prefix, [TYPE_OBJECT], {}, type[PROPERTY_REQUIRED] !== false);
|
|
97
108
|
|
|
98
109
|
propertyTypes.push(TYPE_OBJECT);
|
|
99
110
|
|
|
@@ -134,17 +145,24 @@ function getValidatedSchema(
|
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
let required = true;
|
|
148
|
+
let validators: ValidatedPropertyValidators = {};
|
|
149
|
+
|
|
150
|
+
const isObject = typeof value === 'object' && value !== null;
|
|
137
151
|
|
|
138
|
-
if (
|
|
152
|
+
if (isObject && PROPERTY_REQUIRED in value) {
|
|
139
153
|
required = typeof value[PROPERTY_REQUIRED] === 'boolean' ? value[PROPERTY_REQUIRED] : true;
|
|
140
154
|
}
|
|
141
155
|
|
|
156
|
+
if (isObject && PROPERTY_VALIDATORS in value) {
|
|
157
|
+
validators = getValidators(value[PROPERTY_VALIDATORS]);
|
|
158
|
+
}
|
|
159
|
+
|
|
142
160
|
const prefixedKey = `${prefix}${key}`;
|
|
143
161
|
|
|
144
162
|
const types = getTypes(value, validated, prefixedKey);
|
|
145
163
|
|
|
146
164
|
if (types.length > 0) {
|
|
147
|
-
addPropertyType(validated, prefixedKey, types, required);
|
|
165
|
+
addPropertyType(validated, prefixedKey, types, validators, required);
|
|
148
166
|
}
|
|
149
167
|
}
|
|
150
168
|
|
|
@@ -154,3 +172,21 @@ function getValidatedSchema(
|
|
|
154
172
|
|
|
155
173
|
return validated;
|
|
156
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/is.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Constructor } from './models';
|
|
2
2
|
import type { Schematic } from './schematic';
|
|
3
|
-
export declare function isConstructor(value: unknown): value is Constructor<unknown>;
|
|
4
3
|
export declare function isInstance<Instance>(constructor: Constructor<Instance>): (value: unknown) => value is Instance;
|
|
5
4
|
export declare function isSchematic(value: unknown): value is Schematic<never>;
|
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;
|