@oscarpalmer/jhunal 0.17.0 → 0.19.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.
Files changed (40) hide show
  1. package/dist/constants.d.mts +28 -15
  2. package/dist/constants.mjs +31 -14
  3. package/dist/helpers.d.mts +8 -1
  4. package/dist/helpers.mjs +68 -3
  5. package/dist/index.d.mts +304 -240
  6. package/dist/index.mjs +201 -51
  7. package/dist/models/infer.model.d.mts +66 -0
  8. package/dist/models/infer.model.mjs +1 -0
  9. package/dist/models/misc.model.d.mts +153 -0
  10. package/dist/models/misc.model.mjs +1 -0
  11. package/dist/models/schema.plain.model.d.mts +92 -0
  12. package/dist/models/schema.plain.model.mjs +1 -0
  13. package/dist/models/schema.typed.model.d.mts +96 -0
  14. package/dist/models/schema.typed.model.mjs +1 -0
  15. package/dist/models/transform.model.d.mts +59 -0
  16. package/dist/models/transform.model.mjs +1 -0
  17. package/dist/models/validation.model.d.mts +82 -0
  18. package/dist/models/validation.model.mjs +21 -0
  19. package/dist/schematic.d.mts +34 -1
  20. package/dist/schematic.mjs +11 -12
  21. package/dist/validation/property.validation.d.mts +1 -1
  22. package/dist/validation/property.validation.mjs +21 -17
  23. package/dist/validation/value.validation.d.mts +2 -2
  24. package/dist/validation/value.validation.mjs +73 -12
  25. package/package.json +2 -2
  26. package/src/constants.ts +84 -19
  27. package/src/helpers.ts +162 -4
  28. package/src/index.ts +3 -1
  29. package/src/models/infer.model.ts +105 -0
  30. package/src/models/misc.model.ts +212 -0
  31. package/src/models/schema.plain.model.ts +110 -0
  32. package/src/models/schema.typed.model.ts +109 -0
  33. package/src/models/transform.model.ts +85 -0
  34. package/src/models/validation.model.ts +124 -0
  35. package/src/schematic.ts +55 -10
  36. package/src/validation/property.validation.ts +41 -36
  37. package/src/validation/value.validation.ts +133 -20
  38. package/dist/models.d.mts +0 -484
  39. package/dist/models.mjs +0 -13
  40. package/src/models.ts +0 -665
package/src/schematic.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  import {isPlainObject} from '@oscarpalmer/atoms/is';
2
2
  import type {PlainObject} from '@oscarpalmer/atoms/models';
3
- import {MESSAGE_SCHEMA_INVALID_TYPE, SCHEMATIC_NAME} from './constants';
4
- import {isSchematic} from './helpers';
3
+ import type {Result} from '@oscarpalmer/atoms/result/models';
4
+ import {PROPERTY_SCHEMATIC, SCHEMATIC_MESSAGE_SCHEMA_INVALID_TYPE} from './constants';
5
+ import {getReporting, isSchematic} from './helpers';
6
+ import type {Infer} from './models/infer.model';
7
+ import type {Schema} from './models/schema.plain.model';
8
+ import type {TypedSchema} from './models/schema.typed.model';
5
9
  import {
6
10
  SchematicError,
7
- type Infer,
8
- type Schema,
9
- type TypedSchema,
10
11
  type ValidatedProperty,
11
- } from './models';
12
+ type ValidationInformation,
13
+ } from './models/validation.model';
12
14
  import {getProperties} from './validation/property.validation';
13
15
  import {validateObject} from './validation/value.validation';
16
+ import {error} from '@oscarpalmer/atoms/result/misc';
14
17
 
15
18
  /**
16
19
  * A schematic for validating objects
@@ -21,7 +24,7 @@ export class Schematic<Model> {
21
24
  #properties: ValidatedProperty[];
22
25
 
23
26
  constructor(properties: ValidatedProperty[]) {
24
- Object.defineProperty(this, SCHEMATIC_NAME, {
27
+ Object.defineProperty(this, PROPERTY_SCHEMATIC, {
25
28
  value: true,
26
29
  });
27
30
 
@@ -30,11 +33,53 @@ export class Schematic<Model> {
30
33
 
31
34
  /**
32
35
  * Does the value match the schema?
36
+ *
37
+ * Will assert that the values matches the schema and throw an error if it does not. The error will contain all validation information for the first property that fails validation.
33
38
  * @param value Value to validate
39
+ * @param errors Throws an error for the first validation failure
40
+ * @returns `true` if the value matches the schema, otherwise throws an error
41
+ */
42
+ is(value: unknown, errors: 'throw'): asserts value is Model;
43
+
44
+ /**
45
+ * Does the value match the schema?
46
+ *
47
+ * Will validate that the value matches the schema and return a result of `true` or all validation information for validation failures from the same depth in the object.
48
+ * @param value Value to validate
49
+ * @param errors All
50
+ * @returns `true` if the value matches the schema, otherwise `false`
51
+ */
52
+ is(value: unknown, errors: 'all'): Result<true, ValidationInformation[]>;
53
+
54
+ /**
55
+ * Does the value match the schema?
56
+ *
57
+ * Will validate that the value matches the schema and return a result of `true` or all validation information for the failing property.
58
+ * @param value Value to validate
59
+ * @param errors First
34
60
  * @returns `true` if the value matches the schema, otherwise `false`
35
61
  */
36
- is(value: unknown): value is Model {
37
- return validateObject(value, this.#properties);
62
+ is(value: unknown, errors: 'first'): Result<true, ValidationInformation>;
63
+
64
+ /**
65
+ * Does the value match the schema?
66
+ *
67
+ * Will validate that the value matches the schema and return `true` or `false`, without any validation information for validation failures.
68
+ * @param value Value to validate
69
+ * @returns `true` if the value matches the schema, otherwise `false`
70
+ */
71
+ is(value: unknown): value is Model;
72
+
73
+ is(value: unknown, errors?: unknown): unknown {
74
+ const reporting = getReporting(errors);
75
+
76
+ const result = validateObject(value, this.#properties, reporting);
77
+
78
+ if (typeof result === 'boolean') {
79
+ return result;
80
+ }
81
+
82
+ return error(reporting.all ? result : result[0]);
38
83
  }
39
84
  }
40
85
 
@@ -62,7 +107,7 @@ export function schematic<Model extends Schema>(schema: Model): Schematic<Model>
62
107
  }
63
108
 
64
109
  if (!isPlainObject(schema)) {
65
- throw new SchematicError(MESSAGE_SCHEMA_INVALID_TYPE);
110
+ throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_TYPE);
66
111
  }
67
112
 
68
113
  return new Schematic<Model>(getProperties(schema));
@@ -2,33 +2,31 @@ import {isConstructor, isPlainObject} from '@oscarpalmer/atoms/is';
2
2
  import type {PlainObject} from '@oscarpalmer/atoms/models';
3
3
  import {join} from '@oscarpalmer/atoms/string';
4
4
  import {
5
- MESSAGE_SCHEMA_INVALID_EMPTY,
6
- MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED,
7
- MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE,
8
- MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED,
9
- MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE,
10
- MESSAGE_VALIDATOR_INVALID_KEY,
11
- MESSAGE_VALIDATOR_INVALID_TYPE,
12
- MESSAGE_VALIDATOR_INVALID_VALUE,
5
+ SCHEMATIC_MESSAGE_SCHEMA_INVALID_EMPTY,
6
+ SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED,
7
+ SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE,
8
+ SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED,
9
+ SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE,
10
+ SCHEMATIC_MESSAGE_VALIDATOR_INVALID_KEY,
11
+ SCHEMATIC_MESSAGE_VALIDATOR_INVALID_TYPE,
12
+ SCHEMATIC_MESSAGE_VALIDATOR_INVALID_VALUE,
13
13
  PROPERTY_REQUIRED,
14
14
  PROPERTY_TYPE,
15
15
  PROPERTY_VALIDATORS,
16
16
  TEMPLATE_PATTERN,
17
- TEMPLATE_PATTERN_KEY,
18
- TEMPLATE_PATTERN_PROPERTY,
19
17
  TYPE_ALL,
20
18
  TYPE_OBJECT,
21
19
  TYPE_UNDEFINED,
22
20
  VALIDATABLE_TYPES,
23
21
  } from '../constants';
24
22
  import {instanceOf, isSchematic} from '../helpers';
23
+ import type {ValueName} from '../models/misc.model';
25
24
  import {
26
25
  SchematicError,
27
26
  type ValidatedProperty,
28
27
  type ValidatedPropertyType,
29
28
  type ValidatedPropertyValidators,
30
- type ValueName,
31
- } from '../models';
29
+ } from '../models/validation.model';
32
30
 
33
31
  function getDisallowedProperty(obj: PlainObject): string | undefined {
34
32
  if (PROPERTY_REQUIRED in obj) {
@@ -50,7 +48,7 @@ export function getProperties(
50
48
  fromType?: boolean,
51
49
  ): ValidatedProperty[] {
52
50
  if (Object.keys(original).length === 0) {
53
- throw new SchematicError(MESSAGE_SCHEMA_INVALID_EMPTY);
51
+ throw new SchematicError(SCHEMATIC_MESSAGE_SCHEMA_INVALID_EMPTY);
54
52
  }
55
53
 
56
54
  if (fromType ?? false) {
@@ -58,10 +56,10 @@ export function getProperties(
58
56
 
59
57
  if (property != null) {
60
58
  throw new SchematicError(
61
- MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED.replace(TEMPLATE_PATTERN_KEY, prefix!).replace(
62
- TEMPLATE_PATTERN_PROPERTY,
63
- property,
64
- ),
59
+ SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED.replace(
60
+ TEMPLATE_PATTERN,
61
+ prefix!,
62
+ ).replace(TEMPLATE_PATTERN, property),
65
63
  );
66
64
  }
67
65
  }
@@ -74,14 +72,12 @@ export function getProperties(
74
72
  for (let keyIndex = 0; keyIndex < keysLength; keyIndex += 1) {
75
73
  const key = keys[keyIndex];
76
74
 
75
+ const prefixed = join([prefix, key], '.');
77
76
  const value = original[key];
78
77
 
79
78
  if (value == null) {
80
79
  throw new SchematicError(
81
- MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE.replace(
82
- TEMPLATE_PATTERN,
83
- join([prefix, key], '.'),
84
- ),
80
+ SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE.replace(TEMPLATE_PATTERN, prefixed),
85
81
  );
86
82
  }
87
83
 
@@ -94,11 +90,9 @@ export function getProperties(
94
90
  required = getRequired(key, value) ?? required;
95
91
  validators = getValidators(value[PROPERTY_VALIDATORS]);
96
92
 
97
- if (PROPERTY_TYPE in value) {
98
- types.push(TYPE_OBJECT, ...getTypes(key, value[PROPERTY_TYPE], prefix, true));
99
- } else {
100
- types.push(TYPE_OBJECT, ...getTypes(key, value, prefix));
101
- }
93
+ const hasType = PROPERTY_TYPE in value;
94
+
95
+ types.push(...getTypes(key, hasType ? value[PROPERTY_TYPE] : value, prefix, hasType));
102
96
  } else {
103
97
  types.push(...getTypes(key, value, prefix));
104
98
  }
@@ -108,9 +102,12 @@ export function getProperties(
108
102
  }
109
103
 
110
104
  properties.push({
111
- key,
112
105
  types,
113
106
  validators,
107
+ key: {
108
+ full: prefixed,
109
+ short: key,
110
+ },
114
111
  required: required && !types.includes(TYPE_UNDEFINED),
115
112
  });
116
113
  }
@@ -125,7 +122,7 @@ function getRequired(key: string, obj: PlainObject): boolean | undefined {
125
122
 
126
123
  if (typeof obj[PROPERTY_REQUIRED] !== 'boolean') {
127
124
  throw new SchematicError(
128
- MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED.replace(TEMPLATE_PATTERN, key),
125
+ SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_REQUIRED.replace(TEMPLATE_PATTERN, key),
129
126
  );
130
127
  }
131
128
 
@@ -152,7 +149,7 @@ function getTypes(
152
149
  break;
153
150
 
154
151
  case isPlainObject(value):
155
- types.push(...getProperties(value, join([prefix, key], '.'), fromType));
152
+ types.push(getProperties(value, join([prefix, key], '.'), fromType));
156
153
  break;
157
154
 
158
155
  case isSchematic(value):
@@ -165,14 +162,20 @@ function getTypes(
165
162
 
166
163
  default:
167
164
  throw new SchematicError(
168
- MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace(TEMPLATE_PATTERN, join([prefix, key], '.')),
165
+ SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace(
166
+ TEMPLATE_PATTERN,
167
+ join([prefix, key], '.'),
168
+ ),
169
169
  );
170
170
  }
171
171
  }
172
172
 
173
173
  if (types.length === 0) {
174
174
  throw new SchematicError(
175
- MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace(TEMPLATE_PATTERN, join([prefix, key], '.')),
175
+ SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_TYPE.replace(
176
+ TEMPLATE_PATTERN,
177
+ join([prefix, key], '.'),
178
+ ),
176
179
  );
177
180
  }
178
181
 
@@ -187,7 +190,7 @@ function getValidators(original: unknown): ValidatedPropertyValidators {
187
190
  }
188
191
 
189
192
  if (!isPlainObject(original)) {
190
- throw new TypeError(MESSAGE_VALIDATOR_INVALID_TYPE);
193
+ throw new TypeError(SCHEMATIC_MESSAGE_VALIDATOR_INVALID_TYPE);
191
194
  }
192
195
 
193
196
  const keys = Object.keys(original);
@@ -197,17 +200,19 @@ function getValidators(original: unknown): ValidatedPropertyValidators {
197
200
  const key = keys[index];
198
201
 
199
202
  if (!VALIDATABLE_TYPES.has(key as never)) {
200
- throw new TypeError(MESSAGE_VALIDATOR_INVALID_KEY.replace(TEMPLATE_PATTERN, key));
203
+ throw new TypeError(SCHEMATIC_MESSAGE_VALIDATOR_INVALID_KEY.replace(TEMPLATE_PATTERN, key));
201
204
  }
202
205
 
203
206
  const value = (original as PlainObject)[key];
204
207
 
205
- validators[key as ValueName] = (Array.isArray(value) ? value : [value]).filter(item => {
208
+ validators[key as ValueName] = (Array.isArray(value) ? value : [value]).map(item => {
206
209
  if (typeof item !== 'function') {
207
- throw new TypeError(MESSAGE_VALIDATOR_INVALID_VALUE.replace(TEMPLATE_PATTERN, key));
210
+ throw new TypeError(
211
+ SCHEMATIC_MESSAGE_VALIDATOR_INVALID_VALUE.replace(TEMPLATE_PATTERN, key),
212
+ );
208
213
  }
209
214
 
210
- return true;
215
+ return item;
211
216
  });
212
217
  }
213
218
 
@@ -1,12 +1,82 @@
1
1
  import {isPlainObject} from '@oscarpalmer/atoms/is';
2
- import {isSchematic} from '../helpers';
3
- import type {ValidatedProperty, ValidatedPropertyType, ValueName} from '../models';
2
+ import type {GenericCallback} from '@oscarpalmer/atoms/models';
3
+ import {TYPE_OBJECT} from '../constants';
4
+ import {
5
+ getInvalidInputMessage,
6
+ getInvalidMissingMessage,
7
+ getInvalidTypeMessage,
8
+ getInvalidValidatorMessage,
9
+ isSchematic,
10
+ } from '../helpers';
11
+ import type {ValueName} from '../models/misc.model';
12
+ import {
13
+ ValidationError,
14
+ type ReportingInformation,
15
+ type ValidatedProperty,
16
+ type ValidatedPropertyType,
17
+ type ValidationInformation,
18
+ } from '../models/validation.model';
19
+
20
+ function validateNamed(
21
+ property: ValidatedProperty,
22
+ name: ValueName,
23
+ value: unknown,
24
+ validation: ValidationInformation[],
25
+ ): boolean {
26
+ if (!validators[name](value)) {
27
+ return false;
28
+ }
29
+
30
+ const propertyValidators = property.validators[name];
4
31
 
5
- export function validateObject(obj: unknown, properties: ValidatedProperty[]): boolean {
32
+ if (propertyValidators == null || propertyValidators.length === 0) {
33
+ return true;
34
+ }
35
+
36
+ const {length} = propertyValidators;
37
+
38
+ for (let index = 0; index < length; index += 1) {
39
+ const validator = propertyValidators[index];
40
+
41
+ if (!validator(value)) {
42
+ validation.push({
43
+ value,
44
+ key: {...property.key},
45
+ message: getInvalidValidatorMessage(property, name, index, length),
46
+ validator: validator as GenericCallback,
47
+ });
48
+
49
+ return false;
50
+ }
51
+ }
52
+
53
+ return true;
54
+ }
55
+
56
+ export function validateObject(
57
+ obj: unknown,
58
+ properties: ValidatedProperty[],
59
+ reporting: ReportingInformation,
60
+ property?: ValidatedProperty,
61
+ validation?: ValidationInformation[],
62
+ ): boolean | ValidationInformation[] {
6
63
  if (!isPlainObject(obj)) {
7
- return false;
64
+ const information = {
65
+ key: {full: '', short: ''},
66
+ message:
67
+ property == null ? getInvalidInputMessage(obj) : getInvalidTypeMessage(property, obj),
68
+ value: obj,
69
+ };
70
+
71
+ if (reporting.throw) {
72
+ throw new ValidationError([information]);
73
+ }
74
+
75
+ return reporting.none ? false : [information];
8
76
  }
9
77
 
78
+ const allInformation: ValidationInformation[] = [];
79
+
10
80
  const propertiesLength = properties.length;
11
81
 
12
82
  outer: for (let propertyIndex = 0; propertyIndex < propertiesLength; propertyIndex += 1) {
@@ -14,48 +84,91 @@ export function validateObject(obj: unknown, properties: ValidatedProperty[]): b
14
84
 
15
85
  const {key, required, types} = property;
16
86
 
17
- const value = obj[key];
87
+ const value = obj[key.short];
18
88
 
19
89
  if (value === undefined && required) {
20
- return false;
90
+ const information: ValidationInformation = {
91
+ value,
92
+ key: {...key},
93
+ message: getInvalidMissingMessage(property),
94
+ };
95
+
96
+ if (reporting.throw && validation == null) {
97
+ throw new ValidationError([information]);
98
+ }
99
+
100
+ if (validation != null) {
101
+ validation.push(information);
102
+ }
103
+
104
+ if (reporting.all) {
105
+ allInformation.push(information);
106
+
107
+ continue;
108
+ }
109
+
110
+ return reporting.none ? false : [information];
21
111
  }
22
112
 
23
113
  const typesLength = types.length;
24
114
 
115
+ const information: ValidationInformation[] = [];
116
+
25
117
  for (let typeIndex = 0; typeIndex < typesLength; typeIndex += 1) {
26
118
  const type = types[typeIndex];
27
119
 
28
- if (validateValue(type, property, value)) {
120
+ if (validateValue(type, property, value, reporting, information)) {
29
121
  continue outer;
30
122
  }
31
123
  }
32
124
 
33
- return false;
125
+ if (information.length === 0) {
126
+ information.push({
127
+ value,
128
+ key: {...key},
129
+ message: getInvalidTypeMessage(property, value),
130
+ });
131
+ }
132
+
133
+ if (reporting.throw && validation == null) {
134
+ throw new ValidationError(information);
135
+ }
136
+
137
+ validation?.push(...information);
138
+
139
+ if (reporting.all) {
140
+ allInformation.push(...information);
141
+
142
+ continue;
143
+ }
144
+
145
+ return reporting.none ? false : information;
34
146
  }
35
147
 
36
- return true;
148
+ return reporting.none ? true : allInformation.length === 0 ? true : allInformation;
37
149
  }
38
150
 
39
151
  function validateValue(
40
152
  type: ValidatedPropertyType,
41
153
  property: ValidatedProperty,
42
154
  value: unknown,
155
+ reporting: ReportingInformation,
156
+ validation: ValidationInformation[],
43
157
  ): boolean {
44
158
  switch (true) {
45
- case isSchematic(type):
46
- return type.is(value);
47
-
48
159
  case typeof type === 'function':
49
- return (type as (value: unknown) => boolean)(value);
160
+ return (type as GenericCallback)(value);
50
161
 
51
- case typeof type === 'object':
52
- return validateObject(value, [type] as ValidatedProperty[]);
162
+ case Array.isArray(type): {
163
+ const nested = validateObject(value, type, reporting, property, validation);
164
+ return typeof nested !== 'boolean' ? false : nested;
165
+ }
166
+
167
+ case isSchematic(type):
168
+ return type.is(value, reporting as never) as unknown as boolean;
53
169
 
54
170
  default:
55
- return (
56
- validators[type as ValueName](value) &&
57
- (property.validators[type as ValueName]?.every(validator => validator(value)) ?? true)
58
- );
171
+ return validateNamed(property, type as ValueName, value, validation);
59
172
  }
60
173
  }
61
174
 
@@ -69,7 +182,7 @@ const validators: Record<ValueName, (value: unknown) => boolean> = {
69
182
  function: value => typeof value === 'function',
70
183
  null: value => value === null,
71
184
  number: value => typeof value === 'number',
72
- object: value => typeof value === 'object' && value !== null,
185
+ object: value => typeof value === TYPE_OBJECT && value !== null,
73
186
  string: value => typeof value === 'string',
74
187
  symbol: value => typeof value === 'symbol',
75
188
  undefined: value => value === undefined,