@vytches/ddd-validation 0.26.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.
package/README.md ADDED
@@ -0,0 +1,1562 @@
1
+ # @vytches/ddd-validation
2
+
3
+ <!-- LLM-METADATA
4
+ Package: @vytches/ddd-validation
5
+ Category: Patterns
6
+ Purpose: Comprehensive validation framework with specifications, rules, and domain-specific validators
7
+ Dependencies: @vytches/ddd-domain-primitives, @vytches/ddd-utils
8
+ Complexity: Medium
9
+ DDD Patterns: Specification Pattern, Validation Rules, Domain Validators
10
+ Integration Points: @vytches/ddd-policies, @vytches/ddd-cqrs, @vytches/ddd-value-objects
11
+ -->
12
+
13
+ [![npm version](https://badge.fury.io/js/%40vytches%2Fddd-validation.svg)](https://badge.fury.io/js/%40vytches%2Fddd-validation)
14
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?logo=typescript)](https://www.typescriptlang.org/)
15
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
16
+
17
+ > **Comprehensive validation framework with specifications, fluent rules, and
18
+ > domain-specific validators**
19
+
20
+ Enterprise-grade validation system with specification pattern integration,
21
+ fluent rule builder, async validation support, and comprehensive error
22
+ reporting. Designed for complex domain validation scenarios.
23
+
24
+ ## 📋 Table of Contents
25
+
26
+ - [Installation](#installation)
27
+ - [Key Features](#key-features)
28
+ - [Core Concepts](#core-concepts)
29
+ - [Quick Start](#quick-start)
30
+ - [Specification Pattern](#specification-pattern)
31
+ - [Fluent Rules](#fluent-rules)
32
+ - [Domain Validators](#domain-validators)
33
+ - [Async Validation](#async-validation)
34
+ - [Validation Context](#validation-context)
35
+ - [Error Handling](#error-handling)
36
+ - [Custom Validators](#custom-validators)
37
+ - [Integration Patterns](#integration-patterns)
38
+ - [Testing](#testing)
39
+ - [Best Practices](#best-practices)
40
+ - [Contributing](#contributing)
41
+
42
+ ## 🚀 Installation
43
+
44
+ ```bash
45
+ # npm
46
+ npm install @vytches/ddd-validation
47
+
48
+ # yarn
49
+ yarn add @vytches/ddd-validation
50
+
51
+ # pnpm
52
+ pnpm add @vytches/ddd-validation
53
+ ```
54
+
55
+ ### Peer Dependencies
56
+
57
+ ```bash
58
+ # Required for full functionality
59
+ npm install @vytches/ddd-domain-primitives @vytches/ddd-utils
60
+ ```
61
+
62
+ ## ✨ Key Features
63
+
64
+ ### Validation Framework
65
+
66
+ - **Specification Pattern**: Composable business rules with logical operations
67
+ - **Fluent Rules**: Intuitive fluent API for building validation rules
68
+ - **Domain Validators**: Built-in validators for common domain objects
69
+ - **Async Validation**: Support for asynchronous validation operations
70
+
71
+ ### Enterprise Features
72
+
73
+ - **Validation Context**: Rich context propagation for complex scenarios
74
+ - **Error Reporting**: Comprehensive error details with field-level information
75
+ - **Conditional Validation**: Dynamic validation based on conditions
76
+ - **Rule Composition**: Combine multiple validation rules with logical operators
77
+
78
+ ### Developer Experience
79
+
80
+ - **Type Safety**: Full TypeScript support with strict typing
81
+ - **Extensible**: Easy to create custom validators and specifications
82
+ - **Testing Support**: Comprehensive testing utilities and mocks
83
+ - **Integration**: Seamless integration with other VytchesDDD packages
84
+
85
+ ## 🎯 Core Concepts
86
+
87
+ ### Specification Pattern
88
+
89
+ Specifications encapsulate validation rules that can be composed:
90
+
91
+ ```typescript
92
+ // Base specification interface
93
+ interface ISpecification<T> {
94
+ isSatisfiedBy(entity: T): boolean;
95
+ and(other: ISpecification<T>): ISpecification<T>;
96
+ or(other: ISpecification<T>): ISpecification<T>;
97
+ not(): ISpecification<T>;
98
+ }
99
+
100
+ // Async specification interface
101
+ interface IAsyncSpecification<T> {
102
+ isSatisfiedByAsync(entity: T): Promise<boolean>;
103
+ andAsync(other: IAsyncSpecification<T>): IAsyncSpecification<T>;
104
+ orAsync(other: IAsyncSpecification<T>): IAsyncSpecification<T>;
105
+ notAsync(): IAsyncSpecification<T>;
106
+ }
107
+ ```
108
+
109
+ ### Validation Rules
110
+
111
+ Fluent API for building validation rules:
112
+
113
+ ```typescript
114
+ // Rule builder interface
115
+ interface IRuleBuilder<T> {
116
+ required(): IRuleBuilder<T>;
117
+ optional(): IRuleBuilder<T>;
118
+ minLength(length: number): IRuleBuilder<T>;
119
+ maxLength(length: number): IRuleBuilder<T>;
120
+ pattern(regex: RegExp): IRuleBuilder<T>;
121
+ custom(validator: (value: T) => boolean): IRuleBuilder<T>;
122
+ build(): IValidationRule<T>;
123
+ }
124
+ ```
125
+
126
+ ### Validation Result
127
+
128
+ Comprehensive validation result with error details:
129
+
130
+ ```typescript
131
+ interface ValidationResult {
132
+ isValid: boolean;
133
+ errors: ValidationError[];
134
+ warnings: ValidationWarning[];
135
+ }
136
+
137
+ interface ValidationError {
138
+ field: string;
139
+ code: string;
140
+ message: string;
141
+ details?: Record<string, any>;
142
+ }
143
+ ```
144
+
145
+ ## 🚀 Quick Start
146
+
147
+ ### 1. Basic Specification Usage
148
+
149
+ ```typescript
150
+ import { Specification } from '@vytches/ddd-validation';
151
+
152
+ // Create a simple specification
153
+ class AgeSpecification extends Specification<User> {
154
+ constructor(private readonly minAge: number) {
155
+ super();
156
+ }
157
+
158
+ isSatisfiedBy(user: User): boolean {
159
+ return user.age >= this.minAge;
160
+ }
161
+ }
162
+
163
+ class EmailSpecification extends Specification<User> {
164
+ isSatisfiedBy(user: User): boolean {
165
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
166
+ return emailRegex.test(user.email);
167
+ }
168
+ }
169
+
170
+ // Compose specifications
171
+ const userValidation = new AgeSpecification(18).and(new EmailSpecification());
172
+
173
+ // Validate user
174
+ const user = new User('John Doe', 'john@example.com', 25);
175
+ const isValid = userValidation.isSatisfiedBy(user);
176
+ console.log('User is valid:', isValid);
177
+ ```
178
+
179
+ ### 2. Fluent Rules
180
+
181
+ ```typescript
182
+ import { Rules } from '@vytches/ddd-validation';
183
+
184
+ // Create fluent rules
185
+ const nameRule = Rules.forString()
186
+ .required()
187
+ .minLength(2)
188
+ .maxLength(50)
189
+ .pattern(/^[A-Za-z\s]+$/)
190
+ .build();
191
+
192
+ const emailRule = Rules.forString().required().email().build();
193
+
194
+ const ageRule = Rules.forNumber().required().min(18).max(120).build();
195
+
196
+ // Validate individual fields
197
+ const nameValidation = nameRule.validate('John Doe');
198
+ const emailValidation = emailRule.validate('john@example.com');
199
+ const ageValidation = ageRule.validate(25);
200
+
201
+ console.log('Name valid:', nameValidation.isValid);
202
+ console.log('Email valid:', emailValidation.isValid);
203
+ console.log('Age valid:', ageValidation.isValid);
204
+ ```
205
+
206
+ ### 3. Domain Validators
207
+
208
+ ```typescript
209
+ import { DomainValidator } from '@vytches/ddd-validation';
210
+
211
+ // Create domain validator
212
+ const userValidator = DomainValidator.create<User>()
213
+ .forProperty('name', Rules.forString().required().minLength(2))
214
+ .forProperty('email', Rules.forString().required().email())
215
+ .forProperty('age', Rules.forNumber().required().min(18))
216
+ .build();
217
+
218
+ // Validate entire object
219
+ const user = new User('John Doe', 'john@example.com', 25);
220
+ const validation = await userValidator.validate(user);
221
+
222
+ if (!validation.isValid) {
223
+ console.log('Validation errors:');
224
+ validation.errors.forEach(error => {
225
+ console.log(`${error.field}: ${error.message}`);
226
+ });
227
+ }
228
+ ```
229
+
230
+ ## 🔍 Specification Pattern
231
+
232
+ ### Basic Specifications
233
+
234
+ ```typescript
235
+ import { Specification } from '@vytches/ddd-validation';
236
+
237
+ // User age specification
238
+ class UserAgeSpecification extends Specification<User> {
239
+ constructor(private readonly minAge: number) {
240
+ super();
241
+ }
242
+
243
+ isSatisfiedBy(user: User): boolean {
244
+ return user.age >= this.minAge;
245
+ }
246
+ }
247
+
248
+ // User email specification
249
+ class UserEmailSpecification extends Specification<User> {
250
+ isSatisfiedBy(user: User): boolean {
251
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
252
+ return emailRegex.test(user.email);
253
+ }
254
+ }
255
+
256
+ // User role specification
257
+ class UserRoleSpecification extends Specification<User> {
258
+ constructor(private readonly allowedRoles: UserRole[]) {
259
+ super();
260
+ }
261
+
262
+ isSatisfiedBy(user: User): boolean {
263
+ return this.allowedRoles.includes(user.role);
264
+ }
265
+ }
266
+ ```
267
+
268
+ ### Composite Specifications
269
+
270
+ ```typescript
271
+ // Combine specifications with logical operators
272
+ const adultUserSpec = new UserAgeSpecification(18);
273
+ const validEmailSpec = new UserEmailSpecification();
274
+ const adminOrManagerSpec = new UserRoleSpecification([
275
+ UserRole.ADMIN,
276
+ UserRole.MANAGER,
277
+ ]);
278
+
279
+ // AND operation
280
+ const validAdultUser = adultUserSpec.and(validEmailSpec);
281
+
282
+ // OR operation
283
+ const privilegedUser = adminOrManagerSpec.or(
284
+ new UserRoleSpecification([UserRole.SUPERVISOR])
285
+ );
286
+
287
+ // NOT operation
288
+ const nonAdminUser = adminOrManagerSpec.not();
289
+
290
+ // Complex composition
291
+ const validPrivilegedAdult = adultUserSpec
292
+ .and(validEmailSpec)
293
+ .and(privilegedUser);
294
+
295
+ // Usage
296
+ const user = new User('John Doe', 'john@example.com', 25, UserRole.ADMIN);
297
+ const isValid = validPrivilegedAdult.isSatisfiedBy(user);
298
+ ```
299
+
300
+ ### Parametrized Specifications
301
+
302
+ ```typescript
303
+ // Specifications with parameters
304
+ class UserRegistrationDateSpecification extends Specification<User> {
305
+ constructor(
306
+ private readonly startDate: Date,
307
+ private readonly endDate: Date
308
+ ) {
309
+ super();
310
+ }
311
+
312
+ isSatisfiedBy(user: User): boolean {
313
+ return (
314
+ user.registeredAt >= this.startDate && user.registeredAt <= this.endDate
315
+ );
316
+ }
317
+ }
318
+
319
+ class UserLocationSpecification extends Specification<User> {
320
+ constructor(private readonly allowedCountries: string[]) {
321
+ super();
322
+ }
323
+
324
+ isSatisfiedBy(user: User): boolean {
325
+ return this.allowedCountries.includes(user.country);
326
+ }
327
+ }
328
+
329
+ // Usage with parameters
330
+ const recentUsers = new UserRegistrationDateSpecification(
331
+ new Date('2023-01-01'),
332
+ new Date('2023-12-31')
333
+ );
334
+
335
+ const euUsers = new UserLocationSpecification(['DE', 'FR', 'ES', 'IT']);
336
+
337
+ const recentEuUsers = recentUsers.and(euUsers);
338
+ ```
339
+
340
+ ### Async Specifications
341
+
342
+ For specifications that require asynchronous operations like database queries or
343
+ API calls:
344
+
345
+ ```typescript
346
+ import { AsyncCompositeSpecification } from '@vytches/ddd-validation';
347
+
348
+ // Database-based async specification
349
+ class UserExistsSpecification extends AsyncCompositeSpecification<{
350
+ email: string;
351
+ repository: IUserRepository;
352
+ }> {
353
+ async isSatisfiedByAsync(candidate: {
354
+ email: string;
355
+ repository: IUserRepository;
356
+ }): Promise<boolean> {
357
+ const existingUser = await candidate.repository.findByEmail(
358
+ candidate.email
359
+ );
360
+ return existingUser !== null;
361
+ }
362
+ }
363
+
364
+ // External API validation
365
+ class EmailBlacklistSpecification extends AsyncCompositeSpecification<string> {
366
+ async isSatisfiedByAsync(email: string): Promise<boolean> {
367
+ const response = await fetch(
368
+ `https://api.blacklist.com/check?email=${email}`
369
+ );
370
+ const data = await response.json();
371
+ return !data.isBlacklisted;
372
+ }
373
+ }
374
+
375
+ // Permission check specification
376
+ class HasPermissionSpecification extends AsyncCompositeSpecification<{
377
+ user: User;
378
+ permission: string;
379
+ service: IPermissionService;
380
+ }> {
381
+ async isSatisfiedByAsync(candidate: {
382
+ user: User;
383
+ permission: string;
384
+ service: IPermissionService;
385
+ }): Promise<boolean> {
386
+ const permissions = await candidate.service.getUserPermissions(
387
+ candidate.user.id
388
+ );
389
+ return permissions.includes(candidate.permission);
390
+ }
391
+ }
392
+
393
+ // Combine async specifications
394
+ const userExists = new UserExistsSpecification();
395
+ const notBlacklisted = new EmailBlacklistSpecification();
396
+ const hasAdminPermission = new HasPermissionSpecification();
397
+
398
+ // Logical operations work the same way
399
+ const isValidNewUser = userExists.not().and(notBlacklisted);
400
+
401
+ // Execute async specification
402
+ const isValid = await isValidNewUser.isSatisfiedByAsync({
403
+ email: 'user@example.com',
404
+ repository: userRepository,
405
+ });
406
+
407
+ // Create from async predicate
408
+ const customCheck = AsyncCompositeSpecification.create<User>(
409
+ async (user, context) => {
410
+ // Async operation
411
+ const result = await someAsyncOperation(user, context);
412
+ return result.isValid;
413
+ },
414
+ 'CustomAsyncCheck',
415
+ 'Custom async validation check'
416
+ );
417
+ ```
418
+
419
+ ### Parallel Execution
420
+
421
+ Async specifications execute combined operations in parallel for better
422
+ performance:
423
+
424
+ ```typescript
425
+ // Both database and API calls run simultaneously
426
+ const complexValidation = new AndAsyncSpecification(
427
+ new DatabaseCheckSpecification(), // Runs in parallel
428
+ new ApiValidationSpecification() // Runs in parallel
429
+ );
430
+
431
+ // Result is ready when both complete
432
+ const isValid = await complexValidation.isSatisfiedByAsync(candidate);
433
+
434
+ // OR operations also run in parallel
435
+ const alternativeCheck = new OrAsyncSpecification(
436
+ new PrimaryValidationSpec(), // Runs in parallel
437
+ new FallbackValidationSpec() // Runs in parallel
438
+ );
439
+ ```
440
+
441
+ ### Error Explanation
442
+
443
+ Async specifications can provide detailed failure explanations:
444
+
445
+ ```typescript
446
+ class DetailedAsyncSpec extends AsyncCompositeSpecification<User> {
447
+ async isSatisfiedByAsync(user: User): Promise<boolean> {
448
+ const result = await this.validateUser(user);
449
+ return result.isValid;
450
+ }
451
+
452
+ async explainFailureAsync(user: User): Promise<string | null> {
453
+ const result = await this.validateUser(user);
454
+ if (!result.isValid) {
455
+ return `User validation failed: ${result.reason}`;
456
+ }
457
+ return null;
458
+ }
459
+
460
+ private async validateUser(user: User) {
461
+ // Complex async validation logic
462
+ return { isValid: false, reason: 'Missing required permissions' };
463
+ }
464
+ }
465
+
466
+ // Get detailed error explanation
467
+ const spec = new DetailedAsyncSpec();
468
+ const isValid = await spec.isSatisfiedByAsync(user);
469
+ if (!isValid) {
470
+ const explanation = await spec.explainFailureAsync(user);
471
+ console.log(explanation); // "User validation failed: Missing required permissions"
472
+ }
473
+ ```
474
+
475
+ ## 📏 Fluent Rules
476
+
477
+ ### String Rules
478
+
479
+ ```typescript
480
+ import { Rules } from '@vytches/ddd-validation';
481
+
482
+ // String validation rules
483
+ const nameRule = Rules.forString()
484
+ .required()
485
+ .minLength(2)
486
+ .maxLength(50)
487
+ .pattern(/^[A-Za-z\s]+$/)
488
+ .withMessage('Name must contain only letters and spaces')
489
+ .build();
490
+
491
+ const emailRule = Rules.forString()
492
+ .required()
493
+ .email()
494
+ .withMessage('Please provide a valid email address')
495
+ .build();
496
+
497
+ const phoneRule = Rules.forString()
498
+ .optional()
499
+ .phone()
500
+ .withMessage('Please provide a valid phone number')
501
+ .build();
502
+
503
+ const passwordRule = Rules.forString()
504
+ .required()
505
+ .minLength(8)
506
+ .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
507
+ .withMessage(
508
+ 'Password must contain uppercase, lowercase, number and special character'
509
+ )
510
+ .build();
511
+
512
+ // Validate strings
513
+ const nameValidation = nameRule.validate('John Doe');
514
+ const emailValidation = emailRule.validate('john@example.com');
515
+ const phoneValidation = phoneRule.validate('+1-555-123-4567');
516
+ const passwordValidation = passwordRule.validate('SecurePass123!');
517
+ ```
518
+
519
+ ### Number Rules
520
+
521
+ ```typescript
522
+ // Number validation rules
523
+ const ageRule = Rules.forNumber()
524
+ .required()
525
+ .min(18)
526
+ .max(120)
527
+ .withMessage('Age must be between 18 and 120')
528
+ .build();
529
+
530
+ const priceRule = Rules.forNumber()
531
+ .required()
532
+ .min(0)
533
+ .precision(2)
534
+ .withMessage('Price must be a positive number with up to 2 decimal places')
535
+ .build();
536
+
537
+ const scoreRule = Rules.forNumber()
538
+ .required()
539
+ .min(0)
540
+ .max(100)
541
+ .integer()
542
+ .withMessage('Score must be an integer between 0 and 100')
543
+ .build();
544
+
545
+ // Validate numbers
546
+ const ageValidation = ageRule.validate(25);
547
+ const priceValidation = priceRule.validate(99.99);
548
+ const scoreValidation = scoreRule.validate(85);
549
+ ```
550
+
551
+ ### Date Rules
552
+
553
+ ```typescript
554
+ // Date validation rules
555
+ const birthDateRule = Rules.forDate()
556
+ .required()
557
+ .before(new Date()) // Must be in the past
558
+ .after(new Date('1900-01-01')) // Not too far in the past
559
+ .withMessage('Birth date must be between 1900 and today')
560
+ .build();
561
+
562
+ const appointmentRule = Rules.forDate()
563
+ .required()
564
+ .after(new Date()) // Must be in the future
565
+ .within(30, 'days') // Within 30 days
566
+ .withMessage('Appointment must be scheduled within the next 30 days')
567
+ .build();
568
+
569
+ // Validate dates
570
+ const birthDateValidation = birthDateRule.validate(new Date('1990-05-15'));
571
+ const appointmentValidation = appointmentRule.validate(new Date('2024-02-15'));
572
+ ```
573
+
574
+ ### Array Rules
575
+
576
+ ```typescript
577
+ // Array validation rules
578
+ const tagsRule = Rules.forArray<string>()
579
+ .required()
580
+ .minLength(1)
581
+ .maxLength(10)
582
+ .eachItem(Rules.forString().required().minLength(2))
583
+ .withMessage('Tags must contain 1-10 items, each at least 2 characters')
584
+ .build();
585
+
586
+ const scoresRule = Rules.forArray<number>()
587
+ .optional()
588
+ .minLength(0)
589
+ .maxLength(100)
590
+ .eachItem(Rules.forNumber().min(0).max(100))
591
+ .withMessage('Scores must be between 0 and 100')
592
+ .build();
593
+
594
+ // Validate arrays
595
+ const tagsValidation = tagsRule.validate([
596
+ 'technology',
597
+ 'programming',
598
+ 'typescript',
599
+ ]);
600
+ const scoresValidation = scoresRule.validate([85, 92, 78, 90]);
601
+ ```
602
+
603
+ ### Object Rules
604
+
605
+ ```typescript
606
+ // Object validation rules
607
+ const addressRule = Rules.forObject<Address>()
608
+ .required()
609
+ .property('street', Rules.forString().required().minLength(5))
610
+ .property('city', Rules.forString().required().minLength(2))
611
+ .property(
612
+ 'zipCode',
613
+ Rules.forString()
614
+ .required()
615
+ .pattern(/^\d{5}(-\d{4})?$/)
616
+ )
617
+ .property('country', Rules.forString().required().minLength(2))
618
+ .build();
619
+
620
+ const userRule = Rules.forObject<User>()
621
+ .required()
622
+ .property('name', Rules.forString().required().minLength(2))
623
+ .property('email', Rules.forString().required().email())
624
+ .property('age', Rules.forNumber().required().min(18))
625
+ .property('address', addressRule)
626
+ .build();
627
+
628
+ // Validate objects
629
+ const address = new Address('123 Main St', 'New York', '10001', 'USA');
630
+ const user = new User('John Doe', 'john@example.com', 25, address);
631
+ const userValidation = userRule.validate(user);
632
+ ```
633
+
634
+ ## 🏗️ Domain Validators
635
+
636
+ ### Entity Validators
637
+
638
+ ```typescript
639
+ import { DomainValidator } from '@vytches/ddd-validation';
640
+
641
+ // User entity validator
642
+ const userValidator = DomainValidator.create<User>()
643
+ .forProperty('id', Rules.forString().required().uuid())
644
+ .forProperty('name', Rules.forString().required().minLength(2).maxLength(50))
645
+ .forProperty('email', Rules.forString().required().email())
646
+ .forProperty('age', Rules.forNumber().required().min(18).max(120))
647
+ .forProperty('role', Rules.forEnum(UserRole).required())
648
+ .forProperty('createdAt', Rules.forDate().required().before(new Date()))
649
+ .forProperty(
650
+ 'updatedAt',
651
+ Rules.forDate()
652
+ .required()
653
+ .after(entity => entity.createdAt)
654
+ )
655
+ .build();
656
+
657
+ // Order entity validator
658
+ const orderValidator = DomainValidator.create<Order>()
659
+ .forProperty('id', Rules.forString().required().uuid())
660
+ .forProperty('customerId', Rules.forString().required().uuid())
661
+ .forProperty('items', Rules.forArray().required().minLength(1))
662
+ .forProperty('status', Rules.forEnum(OrderStatus).required())
663
+ .forProperty('totalAmount', Rules.forNumber().required().min(0))
664
+ .forProperty('currency', Rules.forString().required().length(3))
665
+ .build();
666
+
667
+ // Validate entities
668
+ const userValidation = await userValidator.validate(user);
669
+ const orderValidation = await orderValidator.validate(order);
670
+ ```
671
+
672
+ ### Value Object Validators
673
+
674
+ ```typescript
675
+ // Email value object validator
676
+ const emailValidator = DomainValidator.create<Email>()
677
+ .forProperty('value', Rules.forString().required().email())
678
+ .forProperty('domain', Rules.forString().required())
679
+ .forProperty('localPart', Rules.forString().required())
680
+ .build();
681
+
682
+ // Money value object validator
683
+ const moneyValidator = DomainValidator.create<Money>()
684
+ .forProperty('amount', Rules.forNumber().required().min(0))
685
+ .forProperty('currency', Rules.forString().required().length(3))
686
+ .build();
687
+
688
+ // Address value object validator
689
+ const addressValidator = DomainValidator.create<Address>()
690
+ .forProperty('street', Rules.forString().required().minLength(5))
691
+ .forProperty('city', Rules.forString().required().minLength(2))
692
+ .forProperty('state', Rules.forString().required().length(2))
693
+ .forProperty(
694
+ 'zipCode',
695
+ Rules.forString()
696
+ .required()
697
+ .pattern(/^\d{5}(-\d{4})?$/)
698
+ )
699
+ .forProperty('country', Rules.forString().required().length(2))
700
+ .build();
701
+
702
+ // Validate value objects
703
+ const emailValidation = await emailValidator.validate(email);
704
+ const moneyValidation = await moneyValidator.validate(money);
705
+ const addressValidation = await addressValidator.validate(address);
706
+ ```
707
+
708
+ ### Aggregate Validators
709
+
710
+ ```typescript
711
+ // User aggregate validator
712
+ const userAggregateValidator = DomainValidator.create<UserAggregate>()
713
+ .forProperty('id', Rules.forString().required().uuid())
714
+ .forProperty('version', Rules.forNumber().required().min(0))
715
+ .forProperty('profile', userValidator)
716
+ .forProperty('preferences', Rules.forObject().optional())
717
+ .forProperty('subscription', Rules.forObject().optional())
718
+ .build();
719
+
720
+ // Order aggregate validator
721
+ const orderAggregateValidator = DomainValidator.create<OrderAggregate>()
722
+ .forProperty('id', Rules.forString().required().uuid())
723
+ .forProperty('version', Rules.forNumber().required().min(0))
724
+ .forProperty('customerId', Rules.forString().required().uuid())
725
+ .forProperty('items', Rules.forArray().required().minLength(1))
726
+ .forProperty('status', Rules.forEnum(OrderStatus).required())
727
+ .forProperty('paymentInfo', Rules.forObject().optional())
728
+ .forProperty('shippingAddress', addressValidator)
729
+ .forProperty('billingAddress', addressValidator)
730
+ .build();
731
+
732
+ // Validate aggregates
733
+ const userAggregateValidation =
734
+ await userAggregateValidator.validate(userAggregate);
735
+ const orderAggregateValidation =
736
+ await orderAggregateValidator.validate(orderAggregate);
737
+ ```
738
+
739
+ ## ⚡ Async Validation
740
+
741
+ ### Async Specifications
742
+
743
+ ```typescript
744
+ import { AsyncSpecification } from '@vytches/ddd-validation';
745
+
746
+ // Email uniqueness specification
747
+ class EmailUniquenessSpecification extends AsyncSpecification<User> {
748
+ constructor(private readonly userRepository: IUserRepository) {
749
+ super();
750
+ }
751
+
752
+ async isSatisfiedByAsync(user: User): Promise<boolean> {
753
+ const existingUser = await this.userRepository.findByEmail(user.email);
754
+ return existingUser === null || existingUser.id === user.id;
755
+ }
756
+ }
757
+
758
+ // External validation specification
759
+ class ExternalValidationSpecification extends AsyncSpecification<User> {
760
+ constructor(private readonly externalService: IExternalValidationService) {
761
+ super();
762
+ }
763
+
764
+ async isSatisfiedByAsync(user: User): Promise<boolean> {
765
+ const result = await this.externalService.validateUser(user);
766
+ return result.isValid;
767
+ }
768
+ }
769
+
770
+ // Compose async specifications
771
+ const asyncUserValidation = new EmailUniquenessSpecification(
772
+ userRepository
773
+ ).andAsync(new ExternalValidationSpecification(externalService));
774
+
775
+ // Validate asynchronously
776
+ const user = new User('John Doe', 'john@example.com', 25);
777
+ const isValid = await asyncUserValidation.isSatisfiedByAsync(user);
778
+ ```
779
+
780
+ ### Async Rules
781
+
782
+ ```typescript
783
+ // Async validation rules
784
+ const uniqueEmailRule = Rules.forString()
785
+ .required()
786
+ .email()
787
+ .asyncCustom(async (email: string) => {
788
+ const existingUser = await userRepository.findByEmail(email);
789
+ return existingUser === null;
790
+ })
791
+ .withMessage('Email address already exists')
792
+ .build();
793
+
794
+ const validDomainRule = Rules.forString()
795
+ .required()
796
+ .asyncCustom(async (domain: string) => {
797
+ const isValid = await domainValidationService.validateDomain(domain);
798
+ return isValid;
799
+ })
800
+ .withMessage('Domain is not valid')
801
+ .build();
802
+
803
+ // Async validation
804
+ const emailValidation = await uniqueEmailRule.validateAsync('john@example.com');
805
+ const domainValidation = await validDomainRule.validateAsync('example.com');
806
+ ```
807
+
808
+ ### Async Domain Validators
809
+
810
+ ```typescript
811
+ // Async domain validator
812
+ const asyncUserValidator = DomainValidator.create<User>()
813
+ .forProperty('name', Rules.forString().required().minLength(2))
814
+ .forProperty('email', uniqueEmailRule)
815
+ .forProperty(
816
+ 'username',
817
+ Rules.forString()
818
+ .required()
819
+ .asyncCustom(async (username: string) => {
820
+ const exists = await userRepository.existsByUsername(username);
821
+ return !exists;
822
+ })
823
+ )
824
+ .build();
825
+
826
+ // Async validation
827
+ const user = new User('John Doe', 'john@example.com', 25);
828
+ const validation = await asyncUserValidator.validateAsync(user);
829
+
830
+ if (!validation.isValid) {
831
+ console.log('Async validation errors:');
832
+ validation.errors.forEach(error => {
833
+ console.log(`${error.field}: ${error.message}`);
834
+ });
835
+ }
836
+ ```
837
+
838
+ ## 🔧 Validation Context
839
+
840
+ ### Context Creation
841
+
842
+ ```typescript
843
+ import { ValidationContext } from '@vytches/ddd-validation';
844
+
845
+ // Create validation context
846
+ const context = ValidationContext.create()
847
+ .withUserId('user-123')
848
+ .withRequestId('req-456')
849
+ .withTenantId('tenant-789')
850
+ .withEnvironment('production')
851
+ .withMetadata({
852
+ feature: 'user-registration',
853
+ version: '1.2.0',
854
+ })
855
+ .build();
856
+
857
+ // Context-aware validation
858
+ const contextualValidator = DomainValidator.create<User>()
859
+ .forProperty('name', Rules.forString().required().minLength(2))
860
+ .forProperty('email', Rules.forString().required().email())
861
+ .forProperty('role', Rules.forEnum(UserRole).required())
862
+ .withContext(context)
863
+ .build();
864
+
865
+ // Validate with context
866
+ const validation = await contextualValidator.validate(user);
867
+ ```
868
+
869
+ ### Context-Dependent Rules
870
+
871
+ ```typescript
872
+ // Rules that depend on context
873
+ const contextualAgeRule = Rules.forNumber()
874
+ .required()
875
+ .min(context => (context.metadata.feature === 'admin-registration' ? 21 : 18))
876
+ .withMessage('Age requirement depends on registration type')
877
+ .build();
878
+
879
+ const environmentalRule = Rules.forString()
880
+ .required()
881
+ .when(context => context.environment === 'production')
882
+ .then(Rules.forString().pattern(/^[A-Z0-9]+$/))
883
+ .otherwise(Rules.forString().minLength(1))
884
+ .build();
885
+
886
+ // Tenant-specific rules
887
+ const tenantRule = Rules.forString()
888
+ .required()
889
+ .asyncCustom(async (value: string, context: ValidationContext) => {
890
+ const tenantConfig = await configService.getTenantConfig(context.tenantId);
891
+ return tenantConfig.allowedValues.includes(value);
892
+ })
893
+ .build();
894
+ ```
895
+
896
+ ## 🚨 Error Handling
897
+
898
+ ### Validation Error Types
899
+
900
+ ```typescript
901
+ // Validation error hierarchy
902
+ interface ValidationError {
903
+ field: string;
904
+ code: string;
905
+ message: string;
906
+ severity: 'ERROR' | 'WARNING' | 'INFO';
907
+ details?: Record<string, any>;
908
+ }
909
+
910
+ interface ValidationResult {
911
+ isValid: boolean;
912
+ errors: ValidationError[];
913
+ warnings: ValidationError[];
914
+ }
915
+
916
+ // Specific error types
917
+ class RequiredFieldError extends ValidationError {
918
+ constructor(field: string) {
919
+ super({
920
+ field,
921
+ code: 'REQUIRED_FIELD',
922
+ message: `${field} is required`,
923
+ severity: 'ERROR',
924
+ });
925
+ }
926
+ }
927
+
928
+ class InvalidFormatError extends ValidationError {
929
+ constructor(field: string, expectedFormat: string) {
930
+ super({
931
+ field,
932
+ code: 'INVALID_FORMAT',
933
+ message: `${field} must be in ${expectedFormat} format`,
934
+ severity: 'ERROR',
935
+ });
936
+ }
937
+ }
938
+ ```
939
+
940
+ ### Error Handling Patterns
941
+
942
+ ```typescript
943
+ // Comprehensive error handling
944
+ const userValidator = DomainValidator.create<User>()
945
+ .forProperty('name', Rules.forString().required().minLength(2))
946
+ .forProperty('email', Rules.forString().required().email())
947
+ .forProperty('age', Rules.forNumber().required().min(18))
948
+ .build();
949
+
950
+ // Validate and handle errors
951
+ const validation = await userValidator.validate(user);
952
+
953
+ if (!validation.isValid) {
954
+ // Group errors by field
955
+ const errorsByField = validation.errors.reduce(
956
+ (acc, error) => {
957
+ if (!acc[error.field]) {
958
+ acc[error.field] = [];
959
+ }
960
+ acc[error.field].push(error);
961
+ return acc;
962
+ },
963
+ {} as Record<string, ValidationError[]>
964
+ );
965
+
966
+ // Handle each field's errors
967
+ Object.entries(errorsByField).forEach(([field, errors]) => {
968
+ console.log(`Errors for ${field}:`);
969
+ errors.forEach(error => {
970
+ console.log(` - ${error.message} (${error.code})`);
971
+ });
972
+ });
973
+
974
+ // Handle by severity
975
+ const criticalErrors = validation.errors.filter(e => e.severity === 'ERROR');
976
+ const warnings = validation.errors.filter(e => e.severity === 'WARNING');
977
+
978
+ if (criticalErrors.length > 0) {
979
+ throw new ValidationException('Critical validation errors', criticalErrors);
980
+ }
981
+
982
+ if (warnings.length > 0) {
983
+ console.warn('Validation warnings:', warnings);
984
+ }
985
+ }
986
+ ```
987
+
988
+ ### Custom Error Messages
989
+
990
+ ```typescript
991
+ // Custom error messages
992
+ const customMessageValidator = DomainValidator.create<User>()
993
+ .forProperty(
994
+ 'name',
995
+ Rules.forString()
996
+ .required()
997
+ .withMessage('Full name is required')
998
+ .minLength(2)
999
+ .withMessage('Name must be at least 2 characters')
1000
+ )
1001
+ .forProperty(
1002
+ 'email',
1003
+ Rules.forString()
1004
+ .required()
1005
+ .withMessage('Email address is required')
1006
+ .email()
1007
+ .withMessage('Please provide a valid email address')
1008
+ )
1009
+ .forProperty(
1010
+ 'age',
1011
+ Rules.forNumber()
1012
+ .required()
1013
+ .withMessage('Age is required')
1014
+ .min(18)
1015
+ .withMessage('You must be at least 18 years old')
1016
+ )
1017
+ .build();
1018
+
1019
+ // Localized error messages
1020
+ const localizedValidator = DomainValidator.create<User>()
1021
+ .forProperty(
1022
+ 'name',
1023
+ Rules.forString()
1024
+ .required()
1025
+ .withMessage('validation.name.required')
1026
+ .minLength(2)
1027
+ .withMessage('validation.name.min_length')
1028
+ )
1029
+ .withLocalizationService(localizationService)
1030
+ .build();
1031
+ ```
1032
+
1033
+ ## 🛠️ Custom Validators
1034
+
1035
+ ### Custom Specification
1036
+
1037
+ ```typescript
1038
+ // Custom domain-specific specification
1039
+ class ValidBusinessEmailSpecification extends Specification<User> {
1040
+ constructor(private readonly businessDomains: string[]) {
1041
+ super();
1042
+ }
1043
+
1044
+ isSatisfiedBy(user: User): boolean {
1045
+ const domain = user.email.split('@')[1];
1046
+ return this.businessDomains.includes(domain);
1047
+ }
1048
+ }
1049
+
1050
+ // Custom credit score specification
1051
+ class CreditScoreSpecification extends Specification<LoanApplication> {
1052
+ constructor(private readonly minimumScore: number) {
1053
+ super();
1054
+ }
1055
+
1056
+ isSatisfiedBy(application: LoanApplication): boolean {
1057
+ return application.creditScore >= this.minimumScore;
1058
+ }
1059
+ }
1060
+
1061
+ // Usage
1062
+ const businessDomains = ['company.com', 'enterprise.org', 'business.net'];
1063
+ const businessEmailSpec = new ValidBusinessEmailSpecification(businessDomains);
1064
+ const creditSpec = new CreditScoreSpecification(650);
1065
+
1066
+ const businessUserValidation = businessEmailSpec.and(
1067
+ new UserAgeSpecification(21)
1068
+ );
1069
+
1070
+ const loanValidation = creditSpec.and(
1071
+ new IncomeVerificationSpecification(50000)
1072
+ );
1073
+ ```
1074
+
1075
+ ### Custom Rules
1076
+
1077
+ ```typescript
1078
+ // Custom validation rule
1079
+ class CustomRule<T> implements IValidationRule<T> {
1080
+ constructor(
1081
+ private readonly validator: (value: T) => boolean,
1082
+ private readonly errorMessage: string,
1083
+ private readonly errorCode: string
1084
+ ) {}
1085
+
1086
+ validate(value: T): ValidationResult {
1087
+ const isValid = this.validator(value);
1088
+
1089
+ if (!isValid) {
1090
+ return {
1091
+ isValid: false,
1092
+ errors: [
1093
+ {
1094
+ field: '',
1095
+ code: this.errorCode,
1096
+ message: this.errorMessage,
1097
+ severity: 'ERROR',
1098
+ },
1099
+ ],
1100
+ };
1101
+ }
1102
+
1103
+ return { isValid: true, errors: [] };
1104
+ }
1105
+ }
1106
+
1107
+ // Custom rule factory
1108
+ const customRuleFactory = {
1109
+ palindrome: () =>
1110
+ new CustomRule(
1111
+ (value: string) => value === value.split('').reverse().join(''),
1112
+ 'Value must be a palindrome',
1113
+ 'NOT_PALINDROME'
1114
+ ),
1115
+
1116
+ divisibleBy: (divisor: number) =>
1117
+ new CustomRule(
1118
+ (value: number) => value % divisor === 0,
1119
+ `Value must be divisible by ${divisor}`,
1120
+ 'NOT_DIVISIBLE'
1121
+ ),
1122
+
1123
+ uniqueArray: () =>
1124
+ new CustomRule(
1125
+ (value: any[]) => value.length === new Set(value).size,
1126
+ 'Array must contain unique values',
1127
+ 'NOT_UNIQUE'
1128
+ ),
1129
+ };
1130
+
1131
+ // Usage
1132
+ const palindromeRule = customRuleFactory.palindrome();
1133
+ const divisibleByFiveRule = customRuleFactory.divisibleBy(5);
1134
+ const uniqueArrayRule = customRuleFactory.uniqueArray();
1135
+ ```
1136
+
1137
+ ### Custom Async Validators
1138
+
1139
+ ```typescript
1140
+ // Custom async validator
1141
+ class AsyncCustomValidator<T> implements IAsyncValidationRule<T> {
1142
+ constructor(
1143
+ private readonly asyncValidator: (value: T) => Promise<boolean>,
1144
+ private readonly errorMessage: string,
1145
+ private readonly errorCode: string
1146
+ ) {}
1147
+
1148
+ async validateAsync(value: T): Promise<ValidationResult> {
1149
+ const isValid = await this.asyncValidator(value);
1150
+
1151
+ if (!isValid) {
1152
+ return {
1153
+ isValid: false,
1154
+ errors: [
1155
+ {
1156
+ field: '',
1157
+ code: this.errorCode,
1158
+ message: this.errorMessage,
1159
+ severity: 'ERROR',
1160
+ },
1161
+ ],
1162
+ };
1163
+ }
1164
+
1165
+ return { isValid: true, errors: [] };
1166
+ }
1167
+ }
1168
+
1169
+ // Custom async rule factory
1170
+ const asyncRuleFactory = {
1171
+ uniqueInDatabase: (repository: IRepository, field: string) =>
1172
+ new AsyncCustomValidator(
1173
+ async (value: any) => {
1174
+ const existing = await repository.findByField(field, value);
1175
+ return existing === null;
1176
+ },
1177
+ `${field} already exists`,
1178
+ 'NOT_UNIQUE_IN_DATABASE'
1179
+ ),
1180
+
1181
+ validWithExternalService: (service: IExternalValidationService) =>
1182
+ new AsyncCustomValidator(
1183
+ async (value: any) => {
1184
+ const result = await service.validate(value);
1185
+ return result.isValid;
1186
+ },
1187
+ 'External validation failed',
1188
+ 'EXTERNAL_VALIDATION_FAILED'
1189
+ ),
1190
+ };
1191
+ ```
1192
+
1193
+ ## 🔗 Integration Patterns
1194
+
1195
+ ### CQRS Integration
1196
+
1197
+ ```typescript
1198
+ // Command validation
1199
+ @CommandHandler(CreateUserCommand)
1200
+ export class CreateUserCommandHandler {
1201
+ constructor(
1202
+ private readonly userRepository: IUserRepository,
1203
+ private readonly userValidator: DomainValidator<User>
1204
+ ) {}
1205
+
1206
+ async handle(
1207
+ command: CreateUserCommand,
1208
+ context: ExecutionContext
1209
+ ): Promise<void> {
1210
+ // Create user from command
1211
+ const user = User.create(command.name, command.email, command.age);
1212
+
1213
+ // Validate user
1214
+ const validation = await this.userValidator.validate(user);
1215
+
1216
+ if (!validation.isValid) {
1217
+ throw new ValidationException(
1218
+ 'User validation failed',
1219
+ validation.errors
1220
+ );
1221
+ }
1222
+
1223
+ // Save valid user
1224
+ await this.userRepository.save(user);
1225
+ }
1226
+ }
1227
+
1228
+ // Query validation
1229
+ @QueryHandler(GetUsersByAgeRangeQuery)
1230
+ export class GetUsersByAgeRangeQueryHandler {
1231
+ constructor(
1232
+ private readonly userRepository: IUserRepository,
1233
+ private readonly queryValidator: DomainValidator<GetUsersByAgeRangeQuery>
1234
+ ) {}
1235
+
1236
+ async handle(query: GetUsersByAgeRangeQuery): Promise<User[]> {
1237
+ // Validate query
1238
+ const validation = await this.queryValidator.validate(query);
1239
+
1240
+ if (!validation.isValid) {
1241
+ throw new ValidationException(
1242
+ 'Query validation failed',
1243
+ validation.errors
1244
+ );
1245
+ }
1246
+
1247
+ // Execute valid query
1248
+ return await this.userRepository.findByAgeRange(query.minAge, query.maxAge);
1249
+ }
1250
+ }
1251
+ ```
1252
+
1253
+ ### Policy Integration
1254
+
1255
+ ```typescript
1256
+ // Validation with policies
1257
+ import { PolicyBuilder } from '@vytches/ddd-policies';
1258
+
1259
+ // Create policy with validation
1260
+ const userCreationPolicy = PolicyBuilder.create<User>()
1261
+ .withId('user-creation-policy')
1262
+ .withDomain('user-management')
1263
+ .must(new UserAgeSpecification(18))
1264
+ .and()
1265
+ .must(new UserEmailSpecification())
1266
+ .and()
1267
+ .must(
1268
+ new AsyncSpecification(async (user: User) => {
1269
+ const validation = await userValidator.validate(user);
1270
+ return validation.isValid;
1271
+ })
1272
+ )
1273
+ .build();
1274
+
1275
+ // Use in service
1276
+ class UserService {
1277
+ async createUser(userData: CreateUserData): Promise<User> {
1278
+ const user = User.create(userData.name, userData.email, userData.age);
1279
+
1280
+ // Validate with policy
1281
+ const policyResult = await userCreationPolicy.check({
1282
+ entity: user,
1283
+ context,
1284
+ });
1285
+
1286
+ if (policyResult.isFailure()) {
1287
+ throw new PolicyViolationError(
1288
+ 'User creation policy failed',
1289
+ policyResult.violations
1290
+ );
1291
+ }
1292
+
1293
+ return await this.userRepository.save(user);
1294
+ }
1295
+ }
1296
+ ```
1297
+
1298
+ ### Repository Integration
1299
+
1300
+ ```typescript
1301
+ // Repository with validation
1302
+ class ValidatingUserRepository extends BaseRepository<User> {
1303
+ constructor(
1304
+ eventBus: IEventBus,
1305
+ storageAdapter: IStorageAdapter<User>,
1306
+ private readonly userValidator: DomainValidator<User>
1307
+ ) {
1308
+ super(eventBus, storageAdapter);
1309
+ }
1310
+
1311
+ async save(user: User): Promise<void> {
1312
+ // Validate before saving
1313
+ const validation = await this.userValidator.validate(user);
1314
+
1315
+ if (!validation.isValid) {
1316
+ throw new ValidationException(
1317
+ 'Cannot save invalid user',
1318
+ validation.errors
1319
+ );
1320
+ }
1321
+
1322
+ // Save valid user
1323
+ await super.save(user);
1324
+ }
1325
+ }
1326
+ ```
1327
+
1328
+ ## 🧪 Testing
1329
+
1330
+ ### Specification Testing
1331
+
1332
+ ```typescript
1333
+ import { describe, it, expect } from 'vitest';
1334
+
1335
+ describe('UserAgeSpecification', () => {
1336
+ it('should pass for users of legal age', () => {
1337
+ // Arrange
1338
+ const spec = new UserAgeSpecification(18);
1339
+ const user = new User('John Doe', 'john@example.com', 25);
1340
+
1341
+ // Act
1342
+ const result = spec.isSatisfiedBy(user);
1343
+
1344
+ // Assert
1345
+ expect(result).toBe(true);
1346
+ });
1347
+
1348
+ it('should fail for underage users', () => {
1349
+ // Arrange
1350
+ const spec = new UserAgeSpecification(18);
1351
+ const user = new User('Jane Doe', 'jane@example.com', 17);
1352
+
1353
+ // Act
1354
+ const result = spec.isSatisfiedBy(user);
1355
+
1356
+ // Assert
1357
+ expect(result).toBe(false);
1358
+ });
1359
+ });
1360
+
1361
+ describe('Composite Specifications', () => {
1362
+ it('should combine specifications with AND', () => {
1363
+ // Arrange
1364
+ const ageSpec = new UserAgeSpecification(18);
1365
+ const emailSpec = new UserEmailSpecification();
1366
+ const combined = ageSpec.and(emailSpec);
1367
+
1368
+ const validUser = new User('John Doe', 'john@example.com', 25);
1369
+ const invalidUser = new User('Jane Doe', 'invalid-email', 25);
1370
+
1371
+ // Act & Assert
1372
+ expect(combined.isSatisfiedBy(validUser)).toBe(true);
1373
+ expect(combined.isSatisfiedBy(invalidUser)).toBe(false);
1374
+ });
1375
+ });
1376
+ ```
1377
+
1378
+ ### Rule Testing
1379
+
1380
+ ```typescript
1381
+ describe('Fluent Rules', () => {
1382
+ it('should validate string rules', () => {
1383
+ // Arrange
1384
+ const rule = Rules.forString()
1385
+ .required()
1386
+ .minLength(2)
1387
+ .maxLength(50)
1388
+ .pattern(/^[A-Za-z\s]+$/)
1389
+ .build();
1390
+
1391
+ // Act & Assert
1392
+ expect(rule.validate('John Doe').isValid).toBe(true);
1393
+ expect(rule.validate('J').isValid).toBe(false); // Too short
1394
+ expect(rule.validate('John123').isValid).toBe(false); // Invalid pattern
1395
+ expect(rule.validate('').isValid).toBe(false); // Required
1396
+ });
1397
+
1398
+ it('should validate number rules', () => {
1399
+ // Arrange
1400
+ const rule = Rules.forNumber().required().min(18).max(120).build();
1401
+
1402
+ // Act & Assert
1403
+ expect(rule.validate(25).isValid).toBe(true);
1404
+ expect(rule.validate(17).isValid).toBe(false); // Too low
1405
+ expect(rule.validate(121).isValid).toBe(false); // Too high
1406
+ });
1407
+ });
1408
+ ```
1409
+
1410
+ ### Async Validation Testing
1411
+
1412
+ ```typescript
1413
+ describe('Async Validation', () => {
1414
+ it('should validate async specifications', async () => {
1415
+ // Arrange
1416
+ const mockRepository = {
1417
+ findByEmail: jest.fn().mockResolvedValue(null),
1418
+ };
1419
+
1420
+ const spec = new EmailUniquenessSpecification(mockRepository);
1421
+ const user = new User('John Doe', 'john@example.com', 25);
1422
+
1423
+ // Act
1424
+ const result = await spec.isSatisfiedByAsync(user);
1425
+
1426
+ // Assert
1427
+ expect(result).toBe(true);
1428
+ expect(mockRepository.findByEmail).toHaveBeenCalledWith('john@example.com');
1429
+ });
1430
+
1431
+ it('should handle async validation errors', async () => {
1432
+ // Arrange
1433
+ const mockRepository = {
1434
+ findByEmail: jest.fn().mockRejectedValue(new Error('Database error')),
1435
+ };
1436
+
1437
+ const spec = new EmailUniquenessSpecification(mockRepository);
1438
+ const user = new User('John Doe', 'john@example.com', 25);
1439
+
1440
+ // Act & Assert
1441
+ await expect(spec.isSatisfiedByAsync(user)).rejects.toThrow(
1442
+ 'Database error'
1443
+ );
1444
+ });
1445
+ });
1446
+ ```
1447
+
1448
+ ## 🏆 Best Practices
1449
+
1450
+ ### Specification Design
1451
+
1452
+ ```typescript
1453
+ // ✅ Good: Single responsibility
1454
+ class UserAgeSpecification extends Specification<User> {
1455
+ constructor(private readonly minAge: number) {
1456
+ super();
1457
+ }
1458
+
1459
+ isSatisfiedBy(user: User): boolean {
1460
+ return user.age >= this.minAge;
1461
+ }
1462
+ }
1463
+
1464
+ // ❌ Bad: Multiple responsibilities
1465
+ class UserValidationSpecification extends Specification<User> {
1466
+ isSatisfiedBy(user: User): boolean {
1467
+ return user.age >= 18 && user.email.includes('@') && user.name.length > 0;
1468
+ }
1469
+ }
1470
+ ```
1471
+
1472
+ ### Rule Composition
1473
+
1474
+ ```typescript
1475
+ // ✅ Good: Composable rules
1476
+ const nameRule = Rules.forString().required().minLength(2);
1477
+ const emailRule = Rules.forString().required().email();
1478
+ const ageRule = Rules.forNumber().required().min(18);
1479
+
1480
+ const userValidator = DomainValidator.create<User>()
1481
+ .forProperty('name', nameRule)
1482
+ .forProperty('email', emailRule)
1483
+ .forProperty('age', ageRule)
1484
+ .build();
1485
+
1486
+ // ❌ Bad: Monolithic validation
1487
+ const massiveValidator = DomainValidator.create<User>()
1488
+ .forProperty(
1489
+ 'everything',
1490
+ Rules.forObject().custom(user => {
1491
+ // Complex validation logic mixed together
1492
+ return user.name && user.email && user.age >= 18;
1493
+ })
1494
+ )
1495
+ .build();
1496
+ ```
1497
+
1498
+ ### Error Handling
1499
+
1500
+ ```typescript
1501
+ // ✅ Good: Specific error handling
1502
+ const validation = await userValidator.validate(user);
1503
+
1504
+ if (!validation.isValid) {
1505
+ const requiredErrors = validation.errors.filter(
1506
+ e => e.code === 'REQUIRED_FIELD'
1507
+ );
1508
+ const formatErrors = validation.errors.filter(
1509
+ e => e.code === 'INVALID_FORMAT'
1510
+ );
1511
+
1512
+ if (requiredErrors.length > 0) {
1513
+ throw new RequiredFieldsError(requiredErrors);
1514
+ }
1515
+
1516
+ if (formatErrors.length > 0) {
1517
+ throw new InvalidFormatError(formatErrors);
1518
+ }
1519
+ }
1520
+
1521
+ // ❌ Bad: Generic error handling
1522
+ const validation = await userValidator.validate(user);
1523
+ if (!validation.isValid) {
1524
+ throw new Error('Validation failed');
1525
+ }
1526
+ ```
1527
+
1528
+ ## 🤝 Contributing
1529
+
1530
+ We welcome contributions! Please see our
1531
+ [Contributing Guide](../../CONTRIBUTING.md) for details.
1532
+
1533
+ ### Development Setup
1534
+
1535
+ ```bash
1536
+ # Clone repository
1537
+ git clone https://github.com/vytches/ddd.git
1538
+ cd ddd
1539
+
1540
+ # Install dependencies
1541
+ pnpm install
1542
+
1543
+ # Build package
1544
+ pnpm build --filter=@vytches/ddd-validation
1545
+
1546
+ # Run tests
1547
+ pnpm test --filter=@vytches/ddd-validation
1548
+
1549
+ # Run in development mode
1550
+ pnpm dev --filter=@vytches/ddd-validation
1551
+ ```
1552
+
1553
+ ## 📄 License
1554
+
1555
+ This project is licensed under the MIT License - see the
1556
+ [LICENSE](../../LICENSE) file for details.
1557
+
1558
+ ---
1559
+
1560
+ **Part of the [@vytches/ddd-core](https://github.com/vytches/ddd) ecosystem**
1561
+
1562
+ For more information, visit the [main documentation](../../README.md).