@oscarpalmer/jhunal 0.20.0 → 0.21.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.
@@ -1,4 +1,4 @@
1
- import type {GenericCallback} from '@oscarpalmer/atoms/models';
1
+ import type {GenericCallback, PlainObject} from '@oscarpalmer/atoms/models';
2
2
  import {join} from '@oscarpalmer/atoms/string';
3
3
  import {NAME_ERROR_SCHEMATIC, NAME_ERROR_VALIDATION} from '../constants';
4
4
  import type {Schematic} from '../schematic';
@@ -59,7 +59,7 @@ export type ValidatedProperty = {
59
59
  /**
60
60
  * The property name in the schema
61
61
  */
62
- key: ValidatedPropertyKey;
62
+ key: string;
63
63
  /**
64
64
  * Whether the property is required
65
65
  */
@@ -74,16 +74,6 @@ export type ValidatedProperty = {
74
74
  validators: ValidatedPropertyValidators;
75
75
  };
76
76
 
77
- /**
78
- * The full and short forms of a property's key path
79
- *
80
- * For a nested property `address.street`: `full` is `'address.street'`, `short` is `'street'`
81
- */
82
- export type ValidatedPropertyKey = {
83
- full: string;
84
- short: string;
85
- };
86
-
87
77
  /**
88
78
  * A union of valid types for a {@link ValidatedProperty}'s `types` array
89
79
  *
@@ -139,9 +129,12 @@ export type ValidationInformation = {
139
129
  };
140
130
 
141
131
  /**
142
- * Same shape as {@link ValidatedPropertyKey}; the key path of a failed property
132
+ *
143
133
  */
144
- export type ValidationInformationKey = ValidatedPropertyKey;
134
+ export type ValidationInformationKey = {
135
+ full: string;
136
+ short: string;
137
+ };
145
138
 
146
139
  /**
147
140
  * Options for validation
@@ -157,7 +150,11 @@ export type ValidationOptions<Errors extends ReportingType> = {
157
150
  strict?: boolean;
158
151
  };
159
152
 
160
- export type ValidationOptionsExtended = {
153
+ export type ValidationParameters = {
154
+ information?: ValidationInformation[];
155
+ origin?: ValidatedProperty;
156
+ output: PlainObject;
157
+ prefix?: string;
161
158
  reporting: ReportingInformation;
162
159
  strict: boolean;
163
160
  };
package/src/schematic.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import {isPlainObject} from '@oscarpalmer/atoms/is';
2
2
  import type {PlainObject} from '@oscarpalmer/atoms/models';
3
- import {error} from '@oscarpalmer/atoms/result/misc';
3
+ import {error, ok} from '@oscarpalmer/atoms/result/misc';
4
4
  import type {Result} from '@oscarpalmer/atoms/result/models';
5
5
  import {PROPERTY_SCHEMATIC, SCHEMATIC_MESSAGE_SCHEMA_INVALID_TYPE} from './constants';
6
- import {getOptions, isSchematic} from './helpers';
6
+ import {getParameters, isSchematic} from './helpers';
7
7
  import type {Infer} from './models/infer.model';
8
8
  import type {Schema} from './models/schema.plain.model';
9
9
  import type {TypedSchema} from './models/schema.typed.model';
@@ -34,10 +34,96 @@ export class Schematic<Model> {
34
34
  schematicProperties.set(this, properties);
35
35
  }
36
36
 
37
+ /**
38
+ * Parse a value according to the schema
39
+ *
40
+ * Returns a deeply cloned version of the value or throws an error for the first property that fails validation
41
+ * @param value Value to parse
42
+ * @param options Validation options
43
+ * @returns Deeply cloned version of the value if it matches the schema, otherwise throws an error
44
+ */
45
+ get(value: unknown, options: ValidationOptions<'throw'>): Model;
46
+
47
+ /**
48
+ * Parse a value according to the schema
49
+ *
50
+ * Returns a deeply cloned version of the value or throws an error for the first property that fails validation
51
+ * @param value Value to parse
52
+ * @param errors Reporting type
53
+ * @returns Deeply cloned version of the value if it matches the schema, otherwise throws an error
54
+ */
55
+ get(value: unknown, errors: 'throw'): Model;
56
+
57
+ /**
58
+ * Parse a value according to the schema
59
+ *
60
+ * Returns a result of a deeply cloned version of the value or all validation information for validation failures from the same depth in the value
61
+ * @param value Value to parse
62
+ * @param options Validation options
63
+ * @returns Result holding deeply cloned value or all validation information
64
+ */
65
+ get(value: unknown, options: ValidationOptions<'all'>): Result<Model, ValidationInformation[]>;
66
+
67
+ /**
68
+ * Parse a value according to the schema
69
+ *
70
+ * Returns a result of a deeply cloned version of the value or all validation information for validation failures from the same depth in the value
71
+ * @param value Value to parse
72
+ * @param errors Reporting type
73
+ * @returns Result holding deeply cloned value or all validation information
74
+ */
75
+ get(value: unknown, errors: 'all'): Result<Model, ValidationInformation[]>;
76
+
77
+ /**
78
+ * Parse a value according to the schema
79
+ *
80
+ * Returns a deeply cloned version of the value or all validation information for the first failing property
81
+ * @param value Value to parse
82
+ * @param options Validation options
83
+ * @returns Result holding deeply cloned value or all validation information
84
+ */
85
+ get(value: unknown, options: ValidationOptions<'first'>): Result<Model, ValidationInformation>;
86
+
87
+ /**
88
+ * Parse a value according to the schema
89
+ *
90
+ * Returns a deeply cloned version of the value or all validation information for the first failing property
91
+ * @param value Value to parse
92
+ * @param errors Reporting type
93
+ * @returns Result holding deeply cloned value or all validation information
94
+ */
95
+ get(value: unknown, errors: 'first'): Result<Model, ValidationInformation>;
96
+
97
+ /**
98
+ * Parse a value according to the schema
99
+ *
100
+ * Returns a deeply cloned version of the value or `undefined` if the value does not match the schema
101
+ * @param value Value to parse
102
+ * @param strict Validate if unknown keys are present in the object? _(defaults to `false`)_
103
+ * @returns Deeply cloned value, or `undefined` if it's invalid
104
+ */
105
+ get(value: unknown, strict?: true): Model | undefined;
106
+
107
+ get(value: unknown, options?: unknown): unknown {
108
+ const parameters = getParameters(options);
109
+
110
+ const result = validateObject(value, this.#properties, parameters, true);
111
+
112
+ if (result == null) {
113
+ return;
114
+ }
115
+
116
+ if (!Array.isArray(result)) {
117
+ return parameters.reporting.none ? result : ok(result);
118
+ }
119
+
120
+ return error(parameters.reporting.all ? result : result[0]);
121
+ }
122
+
37
123
  /**
38
124
  * Does the value match the schema?
39
125
  *
40
- * 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.
126
+ * 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
41
127
  * @param value Value to validate
42
128
  * @param options Validation options
43
129
  * @returns `true` if the value matches the schema, otherwise throws an error
@@ -47,7 +133,7 @@ export class Schematic<Model> {
47
133
  /**
48
134
  * Does the value match the schema?
49
135
  *
50
- * 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.
136
+ * 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
51
137
  * @param value Value to validate
52
138
  * @param errors Reporting type
53
139
  * @returns `true` if the value matches the schema, otherwise throws an error
@@ -57,27 +143,27 @@ export class Schematic<Model> {
57
143
  /**
58
144
  * Does the value match the schema?
59
145
  *
60
- * 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.
146
+ * 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 value
61
147
  * @param value Value to validate
62
148
  * @param options Validation options
63
- * @returns `true` if the value matches the schema, otherwise `false`
149
+ * @returns Result holding `true` or all validation information
64
150
  */
65
151
  is(value: unknown, options: ValidationOptions<'all'>): Result<true, ValidationInformation[]>;
66
152
 
67
153
  /**
68
154
  * Does the value match the schema?
69
155
  *
70
- * 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.
156
+ * 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 value
71
157
  * @param value Value to validate
72
158
  * @param errors Reporting type
73
- * @returns `true` if the value matches the schema, otherwise `false`
159
+ * @returns Result holding `true` or all validation information
74
160
  */
75
161
  is(value: unknown, errors: 'all'): Result<true, ValidationInformation[]>;
76
162
 
77
163
  /**
78
164
  * Does the value match the schema?
79
165
  *
80
- * Will validate that the value matches the schema and return a result of `true` or all validation information for the failing property.
166
+ * Will validate that the value matches the schema and return a result of `true` or all validation information for the first failing property
81
167
  * @param value Value to validate
82
168
  * @param options Validation options
83
169
  * @returns `true` if the value matches the schema, otherwise `false`
@@ -87,7 +173,7 @@ export class Schematic<Model> {
87
173
  /**
88
174
  * Does the value match the schema?
89
175
  *
90
- * Will validate that the value matches the schema and return a result of `true` or all validation information for the failing property.
176
+ * Will validate that the value matches the schema and return a result of `true` or all validation information for the first failing property
91
177
  * @param value Value to validate
92
178
  * @param errors Reporting type
93
179
  * @returns `true` if the value matches the schema, otherwise `false`
@@ -97,7 +183,7 @@ export class Schematic<Model> {
97
183
  /**
98
184
  * Does the value match the schema?
99
185
  *
100
- * Will validate that the value matches the schema and return `true` or `false`, without any validation information for validation failures.
186
+ * Will validate that the value matches the schema and return `true` or `false`, without any validation information for validation failures
101
187
  * @param value Value to validate
102
188
  * @param strict Validate if unknown keys are present in the object? _(defaults to `false`)_
103
189
  * @returns `true` if the value matches the schema, otherwise `false`
@@ -105,15 +191,19 @@ export class Schematic<Model> {
105
191
  is(value: unknown, strict?: true): value is Model;
106
192
 
107
193
  is(value: unknown, options?: unknown): unknown {
108
- const {reporting, strict} = getOptions(options);
194
+ const parameters = getParameters(options);
109
195
 
110
- const result = validateObject(value, this.#properties, {reporting, strict});
196
+ const result = validateObject(value, this.#properties, parameters, false);
197
+
198
+ if (result == null) {
199
+ return false;
200
+ }
111
201
 
112
- if (typeof result === 'boolean') {
113
- return result;
202
+ if (!Array.isArray(result)) {
203
+ return parameters.reporting.none ? true : ok(true);
114
204
  }
115
205
 
116
- return error(reporting.all ? result : result[0]);
206
+ return error(parameters.reporting.all ? result : result[0]);
117
207
  }
118
208
  }
119
209
 
@@ -70,13 +70,14 @@ export function getProperties(
70
70
 
71
71
  for (let keyIndex = 0; keyIndex < keysLength; keyIndex += 1) {
72
72
  const key = keys[keyIndex];
73
-
74
- const prefixed = join([prefix, key], '.');
75
73
  const value = original[key];
76
74
 
77
75
  if (value == null) {
78
76
  throw new SchematicError(
79
- SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE.replace(TEMPLATE_PATTERN, prefixed),
77
+ SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE.replace(
78
+ TEMPLATE_PATTERN,
79
+ join([prefix, key], '.'),
80
+ ),
80
81
  );
81
82
  }
82
83
 
@@ -101,12 +102,9 @@ export function getProperties(
101
102
  }
102
103
 
103
104
  properties.push({
105
+ key,
104
106
  types,
105
107
  validators,
106
- key: {
107
- full: prefixed,
108
- short: key,
109
- },
110
108
  required: required && !types.includes(TYPE_UNDEFINED),
111
109
  });
112
110
  }
@@ -1,6 +1,7 @@
1
1
  import {isPlainObject} from '@oscarpalmer/atoms/is';
2
- import type {GenericCallback} from '@oscarpalmer/atoms/models';
2
+ import type {GenericCallback, PlainObject} from '@oscarpalmer/atoms/models';
3
3
  import {join} from '@oscarpalmer/atoms/string';
4
+ import {clone} from '@oscarpalmer/atoms/value/clone';
4
5
  import {TYPE_OBJECT} from '../constants';
5
6
  import {
6
7
  getInvalidInputMessage,
@@ -16,21 +17,16 @@ import {
16
17
  type ValidatedProperty,
17
18
  type ValidatedPropertyType,
18
19
  type ValidationInformation,
19
- type ValidationOptionsExtended,
20
+ type ValidationParameters,
20
21
  } from '../models/validation.model';
21
22
  import {schematicProperties, type Schematic} from '../schematic';
22
23
 
23
- function validateNamed(
24
- property: ValidatedProperty,
25
- name: ValueName,
26
- value: unknown,
27
- validation: ValidationInformation[],
28
- ): boolean {
24
+ function validateNamed(name: ValueName, value: unknown, parameters: ValidationParameters): boolean {
29
25
  if (!validators[name](value)) {
30
26
  return false;
31
27
  }
32
28
 
33
- const propertyValidators = property.validators[name];
29
+ const propertyValidators = parameters.origin!.validators[name];
34
30
 
35
31
  if (propertyValidators == null || propertyValidators.length === 0) {
36
32
  return true;
@@ -42,10 +38,13 @@ function validateNamed(
42
38
  const validator = propertyValidators[index];
43
39
 
44
40
  if (!validator(value)) {
45
- validation.push({
41
+ parameters.information!.push({
46
42
  value,
47
- key: {...property.key},
48
- message: getInvalidValidatorMessage(property, name, index, length),
43
+ key: {
44
+ full: parameters.prefix!,
45
+ short: parameters.origin!.key,
46
+ },
47
+ message: getInvalidValidatorMessage(parameters.prefix!, name, index, length),
49
48
  validator: validator as GenericCallback,
50
49
  });
51
50
 
@@ -59,106 +58,118 @@ function validateNamed(
59
58
  export function validateObject(
60
59
  obj: unknown,
61
60
  properties: ValidatedProperty[],
62
- options: ValidationOptionsExtended,
63
- origin?: ValidatedProperty,
64
- validation?: ValidationInformation[],
65
- ): boolean | ValidationInformation[] {
61
+ parameters: ValidationParameters,
62
+ get: boolean,
63
+ ): PlainObject | ValidationInformation[] | undefined {
66
64
  if (!isPlainObject(obj)) {
67
- const key = origin == null ? {full: '', short: ''} : {...origin.key};
65
+ const key =
66
+ parameters?.origin == null
67
+ ? {full: '', short: ''}
68
+ : {
69
+ full: parameters.prefix!,
70
+ short: parameters.origin.key,
71
+ };
68
72
 
69
73
  const information = {
70
74
  key,
71
75
  message:
72
- origin == null
76
+ parameters?.origin == null
73
77
  ? getInvalidInputMessage(obj)
74
- : getInvalidTypeMessage(
75
- {
76
- ...origin,
77
- key,
78
- },
79
- obj,
80
- ),
78
+ : getInvalidTypeMessage(key.full, parameters.origin.types, obj),
81
79
  value: obj,
82
80
  };
83
81
 
84
- if (options.reporting.throw) {
82
+ if (parameters.reporting.throw) {
85
83
  throw new ValidationError([information]);
86
84
  }
87
85
 
88
- validation?.push(information);
86
+ parameters?.information?.push(information);
89
87
 
90
- return options.reporting.none ? false : [information];
88
+ return parameters.reporting.none ? undefined : [information];
91
89
  }
92
90
 
93
- if (options.strict) {
91
+ if (parameters.strict) {
94
92
  const objKeys = Object.keys(obj);
95
93
 
96
- const propertiesKeys = new Set(properties.map(property => property.key.short));
94
+ const propertiesKeys = new Set(properties.map(property => property.key));
97
95
 
98
96
  const unknownKeys = objKeys.filter(key => !propertiesKeys.has(key));
99
97
 
100
98
  if (unknownKeys.length > 0) {
101
- const key = origin == null ? {full: '', short: ''} : {...origin.key};
99
+ const key =
100
+ parameters?.origin == null
101
+ ? {full: '', short: ''}
102
+ : {
103
+ full: join([parameters.prefix, parameters.origin?.key], '.'),
104
+ short: parameters.origin.key,
105
+ };
102
106
 
103
107
  const information: ValidationInformation = {
104
108
  key,
105
- message: getUnknownKeysMessage(unknownKeys.map(key => join([origin?.key.full, key], '.'))),
109
+ message: getUnknownKeysMessage(
110
+ unknownKeys.map(key => join([parameters?.prefix, key], '.')),
111
+ ),
106
112
  value: obj,
107
113
  };
108
114
 
109
- if (options.reporting.throw) {
115
+ if (parameters.reporting.throw) {
110
116
  throw new ValidationError([information]);
111
117
  }
112
118
 
113
- validation?.push(information);
119
+ parameters?.information?.push(information);
114
120
 
115
- return options.reporting.none ? false : [information];
121
+ return parameters.reporting.none ? undefined : [information];
116
122
  }
117
123
  }
118
124
 
119
125
  const allInformation: ValidationInformation[] = [];
120
126
 
127
+ const output: PlainObject = {};
128
+
121
129
  const propertiesLength = properties.length;
122
130
 
123
131
  outer: for (let propertyIndex = 0; propertyIndex < propertiesLength; propertyIndex += 1) {
124
- let property = properties[propertyIndex];
125
-
126
- property = {
127
- ...property,
128
- key: {
129
- full: join([origin?.key.full, property.key.short], '.'),
130
- short: property.key.short,
131
- },
132
- };
132
+ const property = properties[propertyIndex];
133
133
 
134
134
  const {key, required, types} = property;
135
135
 
136
- const value = obj[key.short];
136
+ const value = obj[key];
137
+
138
+ if (get && value === undefined && !required) {
139
+ // TODO: during get, respect a default value for the property
140
+
141
+ continue;
142
+ }
137
143
 
138
144
  if (value === undefined && required) {
145
+ const prefixedKey = join([parameters.prefix, key], '.');
146
+
139
147
  const information: ValidationInformation = {
140
148
  value,
141
- key: {...key},
142
- message: getInvalidMissingMessage(property),
149
+ key: {
150
+ full: prefixedKey,
151
+ short: key,
152
+ },
153
+ message: getInvalidMissingMessage(prefixedKey, property.types),
143
154
  };
144
155
 
145
- if (options.reporting.throw && validation == null) {
156
+ if (parameters.reporting.throw) {
146
157
  throw new ValidationError([information]);
147
158
  }
148
159
 
149
- if (validation != null) {
150
- validation.push(information);
151
- }
160
+ parameters?.information?.push(information);
152
161
 
153
- if (options.reporting.all) {
162
+ if (parameters.reporting.all) {
154
163
  allInformation.push(information);
155
164
 
156
165
  continue;
157
166
  }
158
167
 
159
- return options.reporting.none ? false : [information];
168
+ return parameters.reporting.none ? undefined : [information];
160
169
  }
161
170
 
171
+ const prefixedKey = join([parameters.prefix, key], '.');
172
+
162
173
  const typesLength = types.length;
163
174
 
164
175
  const information: ValidationInformation[] = [];
@@ -166,7 +177,25 @@ export function validateObject(
166
177
  for (let typeIndex = 0; typeIndex < typesLength; typeIndex += 1) {
167
178
  const type = types[typeIndex];
168
179
 
169
- if (validateValue(type, property, value, options, information)) {
180
+ if (
181
+ validateValue(
182
+ type,
183
+ value,
184
+ {
185
+ information,
186
+ output,
187
+ origin: property,
188
+ prefix: prefixedKey,
189
+ reporting: parameters.reporting,
190
+ strict: parameters.strict,
191
+ },
192
+ get,
193
+ )
194
+ ) {
195
+ if (get) {
196
+ output[key] = clone(value);
197
+ }
198
+
170
199
  continue outer;
171
200
  }
172
201
  }
@@ -174,65 +203,76 @@ export function validateObject(
174
203
  if (information.length === 0) {
175
204
  information.push({
176
205
  value,
177
- key: {...key},
178
- message: getInvalidTypeMessage(property, value),
206
+ key: {
207
+ full: prefixedKey,
208
+ short: key,
209
+ },
210
+ message: getInvalidTypeMessage(prefixedKey, property.types, value),
179
211
  });
180
212
  }
181
213
 
182
- if (options.reporting.throw && validation == null) {
214
+ if (parameters.reporting.throw) {
183
215
  throw new ValidationError(information);
184
216
  }
185
217
 
186
- validation?.push(...information);
218
+ parameters?.information?.push(...information);
187
219
 
188
- if (options.reporting.all) {
220
+ if (parameters.reporting.all) {
189
221
  allInformation.push(...information);
190
222
 
191
223
  continue;
192
224
  }
193
225
 
194
- return options.reporting.none ? false : information;
226
+ return parameters.reporting.none ? undefined : information;
227
+ }
228
+
229
+ if (get) {
230
+ if (parameters.origin == null) {
231
+ parameters.output = output;
232
+ } else {
233
+ parameters.output[parameters.origin.key] = output;
234
+ }
195
235
  }
196
236
 
197
- return options.reporting.none || allInformation.length === 0 ? true : allInformation;
237
+ return parameters.reporting.none || allInformation.length === 0
238
+ ? parameters.output
239
+ : allInformation;
198
240
  }
199
241
 
200
242
  function validateSchematic(
201
- property: ValidatedProperty,
202
243
  schematic: Schematic<unknown>,
203
244
  value: unknown,
204
- options: ValidationOptionsExtended,
205
- validation: ValidationInformation[],
245
+ parameters: ValidationParameters,
246
+ get: boolean,
206
247
  ): boolean {
207
248
  const properties = schematicProperties.get(schematic)!;
208
249
 
209
- const result = validateObject(value, properties, options, property, validation);
250
+ const result = validateObject(value, properties, parameters, get);
210
251
 
211
- return typeof result === 'boolean' ? result : result.length === 0;
252
+ return result == null || Array.isArray(result) ? false : true;
212
253
  }
213
254
 
214
255
  function validateValue(
215
256
  type: ValidatedPropertyType,
216
- property: ValidatedProperty,
217
257
  value: unknown,
218
- options: ValidationOptionsExtended,
219
- validation: ValidationInformation[],
258
+ parameters: ValidationParameters,
259
+ get: boolean,
220
260
  ): boolean {
221
261
  switch (true) {
222
262
  case typeof type === 'function':
223
263
  return (type as GenericCallback)(value);
224
264
 
225
265
  case Array.isArray(type): {
226
- const validated = validateObject(value, type, options, property, validation);
266
+ const result = validateObject(value, type, parameters, get);
227
267
 
228
- return typeof validated === 'boolean' ? validated : false;
268
+ return result == null || Array.isArray(result) ? false : true;
229
269
  }
230
270
 
231
271
  case isSchematic(type):
232
- return validateSchematic(property, type, value, options, validation);
272
+ return validateSchematic(type, value, parameters, get);
233
273
 
234
274
  default:
235
- return validateNamed(property, type as ValueName, value, validation);
275
+ return validateNamed(type as ValueName, value, parameters);
236
276
  }
237
277
  }
238
278