@satoshibits/functional 1.0.2
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/README.md +242 -0
- package/dist/array-utils.d.mts +317 -0
- package/dist/array-utils.d.mts.map +1 -0
- package/dist/array-utils.mjs +370 -0
- package/dist/array-utils.mjs.map +1 -0
- package/dist/composition.d.mts +603 -0
- package/dist/composition.d.mts.map +1 -0
- package/dist/composition.mjs +516 -0
- package/dist/composition.mjs.map +1 -0
- package/dist/object-utils.d.mts +267 -0
- package/dist/object-utils.d.mts.map +1 -0
- package/dist/object-utils.mjs +258 -0
- package/dist/object-utils.mjs.map +1 -0
- package/dist/option.d.mts +622 -0
- package/dist/option.d.mts.map +1 -0
- package/dist/option.mjs +637 -0
- package/dist/option.mjs.map +1 -0
- package/dist/performance.d.mts +265 -0
- package/dist/performance.d.mts.map +1 -0
- package/dist/performance.mjs +453 -0
- package/dist/performance.mjs.map +1 -0
- package/dist/pipeline.d.mts +431 -0
- package/dist/pipeline.d.mts.map +1 -0
- package/dist/pipeline.mjs +460 -0
- package/dist/pipeline.mjs.map +1 -0
- package/dist/predicates.d.mts +722 -0
- package/dist/predicates.d.mts.map +1 -0
- package/dist/predicates.mjs +802 -0
- package/dist/predicates.mjs.map +1 -0
- package/dist/reader-result.d.mts +422 -0
- package/dist/reader-result.d.mts.map +1 -0
- package/dist/reader-result.mjs +758 -0
- package/dist/reader-result.mjs.map +1 -0
- package/dist/result.d.mts +684 -0
- package/dist/result.d.mts.map +1 -0
- package/dist/result.mjs +814 -0
- package/dist/result.mjs.map +1 -0
- package/dist/types.d.mts +439 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/types.mjs +191 -0
- package/dist/types.mjs.map +1 -0
- package/dist/validation.d.mts +622 -0
- package/dist/validation.d.mts.map +1 -0
- package/dist/validation.mjs +852 -0
- package/dist/validation.mjs.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module validation
|
|
4
|
+
* @description Functional validation utilities using Result types for composable,
|
|
5
|
+
* type-safe data validation. This module provides a rich set of validators and
|
|
6
|
+
* combinators for building complex validation schemas. Unlike traditional validation
|
|
7
|
+
* libraries that throw exceptions, all validators return Result types, making
|
|
8
|
+
* error handling explicit and composable. Validators can be combined, transformed,
|
|
9
|
+
* and reused to build sophisticated validation logic.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { Validation, validators, schema, ValidationError } from './validation.mts';
|
|
14
|
+
*
|
|
15
|
+
* // simple validators
|
|
16
|
+
* const validateAge = validators.number.between(0, 150);
|
|
17
|
+
* const validateEmail = validators.string.email();
|
|
18
|
+
*
|
|
19
|
+
* // combining validators
|
|
20
|
+
* const validatePassword = Validation.all(
|
|
21
|
+
* validators.string.minLength(8),
|
|
22
|
+
* validators.string.matches(/[A-Z]/, 'Must contain uppercase'),
|
|
23
|
+
* validators.string.matches(/[0-9]/, 'Must contain number')
|
|
24
|
+
* );
|
|
25
|
+
*
|
|
26
|
+
* // object validation schema
|
|
27
|
+
* const userSchema = schema({
|
|
28
|
+
* name: validators.string.nonEmpty(),
|
|
29
|
+
* email: validateEmail,
|
|
30
|
+
* age: Validation.optional(validateAge),
|
|
31
|
+
* password: validatePassword
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // using the validator
|
|
35
|
+
* const result = userSchema({
|
|
36
|
+
* name: 'John Doe',
|
|
37
|
+
* email: 'john@example.com',
|
|
38
|
+
* age: 30,
|
|
39
|
+
* password: 'SecurePass123'
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* if (result.success) {
|
|
43
|
+
* console.log('Valid user:', result.data);
|
|
44
|
+
* } else {
|
|
45
|
+
* console.error('Validation errors:', result.error.errors);
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @category Core
|
|
50
|
+
* @since 2025-07-03
|
|
51
|
+
*/
|
|
52
|
+
var __extends = (this && this.__extends) || (function () {
|
|
53
|
+
var extendStatics = function (d, b) {
|
|
54
|
+
extendStatics = Object.setPrototypeOf ||
|
|
55
|
+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
56
|
+
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
|
57
|
+
return extendStatics(d, b);
|
|
58
|
+
};
|
|
59
|
+
return function (d, b) {
|
|
60
|
+
if (typeof b !== "function" && b !== null)
|
|
61
|
+
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
|
62
|
+
extendStatics(d, b);
|
|
63
|
+
function __() { this.constructor = d; }
|
|
64
|
+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
65
|
+
};
|
|
66
|
+
})();
|
|
67
|
+
var __assign = (this && this.__assign) || function () {
|
|
68
|
+
__assign = Object.assign || function(t) {
|
|
69
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
70
|
+
s = arguments[i];
|
|
71
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
72
|
+
t[p] = s[p];
|
|
73
|
+
}
|
|
74
|
+
return t;
|
|
75
|
+
};
|
|
76
|
+
return __assign.apply(this, arguments);
|
|
77
|
+
};
|
|
78
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
79
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
80
|
+
if (ar || !(i in from)) {
|
|
81
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
82
|
+
ar[i] = from[i];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
86
|
+
};
|
|
87
|
+
import { Result } from "./result.mjs";
|
|
88
|
+
/**
|
|
89
|
+
* Custom validation error that can hold multiple error messages.
|
|
90
|
+
* @description Extends the standard Error class to accumulate multiple validation
|
|
91
|
+
* errors. This allows validators to collect all errors rather than failing on
|
|
92
|
+
* the first error, providing better user experience for form validation.
|
|
93
|
+
*
|
|
94
|
+
* @category Errors
|
|
95
|
+
* @example
|
|
96
|
+
* // Creating validation errors
|
|
97
|
+
* const error = new ValidationError(['Name is required', 'Email is invalid']);
|
|
98
|
+
* console.log(error.message); // "Validation failed: Name is required, Email is invalid"
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // Working with errors
|
|
102
|
+
* if (!result.success) {
|
|
103
|
+
* const validationError = result.error;
|
|
104
|
+
* console.log('First error:', validationError.firstError());
|
|
105
|
+
* console.log('All errors:', validationError.errors);
|
|
106
|
+
*
|
|
107
|
+
* if (validationError.hasError('Email is invalid')) {
|
|
108
|
+
* // Handle email error specifically
|
|
109
|
+
* }
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
* @since 2025-07-03
|
|
113
|
+
*/
|
|
114
|
+
var ValidationError = /** @class */ (function (_super) {
|
|
115
|
+
__extends(ValidationError, _super);
|
|
116
|
+
/**
|
|
117
|
+
* Creates a new ValidationError with the given error messages.
|
|
118
|
+
*
|
|
119
|
+
* @param {string[]} errors - Array of error messages
|
|
120
|
+
*/
|
|
121
|
+
function ValidationError(errors) {
|
|
122
|
+
var _this = _super.call(this, "Validation failed: ".concat(errors.join(", "))) || this;
|
|
123
|
+
_this.errors = errors;
|
|
124
|
+
_this.name = "ValidationError";
|
|
125
|
+
return _this;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Adds additional errors to this validation error.
|
|
129
|
+
* @description Creates a new ValidationError with the combined errors.
|
|
130
|
+
* The original error remains unchanged (immutable).
|
|
131
|
+
*
|
|
132
|
+
* @param {string[]} newErrors - Additional error messages to add
|
|
133
|
+
* @returns {ValidationError} A new ValidationError with all errors
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* const error1 = new ValidationError(['Name required']);
|
|
137
|
+
* const error2 = error1.addErrors(['Email invalid']);
|
|
138
|
+
* // error2.errors => ['Name required', 'Email invalid']
|
|
139
|
+
* // error1.errors => ['Name required'] (unchanged)
|
|
140
|
+
*/
|
|
141
|
+
ValidationError.prototype.addErrors = function (newErrors) {
|
|
142
|
+
return new ValidationError(__spreadArray(__spreadArray([], this.errors, true), newErrors, true));
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Checks if this error contains a specific error message.
|
|
146
|
+
* @description Useful for conditional error handling based on specific
|
|
147
|
+
* validation failures.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} error - The error message to check for
|
|
150
|
+
* @returns {boolean} True if the error message is present
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* if (!result.success && result.error.hasError('Email is invalid')) {
|
|
154
|
+
* showEmailHelp();
|
|
155
|
+
* }
|
|
156
|
+
*/
|
|
157
|
+
ValidationError.prototype.hasError = function (error) {
|
|
158
|
+
return this.errors.includes(error);
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Gets the first error message.
|
|
162
|
+
* @description Returns the first error in the list, useful when you only
|
|
163
|
+
* want to display one error at a time.
|
|
164
|
+
*
|
|
165
|
+
* @returns {string | undefined} The first error message or undefined if no errors
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* const firstError = validationError.firstError();
|
|
169
|
+
* if (firstError) {
|
|
170
|
+
* showToast(firstError);
|
|
171
|
+
* }
|
|
172
|
+
*/
|
|
173
|
+
ValidationError.prototype.firstError = function () {
|
|
174
|
+
return this.errors[0];
|
|
175
|
+
};
|
|
176
|
+
return ValidationError;
|
|
177
|
+
}(Error));
|
|
178
|
+
export { ValidationError };
|
|
179
|
+
/**
|
|
180
|
+
* Validation utilities for creating and composing validators.
|
|
181
|
+
* @description The main namespace for validation combinators and utilities.
|
|
182
|
+
* Provides methods for creating, combining, and transforming validators
|
|
183
|
+
* in a functional style.
|
|
184
|
+
*
|
|
185
|
+
* @category Utilities
|
|
186
|
+
* @since 2025-07-03
|
|
187
|
+
*/
|
|
188
|
+
export var Validation = {
|
|
189
|
+
/**
|
|
190
|
+
* Creates a validator that always succeeds.
|
|
191
|
+
* @description Useful as a default validator or when conditionally applying
|
|
192
|
+
* validation. The value passes through unchanged.
|
|
193
|
+
*
|
|
194
|
+
* @template T - The type of value
|
|
195
|
+
* @returns {Validator<T>} A validator that always returns success
|
|
196
|
+
*
|
|
197
|
+
* @category Constructors
|
|
198
|
+
* @example
|
|
199
|
+
* // Conditional validation
|
|
200
|
+
* const validator = shouldValidate
|
|
201
|
+
* ? validators.string.email()
|
|
202
|
+
* : Validation.success();
|
|
203
|
+
*
|
|
204
|
+
* @since 2025-07-03
|
|
205
|
+
*/
|
|
206
|
+
success: function () {
|
|
207
|
+
return function (value) {
|
|
208
|
+
return Result.ok(value);
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
/**
|
|
212
|
+
* Creates a validator that always fails with the given error.
|
|
213
|
+
* @description Useful for custom validation logic or placeholder validators
|
|
214
|
+
* during development.
|
|
215
|
+
*
|
|
216
|
+
* @template T - The type of value
|
|
217
|
+
* @param {string} error - The error message
|
|
218
|
+
* @returns {Validator<T>} A validator that always returns failure
|
|
219
|
+
*
|
|
220
|
+
* @category Constructors
|
|
221
|
+
* @example
|
|
222
|
+
* // Feature flag validation
|
|
223
|
+
* const validator = featureEnabled
|
|
224
|
+
* ? actualValidator
|
|
225
|
+
* : Validation.failure('Feature not available');
|
|
226
|
+
*
|
|
227
|
+
* @since 2025-07-03
|
|
228
|
+
*/
|
|
229
|
+
failure: function (error) {
|
|
230
|
+
return function () {
|
|
231
|
+
return Result.err(new ValidationError([error]));
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
/**
|
|
235
|
+
* Creates a validator from a predicate function.
|
|
236
|
+
* @description The fundamental building block for custom validators. Converts
|
|
237
|
+
* a boolean-returning function into a validator.
|
|
238
|
+
*
|
|
239
|
+
* @template T - The type of value to validate
|
|
240
|
+
* @param {function(T): boolean} predicate - Function that returns true if valid
|
|
241
|
+
* @param {string} error - Error message if validation fails
|
|
242
|
+
* @returns {Validator<T>} A validator based on the predicate
|
|
243
|
+
*
|
|
244
|
+
* @category Constructors
|
|
245
|
+
* @example
|
|
246
|
+
* // Custom age validator
|
|
247
|
+
* const isAdult = Validation.fromPredicate(
|
|
248
|
+
* (age: number) => age >= 18,
|
|
249
|
+
* 'Must be 18 or older'
|
|
250
|
+
* );
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* // Complex validation
|
|
254
|
+
* const isValidUsername = Validation.fromPredicate(
|
|
255
|
+
* (username: string) => /^[a-zA-Z0-9_]{3,20}$/.test(username),
|
|
256
|
+
* 'Username must be 3-20 characters, alphanumeric or underscore'
|
|
257
|
+
* );
|
|
258
|
+
*
|
|
259
|
+
* @since 2025-07-03
|
|
260
|
+
*/
|
|
261
|
+
fromPredicate: function (predicate, error) {
|
|
262
|
+
return function (value) {
|
|
263
|
+
return predicate(value)
|
|
264
|
+
? Result.ok(value)
|
|
265
|
+
: Result.err(new ValidationError([error]));
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
/**
|
|
269
|
+
* Combines multiple validators using AND logic.
|
|
270
|
+
* @description All validators must pass for the validation to succeed.
|
|
271
|
+
* Collects all errors from all validators before returning, providing
|
|
272
|
+
* comprehensive feedback.
|
|
273
|
+
*
|
|
274
|
+
* @template T - The type of value to validate
|
|
275
|
+
* @param {Validator<T>[]} validators - Validators to combine
|
|
276
|
+
* @returns {Validator<T>} A validator that requires all validations to pass
|
|
277
|
+
*
|
|
278
|
+
* @category Combinators
|
|
279
|
+
* @example
|
|
280
|
+
* // Password validation
|
|
281
|
+
* const validatePassword = Validation.all(
|
|
282
|
+
* validators.string.minLength(8),
|
|
283
|
+
* validators.string.matches(/[A-Z]/, 'Must contain uppercase'),
|
|
284
|
+
* validators.string.matches(/[0-9]/, 'Must contain number'),
|
|
285
|
+
* validators.string.matches(/[!@#$%]/, 'Must contain special character')
|
|
286
|
+
* );
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* // Numeric range validation
|
|
290
|
+
* const validatePercentage = Validation.all(
|
|
291
|
+
* validators.number.min(0),
|
|
292
|
+
* validators.number.max(100),
|
|
293
|
+
* validators.number.integer()
|
|
294
|
+
* );
|
|
295
|
+
*
|
|
296
|
+
* @since 2025-07-03
|
|
297
|
+
*/
|
|
298
|
+
all: function () {
|
|
299
|
+
var validators = [];
|
|
300
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
301
|
+
validators[_i] = arguments[_i];
|
|
302
|
+
}
|
|
303
|
+
return function (value) {
|
|
304
|
+
var errors = [];
|
|
305
|
+
for (var _i = 0, validators_1 = validators; _i < validators_1.length; _i++) {
|
|
306
|
+
var validator = validators_1[_i];
|
|
307
|
+
var result = validator(value);
|
|
308
|
+
if (!result.success) {
|
|
309
|
+
errors.push.apply(errors, result.error.errors);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return errors.length === 0
|
|
313
|
+
? Result.ok(value)
|
|
314
|
+
: Result.err(new ValidationError(errors));
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
/**
|
|
318
|
+
* Combines multiple validators using OR logic.
|
|
319
|
+
* @description At least one validator must pass for the validation to succeed.
|
|
320
|
+
* Returns the result of the first successful validator, or all errors if
|
|
321
|
+
* none succeed.
|
|
322
|
+
*
|
|
323
|
+
* @template T - The type of value to validate
|
|
324
|
+
* @param {Validator<T>[]} validators - Validators to try
|
|
325
|
+
* @returns {Validator<T>} A validator that requires at least one validation to pass
|
|
326
|
+
*
|
|
327
|
+
* @category Combinators
|
|
328
|
+
* @example
|
|
329
|
+
* // Multiple format support
|
|
330
|
+
* const validateDate = Validation.any(
|
|
331
|
+
* validators.string.matches(/^\d{4}-\d{2}-\d{2}$/, 'Invalid ISO date'),
|
|
332
|
+
* validators.string.matches(/^\d{2}\/\d{2}\/\d{4}$/, 'Invalid US date')
|
|
333
|
+
* );
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* // Flexible identifier
|
|
337
|
+
* const validateIdentifier = Validation.any(
|
|
338
|
+
* validators.string.matches(/^\d+$/, 'Not a numeric ID'),
|
|
339
|
+
* validators.string.email(),
|
|
340
|
+
* validators.string.matches(/^[A-Z]{2,}$/, 'Not a code')
|
|
341
|
+
* );
|
|
342
|
+
*
|
|
343
|
+
* @since 2025-07-03
|
|
344
|
+
*/
|
|
345
|
+
any: function () {
|
|
346
|
+
var validators = [];
|
|
347
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
348
|
+
validators[_i] = arguments[_i];
|
|
349
|
+
}
|
|
350
|
+
return function (value) {
|
|
351
|
+
var errors = [];
|
|
352
|
+
for (var _i = 0, validators_2 = validators; _i < validators_2.length; _i++) {
|
|
353
|
+
var validator = validators_2[_i];
|
|
354
|
+
var result = validator(value);
|
|
355
|
+
if (result.success) {
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
errors.push.apply(errors, result.error.errors);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return Result.err(new ValidationError(errors));
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
/**
|
|
366
|
+
* Transforms the validated value if validation passes.
|
|
367
|
+
* @description Note: The returned validator still takes an input of type T.
|
|
368
|
+
* This is the functor map operation for validators, allowing value transformation
|
|
369
|
+
* after successful validation.
|
|
370
|
+
*
|
|
371
|
+
* @template T - The input type
|
|
372
|
+
* @template U - The output type
|
|
373
|
+
* @param {function(T): U} fn - Function to transform the validated value
|
|
374
|
+
* @returns {function(Validator<T>): function(T): Result<U, ValidationError>} A function that transforms validators
|
|
375
|
+
*
|
|
376
|
+
* @category Transformations
|
|
377
|
+
* @example
|
|
378
|
+
* // Normalize email
|
|
379
|
+
* const normalizeEmail = Validation.map((email: string) => email.toLowerCase());
|
|
380
|
+
* const validateEmail = normalizeEmail(validators.string.email());
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* // Parse and validate
|
|
384
|
+
* const parseNumber = Validation.map((s: string) => parseInt(s, 10));
|
|
385
|
+
* const validateNumericString = parseNumber(
|
|
386
|
+
* validators.string.matches(/^\d+$/, 'Must be numeric')
|
|
387
|
+
* );
|
|
388
|
+
*
|
|
389
|
+
* @since 2025-07-03
|
|
390
|
+
*/
|
|
391
|
+
map: function (fn) {
|
|
392
|
+
return function (validator) {
|
|
393
|
+
return function (value) {
|
|
394
|
+
var result = validator(value);
|
|
395
|
+
return result.success ? Result.ok(fn(result.data)) : result;
|
|
396
|
+
};
|
|
397
|
+
};
|
|
398
|
+
},
|
|
399
|
+
/**
|
|
400
|
+
* Chains validators together.
|
|
401
|
+
* @description The choice of the second validator depends on the successful
|
|
402
|
+
* result of the first. This is the monadic bind operation for validators,
|
|
403
|
+
* enabling dynamic validation based on previous results.
|
|
404
|
+
*
|
|
405
|
+
* @template T - The type being validated
|
|
406
|
+
* @param {function(T): Validator<T>} fn - Function that returns the next validator
|
|
407
|
+
* @returns {function(Validator<T>): Validator<T>} A function that chains validators
|
|
408
|
+
*
|
|
409
|
+
* @category Combinators
|
|
410
|
+
* @example
|
|
411
|
+
* // Conditional validation based on value
|
|
412
|
+
* const validateScore = Validation.flatMap((score: number) => {
|
|
413
|
+
* if (score < 0) return Validation.failure('Negative scores not allowed');
|
|
414
|
+
* if (score > 100) return validators.number.max(200); // Allow bonus points
|
|
415
|
+
* return Validation.success();
|
|
416
|
+
* });
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* // Dynamic validation
|
|
420
|
+
* const validateField = Validation.flatMap((field: { type: string; value: any }) => {
|
|
421
|
+
* switch (field.type) {
|
|
422
|
+
* case 'email': return validators.string.email();
|
|
423
|
+
* case 'number': return validators.number.positive();
|
|
424
|
+
* default: return Validation.success();
|
|
425
|
+
* }
|
|
426
|
+
* });
|
|
427
|
+
*
|
|
428
|
+
* @since 2025-07-03
|
|
429
|
+
*/
|
|
430
|
+
flatMap: function (fn) {
|
|
431
|
+
return function (validator) {
|
|
432
|
+
return function (value) {
|
|
433
|
+
var result = validator(value);
|
|
434
|
+
if (result.success) {
|
|
435
|
+
// Get the next validator based on the validated data
|
|
436
|
+
var nextValidator = fn(result.data);
|
|
437
|
+
// Apply the next validator to the successfully validated data
|
|
438
|
+
return nextValidator(result.data);
|
|
439
|
+
}
|
|
440
|
+
// For error case, we return the original error
|
|
441
|
+
return result;
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
},
|
|
445
|
+
/**
|
|
446
|
+
* Validates an optional value.
|
|
447
|
+
* @description If the value is null or undefined, validation passes.
|
|
448
|
+
* Otherwise, applies the validator. Useful for optional form fields
|
|
449
|
+
* or nullable database columns.
|
|
450
|
+
*
|
|
451
|
+
* @template T - The type being validated
|
|
452
|
+
* @param {Validator<T>} validator - Validator to apply if value is present
|
|
453
|
+
* @returns {Validator<T | null | undefined>} A validator that handles optional values
|
|
454
|
+
*
|
|
455
|
+
* @category Modifiers
|
|
456
|
+
* @example
|
|
457
|
+
* // Optional email field
|
|
458
|
+
* const validateOptionalEmail = Validation.optional(
|
|
459
|
+
* validators.string.email()
|
|
460
|
+
* );
|
|
461
|
+
*
|
|
462
|
+
* validateOptionalEmail(null); // => { success: true, data: null }
|
|
463
|
+
* validateOptionalEmail('user@example.com'); // => validates as email
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* // In object schema
|
|
467
|
+
* const userSchema = schema({
|
|
468
|
+
* name: validators.string.nonEmpty(),
|
|
469
|
+
* email: validators.string.email(),
|
|
470
|
+
* phone: Validation.optional(validators.string.matches(/^\d{10}$/))
|
|
471
|
+
* });
|
|
472
|
+
*
|
|
473
|
+
* @since 2025-07-03
|
|
474
|
+
*/
|
|
475
|
+
optional: function (validator) {
|
|
476
|
+
return function (value) {
|
|
477
|
+
if (value === null || value === undefined) {
|
|
478
|
+
return Result.ok(value);
|
|
479
|
+
}
|
|
480
|
+
return validator(value);
|
|
481
|
+
};
|
|
482
|
+
},
|
|
483
|
+
/**
|
|
484
|
+
* Makes a validator required.
|
|
485
|
+
* @description Fails if value is null or undefined. This is a type guard
|
|
486
|
+
* that narrows T | null | undefined to T.
|
|
487
|
+
*
|
|
488
|
+
* @template T - The required type
|
|
489
|
+
* @param {string} error - Error message if value is missing
|
|
490
|
+
* @returns {Validator<T | null | undefined>} A validator that requires presence
|
|
491
|
+
*
|
|
492
|
+
* @category Modifiers
|
|
493
|
+
* @example
|
|
494
|
+
* // Basic usage
|
|
495
|
+
* const required = Validation.required<string>();
|
|
496
|
+
* required(null); // => { success: false, error: 'Value is required' }
|
|
497
|
+
* required('hello'); // => { success: true, data: 'hello' }
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* // Combining with other validators
|
|
501
|
+
* const validateName = Validation.all(
|
|
502
|
+
* Validation.required<string>('Name is required'),
|
|
503
|
+
* validators.string.minLength(2)
|
|
504
|
+
* );
|
|
505
|
+
*
|
|
506
|
+
* @since 2025-07-03
|
|
507
|
+
*/
|
|
508
|
+
required: function (error) {
|
|
509
|
+
if (error === void 0) { error = "Value is required"; }
|
|
510
|
+
return function (value) {
|
|
511
|
+
if (value === null || value === undefined) {
|
|
512
|
+
return Result.err(new ValidationError([error]));
|
|
513
|
+
}
|
|
514
|
+
return Result.ok(value);
|
|
515
|
+
};
|
|
516
|
+
},
|
|
517
|
+
/**
|
|
518
|
+
* Validates each item in an array.
|
|
519
|
+
* @description Applies the same validator to each element of an array,
|
|
520
|
+
* collecting all errors with their indices. Returns a new array with
|
|
521
|
+
* validated (and possibly transformed) items.
|
|
522
|
+
*
|
|
523
|
+
* @template T - The type of array elements
|
|
524
|
+
* @param {Validator<T>} itemValidator - Validator to apply to each item
|
|
525
|
+
* @returns {Validator<T[]>} A validator for arrays
|
|
526
|
+
*
|
|
527
|
+
* @category Collections
|
|
528
|
+
* @example
|
|
529
|
+
* // Validate array of emails
|
|
530
|
+
* const validateEmails = Validation.array(
|
|
531
|
+
* validators.string.email()
|
|
532
|
+
* );
|
|
533
|
+
*
|
|
534
|
+
* const result = validateEmails([
|
|
535
|
+
* 'user@example.com',
|
|
536
|
+
* 'invalid-email',
|
|
537
|
+
* 'admin@example.com'
|
|
538
|
+
* ]);
|
|
539
|
+
* // Error: "[1]: Invalid email format"
|
|
540
|
+
*
|
|
541
|
+
* @example
|
|
542
|
+
* // Complex item validation
|
|
543
|
+
* const validateUsers = Validation.array(
|
|
544
|
+
* schema({
|
|
545
|
+
* name: validators.string.nonEmpty(),
|
|
546
|
+
* age: validators.number.positive()
|
|
547
|
+
* })
|
|
548
|
+
* );
|
|
549
|
+
*
|
|
550
|
+
* @since 2025-07-03
|
|
551
|
+
*/
|
|
552
|
+
array: function (itemValidator) {
|
|
553
|
+
return function (values) {
|
|
554
|
+
var errors = [];
|
|
555
|
+
var validatedItems = [];
|
|
556
|
+
values.forEach(function (value, index) {
|
|
557
|
+
var result = itemValidator(value);
|
|
558
|
+
if (result.success) {
|
|
559
|
+
validatedItems.push(result.data);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
errors.push.apply(errors, result.error.errors.map(function (err) { return "[".concat(index, "]: ").concat(err); }));
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
return errors.length === 0
|
|
566
|
+
? Result.ok(validatedItems)
|
|
567
|
+
: Result.err(new ValidationError(errors));
|
|
568
|
+
};
|
|
569
|
+
},
|
|
570
|
+
/**
|
|
571
|
+
* Validates properties of an object.
|
|
572
|
+
* @description Validates specified properties of an object using individual
|
|
573
|
+
* validators. Unspecified properties are passed through unchanged. Errors
|
|
574
|
+
* are prefixed with the property name for clarity.
|
|
575
|
+
*
|
|
576
|
+
* @template T - The object type
|
|
577
|
+
* @param {object} validators - Map of property names to validators
|
|
578
|
+
* @returns {Validator<T>} A validator for the object
|
|
579
|
+
*
|
|
580
|
+
* @category Objects
|
|
581
|
+
* @example
|
|
582
|
+
* // Partial object validation
|
|
583
|
+
* const validatePerson = Validation.object({
|
|
584
|
+
* name: validators.string.nonEmpty(),
|
|
585
|
+
* age: validators.number.positive()
|
|
586
|
+
* });
|
|
587
|
+
*
|
|
588
|
+
* const result = validatePerson({
|
|
589
|
+
* name: 'John',
|
|
590
|
+
* age: -5,
|
|
591
|
+
* extra: 'ignored'
|
|
592
|
+
* });
|
|
593
|
+
* // Error: "age: Number must be positive"
|
|
594
|
+
*
|
|
595
|
+
* @see schema - Type-safe alternative for complete object validation
|
|
596
|
+
* @since 2025-07-03
|
|
597
|
+
*/
|
|
598
|
+
object:
|
|
599
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needed for flexible object validation
|
|
600
|
+
function (validators) {
|
|
601
|
+
return function (obj) {
|
|
602
|
+
var errors = [];
|
|
603
|
+
var validatedObj = null;
|
|
604
|
+
var _loop_1 = function (key, validator) {
|
|
605
|
+
if (validator) {
|
|
606
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type erasure from Object.entries
|
|
607
|
+
var result = validator(obj[key]);
|
|
608
|
+
if (!result.success) {
|
|
609
|
+
errors.push.apply(errors, result.error.errors.map(function (err) { return "".concat(key, ": ").concat(err); }));
|
|
610
|
+
}
|
|
611
|
+
else if (result.data !== obj[key]) {
|
|
612
|
+
// Lazily create a copy only if data is transformed
|
|
613
|
+
validatedObj !== null && validatedObj !== void 0 ? validatedObj : (validatedObj = __assign({}, obj));
|
|
614
|
+
validatedObj[key] = result.data;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
for (var _i = 0, _a = Object.entries(validators); _i < _a.length; _i++) {
|
|
619
|
+
var _b = _a[_i], key = _b[0], validator = _b[1];
|
|
620
|
+
_loop_1(key, validator);
|
|
621
|
+
}
|
|
622
|
+
return errors.length === 0
|
|
623
|
+
? Result.ok(validatedObj !== null && validatedObj !== void 0 ? validatedObj : obj)
|
|
624
|
+
: Result.err(new ValidationError(errors));
|
|
625
|
+
};
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* Common validators for primitive types.
|
|
630
|
+
* @description Pre-built validators for common validation scenarios.
|
|
631
|
+
* These validators can be used directly or combined with the Validation
|
|
632
|
+
* combinators to create more complex validation logic.
|
|
633
|
+
*
|
|
634
|
+
* @category Validators
|
|
635
|
+
* @example
|
|
636
|
+
* // Using validators directly
|
|
637
|
+
* const emailValidator = validators.string.email();
|
|
638
|
+
* const ageValidator = validators.number.between(0, 150);
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* // Combining validators
|
|
642
|
+
* const strongPassword = Validation.all(
|
|
643
|
+
* validators.string.minLength(12),
|
|
644
|
+
* validators.string.matches(/[A-Z]/, 'Need uppercase'),
|
|
645
|
+
* validators.string.matches(/[a-z]/, 'Need lowercase'),
|
|
646
|
+
* validators.string.matches(/[0-9]/, 'Need number')
|
|
647
|
+
* );
|
|
648
|
+
*
|
|
649
|
+
* @since 2025-07-03
|
|
650
|
+
*/
|
|
651
|
+
export var validators = {
|
|
652
|
+
/**
|
|
653
|
+
* String validators.
|
|
654
|
+
* @description Validators for string values including length checks,
|
|
655
|
+
* format validation, and pattern matching.
|
|
656
|
+
*
|
|
657
|
+
* @category String Validators
|
|
658
|
+
* @since 2025-07-03
|
|
659
|
+
*/
|
|
660
|
+
string: {
|
|
661
|
+
minLength: function (min) {
|
|
662
|
+
return Validation.fromPredicate(function (str) { return str.length >= min; }, "String must be at least ".concat(min, " characters long"));
|
|
663
|
+
},
|
|
664
|
+
maxLength: function (max) {
|
|
665
|
+
return Validation.fromPredicate(function (str) { return str.length <= max; }, "String must be at most ".concat(max, " characters long"));
|
|
666
|
+
},
|
|
667
|
+
nonEmpty: function () {
|
|
668
|
+
return Validation.fromPredicate(function (str) { return str.trim().length > 0; }, "String cannot be empty");
|
|
669
|
+
},
|
|
670
|
+
email: function () {
|
|
671
|
+
return Validation.fromPredicate(
|
|
672
|
+
// A more permissive regex, see https://emailregex.com/
|
|
673
|
+
function (str) { return /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(str); }, "Invalid email format");
|
|
674
|
+
},
|
|
675
|
+
url: function () {
|
|
676
|
+
return Validation.fromPredicate(function (str) {
|
|
677
|
+
try {
|
|
678
|
+
new URL(str);
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
catch (_a) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
}, "Invalid URL format");
|
|
685
|
+
},
|
|
686
|
+
matches: function (pattern, error) {
|
|
687
|
+
if (error === void 0) { error = "String does not match pattern"; }
|
|
688
|
+
return Validation.fromPredicate(function (str) { return pattern.test(str); }, error);
|
|
689
|
+
},
|
|
690
|
+
oneOf: function (options) {
|
|
691
|
+
return Validation.fromPredicate(function (str) { return options.includes(str); }, "String must be one of: ".concat(options.join(", ")));
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
/**
|
|
695
|
+
* Number validators.
|
|
696
|
+
*/
|
|
697
|
+
number: {
|
|
698
|
+
min: function (min) {
|
|
699
|
+
return Validation.fromPredicate(function (num) { return num >= min; }, "Number must be at least ".concat(min));
|
|
700
|
+
},
|
|
701
|
+
max: function (max) {
|
|
702
|
+
return Validation.fromPredicate(function (num) { return num <= max; }, "Number must be at most ".concat(max));
|
|
703
|
+
},
|
|
704
|
+
positive: function () {
|
|
705
|
+
return Validation.fromPredicate(function (num) { return num > 0; }, "Number must be positive");
|
|
706
|
+
},
|
|
707
|
+
nonNegative: function () {
|
|
708
|
+
return Validation.fromPredicate(function (num) { return num >= 0; }, "Number must be non-negative");
|
|
709
|
+
},
|
|
710
|
+
integer: function () {
|
|
711
|
+
return Validation.fromPredicate(function (num) { return Number.isInteger(num); }, "Number must be an integer");
|
|
712
|
+
},
|
|
713
|
+
between: function (min, max) {
|
|
714
|
+
return Validation.fromPredicate(function (num) { return num >= min && num <= max; }, "Number must be between ".concat(min, " and ").concat(max));
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
/**
|
|
718
|
+
* Array validators.
|
|
719
|
+
*/
|
|
720
|
+
array: {
|
|
721
|
+
minLength: function (min) {
|
|
722
|
+
return Validation.fromPredicate(function (arr) { return arr.length >= min; }, "Array must have at least ".concat(min, " items"));
|
|
723
|
+
},
|
|
724
|
+
maxLength: function (max) {
|
|
725
|
+
return Validation.fromPredicate(function (arr) { return arr.length <= max; }, "Array must have at most ".concat(max, " items"));
|
|
726
|
+
},
|
|
727
|
+
nonEmpty: function () {
|
|
728
|
+
return Validation.fromPredicate(function (arr) { return arr.length > 0; }, "Array cannot be empty");
|
|
729
|
+
},
|
|
730
|
+
unique: function () {
|
|
731
|
+
return Validation.fromPredicate(function (arr) { return new Set(arr).size === arr.length; }, "Array must contain unique items");
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
/**
|
|
735
|
+
* Date validators.
|
|
736
|
+
*/
|
|
737
|
+
date: {
|
|
738
|
+
after: function (date) {
|
|
739
|
+
return Validation.fromPredicate(function (d) { return d > date; }, "Date must be after ".concat(date.toISOString()));
|
|
740
|
+
},
|
|
741
|
+
before: function (date) {
|
|
742
|
+
return Validation.fromPredicate(function (d) { return d < date; }, "Date must be before ".concat(date.toISOString()));
|
|
743
|
+
},
|
|
744
|
+
future: function () {
|
|
745
|
+
return Validation.fromPredicate(function (d) { return d > new Date(); }, "Date must be in the future");
|
|
746
|
+
},
|
|
747
|
+
past: function () {
|
|
748
|
+
return Validation.fromPredicate(function (d) { return d < new Date(); }, "Date must be in the past");
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
/**
|
|
752
|
+
* Object validators.
|
|
753
|
+
*/
|
|
754
|
+
object: {
|
|
755
|
+
hasProperty: function (prop) {
|
|
756
|
+
return Validation.fromPredicate(function (obj) { return prop in obj; }, "Object must have property '".concat(String(prop), "'"));
|
|
757
|
+
},
|
|
758
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Flexible for any object type
|
|
759
|
+
notEmpty: function () {
|
|
760
|
+
return Validation.fromPredicate(
|
|
761
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Matches return type
|
|
762
|
+
function (obj) { return Object.keys(obj).length > 0; }, "Object cannot be empty");
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
/**
|
|
767
|
+
* Utility for creating complex validation schemas.
|
|
768
|
+
* @description Type-safe wrapper around Validation.object that requires
|
|
769
|
+
* validators for all properties of the type. This ensures complete
|
|
770
|
+
* validation coverage for object types.
|
|
771
|
+
*
|
|
772
|
+
* @template T - The object type to validate
|
|
773
|
+
* @param {object} validators - Validators for each property of T
|
|
774
|
+
* @returns {Validator<T>} A validator for objects of type T
|
|
775
|
+
*
|
|
776
|
+
* @category Schema
|
|
777
|
+
* @example
|
|
778
|
+
* // Type-safe user validation
|
|
779
|
+
* interface User {
|
|
780
|
+
* name: string;
|
|
781
|
+
* email: string;
|
|
782
|
+
* age: number;
|
|
783
|
+
* }
|
|
784
|
+
*
|
|
785
|
+
* const userValidator = schema<User>({
|
|
786
|
+
* name: validators.string.nonEmpty(),
|
|
787
|
+
* email: validators.string.email(),
|
|
788
|
+
* age: validators.number.between(0, 150)
|
|
789
|
+
* });
|
|
790
|
+
*
|
|
791
|
+
* @example
|
|
792
|
+
* // Nested schemas
|
|
793
|
+
* const addressValidator = schema({
|
|
794
|
+
* street: validators.string.nonEmpty(),
|
|
795
|
+
* city: validators.string.nonEmpty(),
|
|
796
|
+
* zipCode: validators.string.matches(/^\d{5}$/)
|
|
797
|
+
* });
|
|
798
|
+
*
|
|
799
|
+
* const personValidator = schema({
|
|
800
|
+
* name: validators.string.nonEmpty(),
|
|
801
|
+
* address: addressValidator
|
|
802
|
+
* });
|
|
803
|
+
*
|
|
804
|
+
* @since 2025-07-03
|
|
805
|
+
*/
|
|
806
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needed for flexible object validation
|
|
807
|
+
export var schema = function (validators) { return Validation.object(validators); };
|
|
808
|
+
/**
|
|
809
|
+
* Validates a value and returns either the validated value or throws the error.
|
|
810
|
+
* @description Use sparingly, only when you're certain validation should pass.
|
|
811
|
+
* This bridges the Result-based validation system with exception-based code.
|
|
812
|
+
*
|
|
813
|
+
* @template T - The type being validated
|
|
814
|
+
* @param {Validator<T>} validator - The validator to apply
|
|
815
|
+
* @returns {function(T): T} A function that validates or throws
|
|
816
|
+
* @throws {ValidationError} If validation fails
|
|
817
|
+
*
|
|
818
|
+
* @category Utilities
|
|
819
|
+
* @example
|
|
820
|
+
* // Configuration validation at startup
|
|
821
|
+
* const validateConfig = validateOrThrow(
|
|
822
|
+
* schema({
|
|
823
|
+
* port: validators.number.between(1, 65535),
|
|
824
|
+
* host: validators.string.nonEmpty()
|
|
825
|
+
* })
|
|
826
|
+
* );
|
|
827
|
+
*
|
|
828
|
+
* // Throws if invalid, otherwise returns validated config
|
|
829
|
+
* const config = validateConfig(loadConfig());
|
|
830
|
+
*
|
|
831
|
+
* @example
|
|
832
|
+
* // Input sanitization
|
|
833
|
+
* const sanitizeEmail = validateOrThrow(
|
|
834
|
+
* Validation.map((s: string) => s.toLowerCase())(
|
|
835
|
+
* validators.string.email()
|
|
836
|
+
* )
|
|
837
|
+
* );
|
|
838
|
+
*
|
|
839
|
+
* @since 2025-07-03
|
|
840
|
+
*/
|
|
841
|
+
export var validateOrThrow = function (validator) {
|
|
842
|
+
return function (value) {
|
|
843
|
+
var result = validator(value);
|
|
844
|
+
if (result.success) {
|
|
845
|
+
return result.data;
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
throw result.error;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
//# sourceMappingURL=validation.mjs.map
|