UNPKG

6.4 kBJavaScriptView Raw
1import is from '@sindresorhus/is';
2import { ArgumentError } from '../argument-error.js';
3import { not } from '../operators/not.js';
4import { generateArgumentErrorMessage } from '../utils/generate-argument-error-message.js';
5import { testSymbol } from './base-predicate.js';
6/**
7@hidden
8*/
9export const validatorSymbol = Symbol('validators');
10/**
11@hidden
12*/
13export class Predicate {
14 type;
15 options;
16 context = {
17 validators: [],
18 };
19 constructor(type, options = {}) {
20 this.type = type;
21 this.options = options;
22 this.context = {
23 ...this.context,
24 ...this.options,
25 };
26 const typeString = this.type.charAt(0).toLowerCase() + this.type.slice(1);
27 this.addValidator({
28 message: (value, label) => {
29 // We do not include type in this label as we do for other messages, because it would be redundant.
30 const label_ = label?.slice(this.type.length + 1);
31 // TODO: The NaN check can be removed when `@sindresorhus/is` is fixed: https://github.com/sindresorhus/ow/issues/231#issuecomment-1047100612
32 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
33 return `Expected ${label_ || 'argument'} to be of type \`${this.type}\` but received type \`${Number.isNaN(value) ? 'NaN' : is(value)}\``;
34 },
35 // eslint-disable-next-line @typescript-eslint/no-unsafe-call
36 validator: value => is[typeString](value),
37 });
38 }
39 /**
40 @hidden
41 */
42 [testSymbol](value, main, label, idLabel) {
43 // Create a map of labels -> received errors.
44 const errors = new Map();
45 for (const { validator, message } of this.context.validators) {
46 if (this.options.optional === true && value === undefined) {
47 continue;
48 }
49 let result;
50 try {
51 result = validator(value);
52 }
53 catch (error) {
54 // Any errors caught means validators couldn't process the input.
55 result = error;
56 }
57 if (result === true) {
58 continue;
59 }
60 const label2 = is.function_(label) ? label() : label;
61 const labelWithTick = (label2 && idLabel) ? `\`${label2}\`` : label2;
62 const label_ = labelWithTick
63 ? `${this.type} ${labelWithTick}`
64 : this.type;
65 const mapKey = label2 || this.type;
66 // Get the current errors encountered for this label.
67 const currentErrors = errors.get(mapKey);
68 // Pre-generate the error message that will be reported to the user.
69 const errorMessage = message(value, label_, result);
70 // If we already have any errors for this label.
71 if (currentErrors) {
72 // If we don't already have this error logged, add it.
73 currentErrors.add(errorMessage);
74 }
75 else {
76 // Set this label and error in the full map.
77 errors.set(mapKey, new Set([errorMessage]));
78 }
79 }
80 // If we have any errors to report, throw.
81 if (errors.size > 0) {
82 // Generate the `error.message` property.
83 const message = generateArgumentErrorMessage(errors);
84 throw new ArgumentError(message, main, errors);
85 }
86 }
87 /**
88 @hidden
89 */
90 get [validatorSymbol]() {
91 return this.context.validators;
92 }
93 /**
94 Invert the following validators.
95 */
96 get not() {
97 return not(this);
98 }
99 /**
100 Test if the value matches a custom validation function. The validation function should return an object containing a `validator` and `message`. If the `validator` is `false`, the validation fails and the `message` will be used as error message. If the `message` is a function, the function is invoked with the `label` as argument to let you further customize the error message.
101
102 @param customValidator - Custom validation function.
103 */
104 validate(customValidator) {
105 return this.addValidator({
106 message: (_, label, error) => typeof error === 'string'
107 ? `(${label}) ${error}`
108 // eslint-disable-next-line @typescript-eslint/no-unsafe-call
109 : error(label),
110 validator(value) {
111 const { message, validator } = customValidator(value);
112 if (validator) {
113 return true;
114 }
115 return message;
116 },
117 });
118 }
119 /**
120 Test if the value matches a custom validation function. The validation function should return `true` if the value passes the function. If the function either returns `false` or a string, the function fails and the string will be used as error message.
121
122 @param validator - Validation function.
123 */
124 is(validator) {
125 return this.addValidator({
126 message: (value, label, error) => (error
127 ? `(${label}) ${error}`
128 : `Expected ${label} \`${value}\` to pass custom validation function`),
129 validator,
130 });
131 }
132 /**
133 Provide a new error message to be thrown when the validation fails.
134
135 @param newMessage - Either a string containing the new message or a function returning the new message.
136
137 @example
138 ```
139 ow('🌈', 'unicorn', ow.string.equals('🦄').message('Expected unicorn, got rainbow'));
140 //=> ArgumentError: Expected unicorn, got rainbow
141 ```
142
143 @example
144 ```
145 ow('🌈', ow.string.minLength(5).message((value, label) => `Expected ${label}, to have a minimum length of 5, got \`${value}\``));
146 //=> ArgumentError: Expected string, to be have a minimum length of 5, got `🌈`
147 ```
148 */
149 message(newMessage) {
150 const { validators } = this.context;
151 validators.at(-1).message = (value, label) => {
152 if (typeof newMessage === 'function') {
153 return newMessage(value, label);
154 }
155 return newMessage;
156 };
157 return this;
158 }
159 /**
160 Register a new validator.
161
162 @param validator - Validator to register.
163 */
164 addValidator(validator) {
165 this.context.validators.push(validator);
166 return this;
167 }
168}