@4riders/reform 3.0.24

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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +266 -0
  3. package/dist/index.d.ts +2715 -0
  4. package/dist/index.es.js +1715 -0
  5. package/dist/index.es.js.map +1 -0
  6. package/package.json +70 -0
  7. package/src/index.ts +90 -0
  8. package/src/reform/ArrayHelper.ts +164 -0
  9. package/src/reform/Form.tsx +81 -0
  10. package/src/reform/FormManager.ts +494 -0
  11. package/src/reform/Reform.ts +15 -0
  12. package/src/reform/components/BaseCheckboxField.tsx +72 -0
  13. package/src/reform/components/BaseDateField.tsx +84 -0
  14. package/src/reform/components/BaseRadioField.tsx +72 -0
  15. package/src/reform/components/BaseSelectField.tsx +103 -0
  16. package/src/reform/components/BaseTextAreaField.tsx +87 -0
  17. package/src/reform/components/BaseTextField.tsx +135 -0
  18. package/src/reform/components/InputHTMLProps.tsx +89 -0
  19. package/src/reform/observers/observer.ts +131 -0
  20. package/src/reform/observers/observerPath.ts +327 -0
  21. package/src/reform/observers/useObservers.ts +232 -0
  22. package/src/reform/useForm.ts +246 -0
  23. package/src/reform/useFormContext.tsx +37 -0
  24. package/src/reform/useFormField.ts +75 -0
  25. package/src/reform/useRender.ts +12 -0
  26. package/src/yop/MessageProvider.ts +204 -0
  27. package/src/yop/Metadata.ts +304 -0
  28. package/src/yop/ObjectsUtil.ts +811 -0
  29. package/src/yop/TypesUtil.ts +148 -0
  30. package/src/yop/ValidationContext.ts +207 -0
  31. package/src/yop/Yop.ts +430 -0
  32. package/src/yop/constraints/CommonConstraints.ts +124 -0
  33. package/src/yop/constraints/Constraint.ts +135 -0
  34. package/src/yop/constraints/MinMaxConstraints.ts +53 -0
  35. package/src/yop/constraints/OneOfConstraint.ts +40 -0
  36. package/src/yop/constraints/TestConstraint.ts +176 -0
  37. package/src/yop/decorators/array.ts +157 -0
  38. package/src/yop/decorators/boolean.ts +69 -0
  39. package/src/yop/decorators/date.ts +73 -0
  40. package/src/yop/decorators/email.ts +66 -0
  41. package/src/yop/decorators/file.ts +69 -0
  42. package/src/yop/decorators/id.ts +35 -0
  43. package/src/yop/decorators/ignored.ts +40 -0
  44. package/src/yop/decorators/instance.ts +110 -0
  45. package/src/yop/decorators/number.ts +73 -0
  46. package/src/yop/decorators/string.ts +90 -0
  47. package/src/yop/decorators/test.ts +41 -0
  48. package/src/yop/decorators/time.ts +112 -0
package/src/yop/Yop.ts ADDED
@@ -0,0 +1,430 @@
1
+ import { CommonConstraints } from "./constraints/CommonConstraints"
2
+ import { validateConstraint } from "./constraints/Constraint"
3
+ import { MinMaxConstraints, validateMinMaxConstraints } from "./constraints/MinMaxConstraints"
4
+ import { MessageProvider, messageProvider_en_US, messageProvider_fr_FR } from "./MessageProvider"
5
+ import { ClassFieldDecorator, getMetadataFromDecorator } from "./Metadata"
6
+ import { joinPath, Path, splitPath } from "./ObjectsUtil"
7
+ import { Constructor, isBoolean } from "./TypesUtil"
8
+ import { Group, InternalValidationContext, ValidationStatus } from "./ValidationContext"
9
+ import { FormManager } from "../reform/FormManager"
10
+
11
+ (Symbol as any).metadata ??= Symbol.for("Symbol.metadata")
12
+
13
+ /**
14
+ * Symbol used to store validation metadata on class fields. This symbol is used as a key to attach validation constraints and related
15
+ * information to class properties.
16
+ * @ignore
17
+ */
18
+ export const validationSymbol = Symbol('YopValidation')
19
+
20
+ /**
21
+ * Type for async validation status, including dependencies.
22
+ * @category Validation Management
23
+ */
24
+ export type AsyncValidationStatus = {
25
+
26
+ /**
27
+ * The current validation status, which can be "pending", "unavailable", or a regular validation status.
28
+ */
29
+ status?: ValidationStatus | undefined
30
+ /**
31
+ * The dependencies for the asynchronous validation.
32
+ */
33
+ dependencies: any[]
34
+ }
35
+
36
+ /**
37
+ * Type for resolved constraints, including required, min, and max.
38
+ * @template MinMax - The type for min/max values.
39
+ * @category Validation Management
40
+ */
41
+ export type ResolvedConstraints<MinMax = unknown> = {
42
+
43
+ /**
44
+ * Whether the field is required.
45
+ */
46
+ required: boolean
47
+ /**
48
+ * The minimum value or length for the field, if applicable.
49
+ */
50
+ min?: MinMax
51
+ /**
52
+ * The maximum value or length for the field, if applicable.
53
+ */
54
+ max?: MinMax
55
+ }
56
+
57
+ /**
58
+ * Type for resolved constraints with an editable field metadata. *Warning*: the `fieldMetadata` property is mutable and should be used with
59
+ * caution, as modifying it can lead to unexpected behavior in validation logic. It is intended for advanced use cases where access to the
60
+ * original constraints is necessary, but it should not be modified during normal validation operations.
61
+ * @template MinMax - The type for min/max values.
62
+ * @category Validation Management
63
+ */
64
+ export type UnsafeResolvedConstraints<MinMax = unknown> = ResolvedConstraints<MinMax> &{
65
+ fieldMetadata?: CommonConstraints<any, any> | undefined
66
+ }
67
+
68
+ /**
69
+ * Base interface of a form manager, extended by {@link FormManager}, representing form state and operations.
70
+ * @category Validation Management
71
+ */
72
+ export interface ValidationForm {
73
+
74
+ /**
75
+ * Whether the form has been submitted at least once, even if the submission was unsuccessful.
76
+ */
77
+ readonly submitted: boolean
78
+ /**
79
+ * Whether the form is currently being submitted.
80
+ */
81
+ readonly submitting: boolean
82
+ /**
83
+ * Map of validation statuses for each field.
84
+ */
85
+ readonly statuses: Map<string, ValidationStatus>
86
+ /**
87
+ * List of all errors in the form: this includes all validation statuses with a severity level of "error".
88
+ */
89
+ readonly errors: ValidationStatus[]
90
+ /**
91
+ * A map that can be used to store arbitrary data related to the form.
92
+ */
93
+ readonly store: Map<string, any>
94
+ /**
95
+ * The HTML form element associated with this validation form.
96
+ */
97
+ readonly htmlForm?: HTMLFormElement
98
+
99
+ /**
100
+ * Retrieves the value of a field by its path.
101
+ * @template T - The expected type of the field value.
102
+ * @param path - The path to the field.
103
+ * @returns The value of the field, or undefined if not found.
104
+ */
105
+ getValue<T = any>(path: string | Path): T | undefined
106
+
107
+ /**
108
+ * Checks if a field has been touched.
109
+ * @param path - The path to the field.
110
+ * @returns True if the field has been touched, false otherwise.
111
+ */
112
+ isTouched(path?: string | Path): boolean
113
+ /**
114
+ * Marks a field as touched.
115
+ * @param path - The path to the field.
116
+ */
117
+ touch(path?: string | Path): void
118
+ /**
119
+ * Marks a field as untouched.
120
+ * @param path - The path to the field.
121
+ */
122
+ untouch(path?: string | Path): void
123
+
124
+ /**
125
+ * Checks if a field is dirty (has been modified). If no path is provided, checks if any field in the entire form is dirty.
126
+ * @param path - The path to the field.
127
+ * @param ignoredPath - Optional path to ignore during the check.
128
+ * @returns True if the field is dirty, false otherwise.
129
+ */
130
+ isDirty(path?: string | Path, ignoredPath?: string | Path): boolean
131
+ }
132
+
133
+ /**
134
+ * Interface for validation settings, including path, groups, and options.
135
+ * @category Validation Management
136
+ */
137
+ export interface ValidationSettings {
138
+
139
+ /**
140
+ * The path to the field or object being validated. This can be a string with dot notation (e.g., "address.street") or
141
+ * an array of path segments (e.g., ["address", "street"]). If omitted, the validation will be performed on the root value.
142
+ */
143
+ path?: string | Path
144
+
145
+ /**
146
+ * Validation groups to apply during validation. This can be a single group or an array of groups. If specified, only the
147
+ * constraints associated with at least one of the active groups will be validated.
148
+ */
149
+ groups?: Group
150
+
151
+ /**
152
+ * Function to determine if a path should be ignored during validation.
153
+ */
154
+ ignore?: (path: Path) => boolean
155
+
156
+ /**
157
+ * Whether to skip asynchronous validation.
158
+ */
159
+ skipAsync?: boolean
160
+
161
+ /**
162
+ * The validation form associated with this validation context. This form is set by the form manager when performing form-level validation
163
+ * and can be used to access form state and operations during validation.
164
+ */
165
+ form?: ValidationForm
166
+ }
167
+
168
+ /**
169
+ * Interface for settings used when resolving constraints at a specific path.
170
+ * @ignore
171
+ */
172
+ export interface ConstraintsAtSettings extends ValidationSettings {
173
+ unsafeMetadata?: boolean
174
+ }
175
+
176
+ /**
177
+ * Main class for validation logic, constraint resolution, and message provider management.
178
+ * @category Validation Management
179
+ */
180
+ export class Yop {
181
+
182
+ private static defaultInstance?: Yop
183
+ private static classIds = new Map<string, Constructor<unknown>>()
184
+
185
+ private static messageProviders = new Map<string, MessageProvider>()
186
+ static {
187
+ this.registerMessageProvider(messageProvider_en_US)
188
+ this.registerMessageProvider(messageProvider_fr_FR)
189
+ }
190
+
191
+ private locale: string = Yop.defaultInstance?.locale ?? "en-US"
192
+
193
+ private _store = new Map<string, any>()
194
+ private _asyncStatuses = new Map<string, AsyncValidationStatus>()
195
+
196
+ /**
197
+ * Gets the store map, which can be used to store arbitrary data related to validation operations. This allows sharing data across different
198
+ * validation contexts and operations.
199
+ */
200
+ get store() {
201
+ return this._store
202
+ }
203
+
204
+ /**
205
+ * Gets the map of asynchronous validation statuses, where each key is a path and the value is the corresponding asynchronous validation status.
206
+ */
207
+ get asyncStatuses() {
208
+ return this._asyncStatuses
209
+ }
210
+
211
+ /**
212
+ * Registers a class constructor with a given ID.
213
+ * @param id The ID to associate with the class constructor.
214
+ * @param constructor The class constructor to register.
215
+ * @ignore
216
+ */
217
+ static registerClass(id: string, constructor: Constructor<unknown>) {
218
+ Yop.classIds.set(id, constructor)
219
+ }
220
+
221
+ /**
222
+ * Resolves a class constructor by its ID.
223
+ * @param id The ID of the class or a function reference.
224
+ * @param silent If true, suppresses error messages for unregistered classes.
225
+ * @returns The resolved class constructor, or undefined if not found.
226
+ * @ignore
227
+ */
228
+ static resolveClass<T>(id: unknown, silent = false): Constructor<T> | undefined {
229
+ if (typeof id === "string") {
230
+ const resolved = Yop.classIds.get(id)
231
+ if (resolved == null && silent === false)
232
+ console.error(`Class "${ id }" unregistered in Yop. Did you forget to add a @id("${ id }") decorator to the class?`)
233
+ return resolved as Constructor<T>
234
+ }
235
+ if (typeof id === "function" && id.prototype == null)
236
+ return id() as Constructor<T>
237
+ return id as Constructor<T> | undefined
238
+ }
239
+
240
+ private contextAt(decorator: ClassFieldDecorator<any>, value: any, settings: ValidationSettings, traverseNullish = false) {
241
+ let constraints = getMetadataFromDecorator(decorator)
242
+ if (constraints == null)
243
+ return undefined
244
+
245
+ const segments = typeof settings.path === "string" ? splitPath(settings.path) : (settings.path ?? [])
246
+ if (segments == null)
247
+ return undefined
248
+
249
+ let context = new InternalValidationContext<any>({
250
+ yop: this,
251
+ kind: constraints.kind,
252
+ value,
253
+ settings: settings,
254
+ })
255
+
256
+ for (const segment of segments) {
257
+ [constraints, value] = constraints.traverse?.(context, constraints, segment, traverseNullish) ?? [,]
258
+ if (constraints == null)
259
+ return undefined
260
+ context = context.createChildContext({ kind: constraints.kind, value, key: segment })
261
+ }
262
+
263
+ return [context, constraints] as const
264
+ }
265
+
266
+ /**
267
+ * Retrieves the constraints for a given class field decorator and value, using the provided settings.
268
+ * @param decorator The class field decorator defining the constraints.
269
+ * @param value The value to be validated against the constraints.
270
+ * @param settings Optional settings for constraint resolution, including path and unsafe metadata flag.
271
+ * @returns The resolved constraints, or undefined if no constraints are found.
272
+ */
273
+ constraintsAt<MinMax = unknown>(decorator: ClassFieldDecorator<any>, value: any, settings?: ConstraintsAtSettings): ResolvedConstraints<MinMax> | undefined {
274
+ const [context, constraints] = this.contextAt(decorator, value, settings ?? {}, true) ?? []
275
+
276
+ if (context != null && constraints != null) {
277
+ const resolvedContraints: UnsafeResolvedConstraints<MinMax> = { required: false }
278
+ validateConstraint(context, constraints, "required", isBoolean, (_, constraint) => { resolvedContraints.required = constraint; return true })
279
+ const isMinMaxType = (constraints as MinMaxConstraints<any, any>).isMinMaxType
280
+ if (isMinMaxType != null) {
281
+ validateMinMaxConstraints(
282
+ context,
283
+ constraints as MinMaxConstraints<unknown, unknown>,
284
+ isMinMaxType,
285
+ (_, min) => { resolvedContraints.min = min; return true },
286
+ (_, max) => { resolvedContraints.max = max; return true }
287
+ )
288
+ }
289
+ if (settings?.unsafeMetadata)
290
+ resolvedContraints.fieldMetadata = constraints
291
+ return resolvedContraints
292
+ }
293
+
294
+ return undefined
295
+ }
296
+ /**
297
+ * Static version of the constraintsAt method, which initializes the Yop instance and retrieves the constraints for a given class field decorator and value.
298
+ * @param decorator The class field decorator defining the constraints.
299
+ * @param value The value to be validated against the constraints.
300
+ * @param settings Optional settings for constraint resolution, including path and unsafe metadata flag.
301
+ * @returns The resolved constraints, or undefined if no constraints are found.
302
+ */
303
+ static constraintsAt<Value>(decorator: ClassFieldDecorator<Value>, value: any, settings?: ConstraintsAtSettings) {
304
+ return Yop.init().constraintsAt(decorator, value, settings)
305
+ }
306
+
307
+ /**
308
+ * Retrieves the asynchronous validation status for a given path. The path can be a string or an array of path segments.
309
+ * @param path The path for which to retrieve the asynchronous validation status.
310
+ * @returns The asynchronous validation status, or undefined if not found.
311
+ */
312
+ getAsyncStatus(path: string | Path) {
313
+ return this.asyncStatuses.get(typeof path === "string" ? path : joinPath(path))?.status
314
+ }
315
+
316
+ /**
317
+ * Validates a value against the constraints defined by a class field decorator, using the provided validation settings. This method performs
318
+ * the validation logic and returns the validation context, which includes the results of the validation operation. The context contains
319
+ * information about the value, its path, any validation statuses.
320
+ * @param value The value to be validated.
321
+ * @param decorator The decorator defining the validation constraints.
322
+ * @param settings The validation settings, including the path and other options.
323
+ * @returns The validation context after performing the validation.
324
+ * @ignore
325
+ */
326
+ rawValidate<Value>(value: any, decorator: ClassFieldDecorator<Value>, settings: ValidationSettings = { path: [] }) {
327
+ const [context, constraints] = this.contextAt(decorator, value, settings) ?? []
328
+ if (context != null && constraints != null)
329
+ constraints.validate(context, constraints)
330
+ return context
331
+ }
332
+
333
+ /**
334
+ * Validates a value against the constraints defined by a class field decorator, using the provided validation settings. This method performs
335
+ * the validation logic and returns the validation statuses array.
336
+ * @param value The value to be validated.
337
+ * @param decorator The decorator defining the validation constraints.
338
+ * @param settings The validation settings, including the path and other options.
339
+ * @returns An array of validation statuses after performing the validation.
340
+ */
341
+ validate<Value>(value: any, decorator: ClassFieldDecorator<Value>, settings: ValidationSettings = { path: [] }) {
342
+ const context = this.rawValidate(value, decorator, settings)
343
+ return context != null ? Array.from(context.statuses.values()) : []
344
+ }
345
+ /**
346
+ * Static version of the validate method, which initializes the Yop instance and performs validation on the provided value using the specified
347
+ * decorator and settings.
348
+ * @param value The value to be validated.
349
+ * @param decorator The decorator defining the validation constraints.
350
+ * @param settings The validation settings, including the path and other options.
351
+ * @returns An array of validation statuses after performing the validation.
352
+ * @see {@link Yop#validate}
353
+ */
354
+ static validate<Value>(value: any, decorator: ClassFieldDecorator<Value>, settings?: ValidationSettings) {
355
+ return Yop.init().validate(value, decorator, settings)
356
+ }
357
+
358
+ /**
359
+ * Registers a message provider for a specific locale. The message provider is used to retrieve localized validation messages
360
+ * based on the current locale setting.
361
+ * @param provider The message provider to register.
362
+ */
363
+ static registerMessageProvider(provider: MessageProvider) {
364
+ try {
365
+ const locale = Intl.getCanonicalLocales(provider.locale)[0]
366
+ Yop.messageProviders.set(locale, provider)
367
+ }
368
+ catch (e) {
369
+ console.error(`Invalid locale "${ provider.locale }" in message provider. Ignoring.`, e)
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Gets the current locale used for message resolution. This locale determines which message provider is used to retrieve validation messages.
375
+ * @returns The current locale as a string.
376
+ */
377
+ getLocale() {
378
+ return this.locale
379
+ }
380
+ /**
381
+ * Static version of the getLocale method, which initializes the Yop instance and retrieves the current locale.
382
+ * @returns The current locale as a string.
383
+ * @see {@link Yop#getLocale}
384
+ */
385
+ static getLocale() {
386
+ return Yop.init().locale
387
+ }
388
+
389
+ /**
390
+ * Sets the current locale used for message resolution. This locale determines which message provider is used to retrieve validation messages.
391
+ * @param locale The locale to set. This should be a valid BCP 47 language tag (e.g., "en-US", "fr-FR").
392
+ */
393
+ setLocale(locale: string) {
394
+ try {
395
+ locale = Intl.getCanonicalLocales(locale)[0]
396
+ if (Yop.messageProviders.has(locale))
397
+ this.locale = locale
398
+ else
399
+ console.error(`No message provider for locale "${ locale }". Ignoring`)
400
+ }
401
+ catch (e) {
402
+ console.error(`Invalid locale "${ locale }". Ignoring.`, e)
403
+ }
404
+ }
405
+ /**
406
+ * Static version of the setLocale method, which initializes the Yop instance and sets the current locale.
407
+ * @param locale The locale to set. This should be a valid BCP 47 language tag (e.g., "en-US", "fr-FR").
408
+ * @see {@link Yop#setLocale}
409
+ */
410
+ static setLocale(locale: string) {
411
+ Yop.init().setLocale(locale)
412
+ }
413
+
414
+ /**
415
+ * Gets the message provider for the current locale. This message provider is used to retrieve localized validation messages based on the current locale setting.
416
+ */
417
+ get messageProvider() {
418
+ return Yop.messageProviders.get(this.locale)!
419
+ }
420
+
421
+ /**
422
+ * Initializes the Yop instance if it hasn't been initialized yet and returns the default instance.
423
+ * @returns The default Yop instance.
424
+ */
425
+ static init(): Yop {
426
+ if (Yop.defaultInstance == null)
427
+ Yop.defaultInstance = new Yop()
428
+ return Yop.defaultInstance
429
+ }
430
+ }
@@ -0,0 +1,124 @@
1
+ import { Constraint, validateConstraint } from "./Constraint"
2
+ import { isBoolean } from "../TypesUtil"
3
+ import { InternalValidationContext } from "../ValidationContext"
4
+ import { Groups } from "../Metadata"
5
+
6
+ /**
7
+ * Common validation constraints for a value, used in decorators and validation logic.
8
+ * @template Value - The type of the value being validated.
9
+ * @template Parent - The type of the parent object.
10
+ * @category Shared Constraints
11
+ */
12
+ export interface CommonConstraints<Value, Parent = unknown> {
13
+ /**
14
+ * If `true`, the validation of the decorated element is skipped.
15
+ */
16
+ ignored?: Constraint<Value | null | undefined, boolean, Parent>
17
+ /**
18
+ * If `true`, the property must be present in the parent object (ie: `"prop" in obj` is true).
19
+ */
20
+ exists?: Constraint<Value | null | undefined, boolean, Parent>
21
+ /**
22
+ * If `true`, the value must not be `undefined`.
23
+ */
24
+ defined?: Constraint<Value | null | undefined, boolean, Parent>
25
+ /**
26
+ * If `true`, the value must not be `null`.
27
+ */
28
+ notnull?: Constraint<Value | null | undefined, boolean, Parent>
29
+ /**
30
+ * If `true`, the value must not be `undefined` or `null`.
31
+ */
32
+ required?: Constraint<Value | null | undefined, boolean, Parent>
33
+ }
34
+
35
+ /**
36
+ * Extracts the value type from a CommonConstraints type.
37
+ * @ignore
38
+ */
39
+ export type ContraintsValue<Contraints> = Contraints extends CommonConstraints<infer Value, infer _Parent> ? Value : never
40
+
41
+ /**
42
+ * Extracts the parent type from a CommonConstraints type.
43
+ * @ignore
44
+ */
45
+ export type ContraintsParent<Contraints> = Contraints extends CommonConstraints<infer _Value, infer Parent> ? Parent : never
46
+
47
+ /**
48
+ * Type for a validation function for a set of constraints.
49
+ * @ignore
50
+ */
51
+ export type Validator<Constraints, Value = ContraintsValue<Constraints>, Parent = ContraintsParent<Constraints>> =
52
+ (context: InternalValidationContext<Value, Parent>, constraints: Constraints) => boolean
53
+
54
+ /**
55
+ * Type for a function that traverses nested constraints and values.
56
+ * @ignore
57
+ */
58
+ export type Traverser<Constraints, Value = ContraintsValue<Constraints>, Parent = ContraintsParent<Constraints>> =
59
+ ((context: InternalValidationContext<Value, Parent>, constraints: Constraints, propertyOrIndex: string | number, traverseNullish?: boolean) =>
60
+ readonly [InternalCommonConstraints | undefined, InternalValidationContext<unknown>])
61
+
62
+ /**
63
+ * Used internally.
64
+ * @ignore
65
+ */
66
+ export interface InternalConstraints {
67
+ /**
68
+ * The kind of the decorated value (eg: `string`, `number`, etc.)
69
+ */
70
+ kind: string
71
+
72
+ /**
73
+ * The method that validates the decorated value.
74
+ */
75
+ validate: Validator<this>
76
+
77
+ /**
78
+ * The method that returns the constraints and value of a nested field.
79
+ */
80
+ traverse?: Traverser<this>
81
+
82
+ /**
83
+ * Validation groups with specific constraints. If specified, the given constraints will only be validated if at least one
84
+ * of the groups is active in the validation context.
85
+ */
86
+ groups?: Groups<this>
87
+ }
88
+
89
+
90
+ /**
91
+ * Internal constraints that include both common and internal constraint logic.
92
+ * @ignore
93
+ */
94
+ export interface InternalCommonConstraints extends CommonConstraints<any, any>, InternalConstraints {
95
+ }
96
+
97
+ /**
98
+ * Validates the common constraints (defined, notnull, required) for a value.
99
+ * @template Value - The type of the value being validated.
100
+ * @template Parent - The type of the parent object.
101
+ * @param context - The validation context.
102
+ * @param constraints - The constraints to validate.
103
+ * @returns True if all constraints pass, false otherwise.
104
+ * @ignore
105
+ */
106
+ export function validateCommonConstraints<Value, Parent>(context: InternalValidationContext<Value, Parent>, constraints: CommonConstraints<Value, Parent>) {
107
+ return (
108
+ validateConstraint(context, constraints, "defined", isBoolean, (value, constraint) => constraint !== true || value !== undefined) &&
109
+ validateConstraint(context, constraints, "notnull", isBoolean, (value, constraint) => constraint !== true || value !== null) &&
110
+ validateConstraint(context, constraints, "required", isBoolean, (value, constraint) => constraint !== true || value != null)
111
+ )
112
+ }
113
+
114
+ /**
115
+ * Validates the type of a value using a provided type check function.
116
+ * @param context - The validation context.
117
+ * @param checkType - Function to check the value's type.
118
+ * @param expectedType - The expected type as a string.
119
+ * @returns True if the value matches the expected type, false otherwise.
120
+ * @ignore
121
+ */
122
+ export function validateTypeConstraint(context: InternalValidationContext<any>, checkType: (value: any) => boolean, expectedType: string) {
123
+ return checkType(context.value) || context.setStatus("type", expectedType) == null
124
+ }
@@ -0,0 +1,135 @@
1
+ import { isFunction } from "../TypesUtil"
2
+ import { InternalValidationContext, Level, ValidationContext } from "../ValidationContext"
3
+ import { JSX } from "react"
4
+
5
+ /**
6
+ * Type representing a constraint message, which can be a direct string or JSX element.
7
+ * @category Shared Constraints
8
+ */
9
+ export type ConstraintMessage = string | JSX.Element
10
+
11
+ /**
12
+ * Type representing a constraint value, which can be a direct value or a tuple containing the value, an optional message, and an optional level.
13
+ * This allows for flexible constraint definitions that can include custom error messages and severity levels.
14
+ * @template ConstraintType - The type of the constraint value.
15
+ * @category Shared Constraints
16
+ */
17
+ export type ConstraintValue<ConstraintType> = ConstraintType | readonly [ConstraintType, ConstraintMessage, Level?]
18
+
19
+ /**
20
+ * Type representing a constraint function that returns a constraint value.
21
+ * @template Value - The type of the value being validated.
22
+ * @template ConstraintType - The type of the constraint value.
23
+ * @template Parent - The type of the parent object.
24
+ * @category Shared Constraints
25
+ */
26
+ export type ConstraintFunction<Value, ConstraintType, Parent = unknown> = ((context: ValidationContext<Value, Parent>) => ConstraintValue<ConstraintType> | undefined)
27
+
28
+ /**
29
+ * A constraint can be defined as a direct value, a tuple with value, message and level, or a function returning
30
+ * either of those.
31
+ * @category Shared Constraints
32
+ */
33
+ export type Constraint<Value, ConstraintType, Parent = unknown> =
34
+ ConstraintValue<ConstraintType> |
35
+ ConstraintFunction<Value, ConstraintType, Parent>
36
+
37
+ /**
38
+ * A validation message can be defined as a direct string/JSX element or a function returning a message.
39
+ * @category Shared Constraints
40
+ */
41
+ export type Message<Value, Parent> = ConstraintMessage | ((context: ValidationContext<Value, Parent>) => ConstraintMessage)
42
+
43
+ /**
44
+ * Validates a constraint for a given context and constraint definition.
45
+ * Handles group-based constraints and default values/messages.
46
+ *
47
+ * @template Value - The type of the value being validated.
48
+ * @template ConstraintType - The type of the constraint value.
49
+ * @template Parent - The type of the parent object.
50
+ * @template Constraints - The type of the constraints object.
51
+ * @param context - The validation context.
52
+ * @param constraints - The constraints object.
53
+ * @param name - The name of the constraint to validate.
54
+ * @param isConstraintType - Type guard for the constraint value.
55
+ * @param validate - Function to validate the value against the constraint.
56
+ * @param defaultConstraint - Optional default constraint value.
57
+ * @param defaultMessage - Optional default error message.
58
+ * @param setStatus - Whether to set the status on failure (default: true).
59
+ * @returns True if the constraint passes, false otherwise.
60
+ * @ignore
61
+ */
62
+ export function validateConstraint<Value, ConstraintType, Parent, Constraints = { [name: string]: Constraint<Value, ConstraintType, Parent> }>(
63
+ context: InternalValidationContext<Value, Parent>,
64
+ constraints: Constraints,
65
+ name: keyof Constraints,
66
+ isConstraintType: (value: any) => value is ConstraintType,
67
+ validate: (value: Value, constraintValue: NonNullable<ConstraintType>) => boolean,
68
+ defaultConstraint?: ConstraintType,
69
+ defaultMessage?: Message<Value, Parent>,
70
+ setStatus = true
71
+ ) {
72
+ if (context.groups == null) {
73
+ const constraint = constraints[name] as Constraint<Value, ConstraintType, Parent> | undefined
74
+ return _validateConstraint(context, constraint, isConstraintType, validate, name as string, defaultConstraint, defaultMessage, setStatus)
75
+ }
76
+
77
+ const groups = Array.isArray(context.groups) ? context.groups : [context.groups]
78
+ for (const group of groups) {
79
+ const constraint = (group == null ? constraints[name] : (constraints as any).groups?.[group]?.[name]) as Constraint<Value, ConstraintType, Parent> | undefined
80
+ if (!_validateConstraint(context, constraint, isConstraintType, validate, name as string, defaultConstraint, defaultMessage, setStatus))
81
+ return false
82
+ }
83
+ return true
84
+ }
85
+
86
+ /**
87
+ * Internal helper to validate a single constraint value, handling tuple and function forms.
88
+ * @ignore
89
+ */
90
+ function _validateConstraint<Value, ConstraintType, Parent>(
91
+ context: InternalValidationContext<Value, Parent>,
92
+ constraint: Constraint<Value, ConstraintType, Parent> | undefined,
93
+ isConstraintType: (value: any) => value is ConstraintType,
94
+ validate: (value: Value, constraintValue: NonNullable<ConstraintType>) => boolean,
95
+ errorCode: string,
96
+ defaultConstraint?: ConstraintType,
97
+ defaultMessage?: Message<Value, Parent>,
98
+ setStatus = true
99
+ ) {
100
+ if (constraint == null && defaultConstraint == null)
101
+ return true
102
+
103
+ let message: ConstraintMessage | undefined = undefined
104
+ let level: Level = "error"
105
+
106
+ if (isFunction(constraint))
107
+ constraint = (constraint as ConstraintFunction<Value, ConstraintType>)(context)
108
+
109
+ if (constraint != null && !isConstraintType(constraint)) {
110
+ if (Array.isArray(constraint)) {
111
+ const [maybeConstraint, maybeMessage, maybeLevel, _maybeGroup] = constraint
112
+ if (maybeConstraint == null || isConstraintType(maybeConstraint)) {
113
+ constraint = maybeConstraint
114
+ message = maybeMessage
115
+ level = (maybeLevel as unknown as Level) ?? "error"
116
+ }
117
+ else
118
+ constraint = undefined
119
+ }
120
+ else
121
+ constraint = undefined
122
+ }
123
+
124
+ if (constraint == null && defaultConstraint != null)
125
+ constraint = defaultConstraint
126
+
127
+ if (message == null && defaultMessage != null)
128
+ message = isFunction(defaultMessage) ? defaultMessage(context) : defaultMessage
129
+
130
+ return (
131
+ constraint == null ||
132
+ validate(context.value as Value, constraint as NonNullable<ConstraintType>) ||
133
+ (setStatus === true && context.setStatus(errorCode, constraint, message, level) == null)
134
+ )
135
+ }