@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 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 TYPE_ALL = new Set([
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
@@ -1,3 +1,4 @@
1
1
  import { isInstance } from "./is.js";
2
+ import { SchematicError } from "./models.js";
2
3
  import { schematic } from "./schematic.js";
3
- export { isInstance, schematic };
4
+ export { SchematicError, isInstance, schematic };
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("Expected a constructor function");
4
+ if (!isConstructor(constructor)) throw new TypeError(MESSAGE_CONSTRUCTOR);
5
5
  return (value) => {
6
6
  return value instanceof constructor;
7
7
  };
@@ -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 TYPE_ALL = new Set([
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("Expected a constructor function");
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) if (!property.types.includes(type)) property.types.push(type);
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
- const validated = {
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
- const typeOfType = typeof type;
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 (typeOfType === "function") {
179
+ if (typeof type === "function") {
161
180
  propertyTypes.push(isConstructor(type) ? isInstance(type) : type);
162
181
  continue;
163
182
  }
164
- if (typeOfType !== "object" || type === null) continue;
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
- addPropertyType(validated, prefix, [TYPE_OBJECT], type[PROPERTY_REQUIRED] !== false);
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(type, validated, prefix);
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
- if (typeof value === "object" && value !== null && PROPERTY_REQUIRED in value) required = typeof value[PROPERTY_REQUIRED] === "boolean" ? value[PROPERTY_REQUIRED] : true;
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 > 0) addPropertyType(validated, prefixedKey, types, required);
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 validateType(type, value) {
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 = getSchema(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 this.#schema.enabled && validateValue(this.#schema, value);
306
+ return validateValue(this.#schema, value);
265
307
  }
266
308
  };
267
309
  function schematic(schema) {
268
- return new Schematic(schema);
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
@@ -0,0 +1,8 @@
1
+ import { ERROR_NAME } from "./constants.js";
2
+ var SchematicError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = ERROR_NAME;
6
+ }
7
+ };
8
+ export { SchematicError };
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 = getSchema(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 this.#schema.enabled && validateValue(this.#schema, value);
19
+ return validateValue(this.#schema, value);
22
20
  }
23
21
  };
24
22
  function schematic(schema) {
25
- return new Schematic(schema);
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 { isConstructor } from "@oscarpalmer/atoms/is";
4
- import { smush } from "@oscarpalmer/atoms/value";
5
- function addPropertyType(to, key, values, required) {
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) if (!property.types.includes(type)) property.types.push(type);
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
- const validated = {
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
- const typeOfType = typeof type;
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 (typeOfType === "function") {
41
+ if (typeof type === "function") {
42
42
  propertyTypes.push(isConstructor(type) ? isInstance(type) : type);
43
43
  continue;
44
44
  }
45
- if (typeOfType !== "object" || type === null) continue;
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
- addPropertyType(validated, prefix, [TYPE_OBJECT], type[PROPERTY_REQUIRED] !== false);
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(type, validated, prefix);
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
- if (typeof value === "object" && value !== null && "$required" in value) required = typeof value["$required"] === "boolean" ? value[PROPERTY_REQUIRED] : true;
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 > 0) addPropertyType(validated, prefixedKey, types, required);
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.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.11.0"
49
49
  }
package/src/constants.ts CHANGED
@@ -1,31 +1,54 @@
1
- import type {Values} from './models';
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 TYPE_ALL = new Set<keyof Values>([
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
@@ -1,3 +1,3 @@
1
1
  export {isInstance} from './is';
2
- export type {Schema, TypedSchema} from './models';
2
+ export {SchematicError, type Schema, type TypedSchema} from './models';
3
3
  export {schematic, type Schematic} from './schematic';
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('Expected a constructor function');
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 ValueName
61
- ? Values[Value & ValueName]
62
- : Value extends Schema
63
- ? Infer<Value>
64
- : never;
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
- : false;
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
- } & Schema;
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 | NestedSchema | SchemaProperty | Schematic<unknown> | ValueName;
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 PlainObject
124
- ? TypedSchema<Value>
125
- : ToValueType<Value>;
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 Function
138
- ? 'function'
139
- : Value extends null
140
- ? 'null'
141
- : Value extends number
142
- ? 'number'
143
- : Value extends object
144
- ? 'object' | ((value: unknown) => value is Value)
145
- : Value extends string
146
- ? 'string'
147
- : Value extends symbol
148
- ? 'symbol'
149
- : Value extends undefined
150
- ? 'undefined'
151
- : (value: unknown) => value is Value;
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
- ? TypedSchemaOptional<Exclude<Model[Key], undefined>>
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 type {Infer, Schema, TypedSchema, ValidatedSchema} from './models';
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
- get enabled(): boolean {
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 = getSchema(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 this.#schema.enabled && validateValue(this.#schema, value);
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
- return new Schematic<Model>(schema);
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 type {Schema, ValidatedPropertyType, ValidatedSchema} from '../models';
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
- if (!property.types.includes(type)) {
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: unknown): ValidatedSchema {
47
- const validated: ValidatedSchema = {
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) || (typeOfType === 'string' && TYPE_ALL.has(type as never))) {
76
- propertyTypes.push(type as never);
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 (typeOfType === 'function') {
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 (typeOfType !== 'object' || type === null) {
88
- continue;
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
- addPropertyType(validated, prefix, [TYPE_OBJECT], type[PROPERTY_REQUIRED] !== false);
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(type as Schema, validated, prefix);
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 (typeof value === 'object' && value !== null && PROPERTY_REQUIRED in value) {
140
- required = typeof value[PROPERTY_REQUIRED] === 'boolean' ? value[PROPERTY_REQUIRED] : true;
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 > 0) {
148
- addPropertyType(validated, prefixedKey, types, required);
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(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
  }
@@ -1,10 +1,21 @@
1
- import type { Values } from './models';
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 TYPE_ALL: Set<keyof Values>;
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
@@ -1,3 +1,3 @@
1
1
  export { isInstance } from './is';
2
- export type { Schema, TypedSchema } from './models';
2
+ export { SchematicError, type Schema, type TypedSchema } from './models';
3
3
  export { schematic, type Schematic } from './schematic';
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 : 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
- } & Schema;
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 | NestedSchema | SchemaProperty | Schematic<unknown> | ValueName;
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>;
@@ -1,13 +1,12 @@
1
1
  import type { PlainObject } from '@oscarpalmer/atoms/models';
2
- import type { Infer, Schema, TypedSchema } from './models';
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
- get enabled(): boolean;
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 { ValidatedSchema } from '../models';
2
- export declare function getSchema(schema: unknown): ValidatedSchema;
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;