@oscarpalmer/jhunal 0.19.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.
@@ -6,7 +6,7 @@ import type {TypedSchema} from './schema.typed.model';
6
6
  /**
7
7
  * Maps each element of a tuple through {@link ToValueType}
8
8
  *
9
- * @template Value - Tuple of types to map
9
+ * @template Value Tuple of types to map
10
10
  */
11
11
  export type MapToValueTypes<Value extends unknown[]> = Value extends [infer Head, ...infer Tail]
12
12
  ? [ToValueType<Head>, ...MapToValueTypes<Tail>]
@@ -15,7 +15,7 @@ export type MapToValueTypes<Value extends unknown[]> = Value extends [infer Head
15
15
  /**
16
16
  * Maps each element of a tuple through {@link ToSchemaPropertyTypeEach}
17
17
  *
18
- * @template Value - Tuple of types to map
18
+ * @template Value Tuple of types to map
19
19
  */
20
20
  export type MapToSchemaPropertyTypes<Value extends unknown[]> = Value extends [
21
21
  infer Head,
@@ -25,11 +25,9 @@ export type MapToSchemaPropertyTypes<Value extends unknown[]> = Value extends [
25
25
  : [];
26
26
 
27
27
  /**
28
- * Converts a type into its corresponding {@link SchemaPropertyType}-representation
28
+ * Converts a TypeScript type to its {@link SchemaPropertyType} representation, suitable for use in a typed schema
29
29
  *
30
- * Deduplicates and unwraps single-element tuples via {@link UnwrapSingle}
31
- *
32
- * @template Value - type to convert
30
+ * @template Value Type to convert
33
31
  */
34
32
  export type ToSchemaPropertyType<Value> = UnwrapSingle<
35
33
  DeduplicateTuple<MapToSchemaPropertyTypes<UnionToTuple<Value>>>
@@ -38,20 +36,18 @@ export type ToSchemaPropertyType<Value> = UnwrapSingle<
38
36
  /**
39
37
  * Converts a single type to its schema property equivalent
40
38
  *
41
- * {@link NestedSchema} values have `$required` stripped, plain objects become {@link TypedSchema}, and primitives go through {@link ToValueType}
39
+ * Plain objects become {@link TypedSchema}; primitives go through {@link ToValueType}
42
40
  *
43
- * @template Value - type to convert
41
+ * @template Value Type to convert
44
42
  */
45
43
  export type ToSchemaPropertyTypeEach<Value> = Value extends PlainObject
46
44
  ? TypedSchema<Value>
47
45
  : ToValueType<Value>;
48
46
 
49
47
  /**
50
- * Converts a type into its corresponding {@link ValueName}-representation
51
- *
52
- * Deduplicates and unwraps single-element tuples via {@link UnwrapSingle}
48
+ * Converts a TypeScript type to its {@link ValueName} representation, suitable for use as a top-level schema entry
53
49
  *
54
- * @template Value - type to convert
50
+ * @template Value Type to convert
55
51
  */
56
52
  export type ToSchemaType<Value> = UnwrapSingle<
57
53
  DeduplicateTuple<MapToValueTypes<UnionToTuple<Value>>>
@@ -62,7 +58,7 @@ export type ToSchemaType<Value> = UnwrapSingle<
62
58
  *
63
59
  * Resolves {@link Schematic} types as-is, then performs a reverse-lookup against {@link Values} _(excluding `'object'`)_ to find a matching key. If no match is found, `object` types resolve to `'object'` or a type-guard function, and all other unrecognised types resolve to a type-guard function
64
60
  *
65
- * @template Value - type to map
61
+ * @template Value Type to map
66
62
  *
67
63
  * @example
68
64
  * ```ts
@@ -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';
@@ -6,8 +6,21 @@ import type {ValueName} from './misc.model';
6
6
 
7
7
  // #region Reporting
8
8
 
9
- export type ReportingInformation = Record<ReportingType, boolean>;
9
+ /**
10
+ * Maps each {@link ReportingType} to a boolean flag
11
+ */
12
+ export type ReportingInformation = Record<ReportingType, boolean> & {
13
+ type: ReportingType;
14
+ };
10
15
 
16
+ /**
17
+ * Controls how validation failures are reported
18
+ *
19
+ * - `'none'` — returns a boolean _(default)_
20
+ * - `'first'` — returns the first failure as a `Result`
21
+ * - `'all'` — returns all failures as a `Result` _(from same level)_
22
+ * - `'throw'` — throws a {@link ValidationError} on failure
23
+ */
11
24
  export type ReportingType = 'all' | 'first' | 'none' | 'throw';
12
25
 
13
26
  // #endregion
@@ -15,7 +28,7 @@ export type ReportingType = 'all' | 'first' | 'none' | 'throw';
15
28
  // #region Schematic validation
16
29
 
17
30
  /**
18
- * A custom error class for schematic validation failures
31
+ * Thrown when a schema definition is invalid
19
32
  */
20
33
  export class SchematicError extends Error {
21
34
  constructor(message: string) {
@@ -46,7 +59,7 @@ export type ValidatedProperty = {
46
59
  /**
47
60
  * The property name in the schema
48
61
  */
49
- key: ValidatedPropertyKey;
62
+ key: string;
50
63
  /**
51
64
  * Whether the property is required
52
65
  */
@@ -61,20 +74,6 @@ export type ValidatedProperty = {
61
74
  validators: ValidatedPropertyValidators;
62
75
  };
63
76
 
64
- /**
65
- * Property name in schema
66
- */
67
- export type ValidatedPropertyKey = {
68
- /**
69
- * Full property key, including parent keys for nested properties _(e.g., `address.street`)_
70
- */
71
- full: string;
72
- /**
73
- * The last segment of the property key _(e.g., `street` for `address.street`)_
74
- */
75
- short: string;
76
- };
77
-
78
77
  /**
79
78
  * A union of valid types for a {@link ValidatedProperty}'s `types` array
80
79
  *
@@ -99,6 +98,9 @@ export type ValidatedPropertyValidators = {
99
98
 
100
99
  // #region Property validation
101
100
 
101
+ /**
102
+ * Thrown in `'throw'` mode when one or more properties fail validation; `information` holds all failures
103
+ */
102
104
  export class ValidationError extends Error {
103
105
  constructor(readonly information: ValidationInformation[]) {
104
106
  super(
@@ -112,13 +114,49 @@ export class ValidationError extends Error {
112
114
  }
113
115
  }
114
116
 
117
+ /**
118
+ * Describes a single validation failure
119
+ */
115
120
  export type ValidationInformation = {
121
+ /** The key path of the property that failed */
116
122
  key: ValidationInformationKey;
123
+ /** Human-readable description of the failure */
117
124
  message: string;
125
+ /** The validator function that failed, if the failure was from a `$validators` entry */
118
126
  validator?: GenericCallback;
127
+ /** The value that was provided */
119
128
  value: unknown;
120
129
  };
121
130
 
122
- export type ValidationInformationKey = ValidatedPropertyKey;
131
+ /**
132
+ *
133
+ */
134
+ export type ValidationInformationKey = {
135
+ full: string;
136
+ short: string;
137
+ };
138
+
139
+ /**
140
+ * Options for validation
141
+ */
142
+ export type ValidationOptions<Errors extends ReportingType> = {
143
+ /**
144
+ * How should validation failures be reported; see {@link ReportingType} _(defaults to `'none'`)_
145
+ */
146
+ errors?: Errors;
147
+ /**
148
+ * Validate if unknown keys are present in the object? _(defaults to `false`)_
149
+ */
150
+ strict?: boolean;
151
+ };
152
+
153
+ export type ValidationParameters = {
154
+ information?: ValidationInformation[];
155
+ origin?: ValidatedProperty;
156
+ output: PlainObject;
157
+ prefix?: string;
158
+ reporting: ReportingInformation;
159
+ strict: boolean;
160
+ };
123
161
 
124
162
  // #endregion
package/src/schematic.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import {isPlainObject} from '@oscarpalmer/atoms/is';
2
2
  import type {PlainObject} from '@oscarpalmer/atoms/models';
3
+ import {error, ok} from '@oscarpalmer/atoms/result/misc';
3
4
  import type {Result} from '@oscarpalmer/atoms/result/models';
4
5
  import {PROPERTY_SCHEMATIC, SCHEMATIC_MESSAGE_SCHEMA_INVALID_TYPE} from './constants';
5
- import {getReporting, isSchematic} from './helpers';
6
+ import {getParameters, isSchematic} from './helpers';
6
7
  import type {Infer} from './models/infer.model';
7
8
  import type {Schema} from './models/schema.plain.model';
8
9
  import type {TypedSchema} from './models/schema.typed.model';
@@ -10,10 +11,10 @@ import {
10
11
  SchematicError,
11
12
  type ValidatedProperty,
12
13
  type ValidationInformation,
14
+ type ValidationOptions,
13
15
  } from './models/validation.model';
14
16
  import {getProperties} from './validation/property.validation';
15
17
  import {validateObject} from './validation/value.validation';
16
- import {error} from '@oscarpalmer/atoms/result/misc';
17
18
 
18
19
  /**
19
20
  * A schematic for validating objects
@@ -29,14 +30,112 @@ export class Schematic<Model> {
29
30
  });
30
31
 
31
32
  this.#properties = properties;
33
+
34
+ schematicProperties.set(this, properties);
35
+ }
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]);
32
121
  }
33
122
 
34
123
  /**
35
124
  * Does the value match the schema?
36
125
  *
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.
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
38
127
  * @param value Value to validate
39
- * @param errors Throws an error for the first validation failure
128
+ * @param options Validation options
129
+ * @returns `true` if the value matches the schema, otherwise throws an error
130
+ */
131
+ is(value: unknown, options: ValidationOptions<'throw'>): asserts value is Model;
132
+
133
+ /**
134
+ * Does the value match the schema?
135
+ *
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
137
+ * @param value Value to validate
138
+ * @param errors Reporting type
40
139
  * @returns `true` if the value matches the schema, otherwise throws an error
41
140
  */
42
141
  is(value: unknown, errors: 'throw'): asserts value is Model;
@@ -44,19 +143,39 @@ export class Schematic<Model> {
44
143
  /**
45
144
  * Does the value match the schema?
46
145
  *
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.
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
48
147
  * @param value Value to validate
49
- * @param errors All
50
- * @returns `true` if the value matches the schema, otherwise `false`
148
+ * @param options Validation options
149
+ * @returns Result holding `true` or all validation information
150
+ */
151
+ is(value: unknown, options: ValidationOptions<'all'>): Result<true, ValidationInformation[]>;
152
+
153
+ /**
154
+ * Does the value match the schema?
155
+ *
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
157
+ * @param value Value to validate
158
+ * @param errors Reporting type
159
+ * @returns Result holding `true` or all validation information
51
160
  */
52
161
  is(value: unknown, errors: 'all'): Result<true, ValidationInformation[]>;
53
162
 
54
163
  /**
55
164
  * Does the value match the schema?
56
165
  *
57
- * 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
58
167
  * @param value Value to validate
59
- * @param errors First
168
+ * @param options Validation options
169
+ * @returns `true` if the value matches the schema, otherwise `false`
170
+ */
171
+ is(value: unknown, options: ValidationOptions<'first'>): Result<true, ValidationInformation>;
172
+
173
+ /**
174
+ * Does the value match the schema?
175
+ *
176
+ * Will validate that the value matches the schema and return a result of `true` or all validation information for the first failing property
177
+ * @param value Value to validate
178
+ * @param errors Reporting type
60
179
  * @returns `true` if the value matches the schema, otherwise `false`
61
180
  */
62
181
  is(value: unknown, errors: 'first'): Result<true, ValidationInformation>;
@@ -64,22 +183,27 @@ export class Schematic<Model> {
64
183
  /**
65
184
  * Does the value match the schema?
66
185
  *
67
- * 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
68
187
  * @param value Value to validate
188
+ * @param strict Validate if unknown keys are present in the object? _(defaults to `false`)_
69
189
  * @returns `true` if the value matches the schema, otherwise `false`
70
190
  */
71
- is(value: unknown): value is Model;
191
+ is(value: unknown, strict?: true): value is Model;
72
192
 
73
- is(value: unknown, errors?: unknown): unknown {
74
- const reporting = getReporting(errors);
193
+ is(value: unknown, options?: unknown): unknown {
194
+ const parameters = getParameters(options);
75
195
 
76
- const result = validateObject(value, this.#properties, reporting);
196
+ const result = validateObject(value, this.#properties, parameters, false);
77
197
 
78
- if (typeof result === 'boolean') {
79
- return result;
198
+ if (result == null) {
199
+ return false;
80
200
  }
81
201
 
82
- return error(reporting.all ? result : result[0]);
202
+ if (!Array.isArray(result)) {
203
+ return parameters.reporting.none ? true : ok(true);
204
+ }
205
+
206
+ return error(parameters.reporting.all ? result : result[0]);
83
207
  }
84
208
  }
85
209
 
@@ -112,3 +236,5 @@ export function schematic<Model extends Schema>(schema: Model): Schematic<Model>
112
236
 
113
237
  return new Schematic<Model>(getProperties(schema));
114
238
  }
239
+
240
+ export const schematicProperties = new WeakMap<Schematic<unknown>, ValidatedProperty[]>();
@@ -2,6 +2,9 @@ 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
+ PROPERTY_REQUIRED,
6
+ PROPERTY_TYPE,
7
+ PROPERTY_VALIDATORS,
5
8
  SCHEMATIC_MESSAGE_SCHEMA_INVALID_EMPTY,
6
9
  SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_DISALLOWED,
7
10
  SCHEMATIC_MESSAGE_SCHEMA_INVALID_PROPERTY_NULLABLE,
@@ -10,12 +13,8 @@ import {
10
13
  SCHEMATIC_MESSAGE_VALIDATOR_INVALID_KEY,
11
14
  SCHEMATIC_MESSAGE_VALIDATOR_INVALID_TYPE,
12
15
  SCHEMATIC_MESSAGE_VALIDATOR_INVALID_VALUE,
13
- PROPERTY_REQUIRED,
14
- PROPERTY_TYPE,
15
- PROPERTY_VALIDATORS,
16
16
  TEMPLATE_PATTERN,
17
17
  TYPE_ALL,
18
- TYPE_OBJECT,
19
18
  TYPE_UNDEFINED,
20
19
  VALIDATABLE_TYPES,
21
20
  } from '../constants';
@@ -71,13 +70,14 @@ export function getProperties(
71
70
 
72
71
  for (let keyIndex = 0; keyIndex < keysLength; keyIndex += 1) {
73
72
  const key = keys[keyIndex];
74
-
75
- const prefixed = join([prefix, key], '.');
76
73
  const value = original[key];
77
74
 
78
75
  if (value == null) {
79
76
  throw new SchematicError(
80
- 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
+ ),
81
81
  );
82
82
  }
83
83
 
@@ -102,12 +102,9 @@ export function getProperties(
102
102
  }
103
103
 
104
104
  properties.push({
105
+ key,
105
106
  types,
106
107
  validators,
107
- key: {
108
- full: prefixed,
109
- short: key,
110
- },
111
108
  required: required && !types.includes(TYPE_UNDEFINED),
112
109
  });
113
110
  }