@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 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 };
@@ -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
- if (typeof value === "object" && value !== null && PROPERTY_REQUIRED in value) required = typeof value[PROPERTY_REQUIRED] === "boolean" ? value[PROPERTY_REQUIRED] : true;
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 validateType(type, value) {
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
- import { smush } from "@oscarpalmer/atoms/value";
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
- if (typeof value === "object" && value !== null && "$required" in value) required = typeof value["$required"] === "boolean" ? value[PROPERTY_REQUIRED] : true;
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 "@oscarpalmer/atoms/value";
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.139"
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.34",
15
- "oxlint": "^1.49",
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.9.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 {Schema, ValidatedPropertyType, ValidatedSchema} from '../models';
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 (typeof value === 'object' && value !== null && PROPERTY_REQUIRED in value) {
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(type: ValidatedPropertyType, value: unknown): boolean {
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 validators[type](value);
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
  }
@@ -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;