Skip to content

Commit f3515a3

Browse files
chore: update message logic
1 parent 24d60b0 commit f3515a3

File tree

4 files changed

+63
-55
lines changed

4 files changed

+63
-55
lines changed

src/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ import type { UnixValidator } from './validators/unix'
1414

1515
export interface ValidationError {
1616
message: string
17-
value: any
17+
}
18+
19+
export interface ValidationErrorMap {
20+
[field: string]: ValidationError[]
1821
}
1922

2023
export interface ValidationResult {
2124
valid: boolean
22-
errors: ValidationError[]
25+
errors: ValidationErrorMap
2326
}
2427

2528
export interface ValidationRule<T> {

src/validators/base.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ValidationError, ValidationResult, ValidationRule } from '../types'
1+
import type { ValidationError, ValidationErrorMap, ValidationResult, ValidationRule } from '../types'
22

33
export abstract class BaseValidator<T> {
44
protected rules: ValidationRule<T>[] = []
@@ -26,30 +26,35 @@ export abstract class BaseValidator<T> {
2626
}
2727

2828
validate(value: T | undefined | null): ValidationResult {
29-
const errors: ValidationError[] = []
29+
const errors: ValidationErrorMap = {}
3030

3131
if ((value === undefined || value === null)) {
3232
if (!this.isRequired) {
33-
return { valid: true, errors: [] }
33+
return { valid: true, errors: {} }
3434
}
3535
else {
36-
return {
37-
valid: false,
38-
errors: [{ message: 'This field is required', value }],
39-
}
36+
errors[this.fieldName] = [{
37+
message: 'This field is required',
38+
}]
39+
return { valid: false, errors }
4040
}
4141
}
4242

43+
const fieldErrors: ValidationError[] = []
44+
4345
for (const rule of this.rules) {
4446
if (!rule.test(value)) {
45-
errors.push({
47+
fieldErrors.push({
4648
message: this.formatMessage(rule.message, rule.params ?? {}),
47-
value,
4849
})
4950
}
5051
}
5152

52-
return { valid: errors.length === 0, errors }
53+
if (fieldErrors.length > 0) {
54+
errors[this.fieldName] = fieldErrors
55+
}
56+
57+
return { valid: Object.keys(errors).length === 0, errors }
5358
}
5459

5560
test(value: T): boolean {

src/validators/objects.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ValidationError, ValidationResult, Validator } from '../types'
1+
import type { ValidationErrorMap, ValidationResult, Validator } from '../types'
22
import { BaseValidator } from './base'
33

44
export class ObjectValidator<T extends Record<string, any>> extends BaseValidator<T> {
@@ -47,27 +47,21 @@ export class ObjectValidator<T extends Record<string, any>> extends BaseValidato
4747

4848
// If the base validation passed and we have a schema, validate each field
4949
if (result.valid && Object.keys(this.schema).length > 0 && value !== null && value !== undefined) {
50-
const fieldErrors: ValidationError[] = []
50+
const errors: ValidationErrorMap = {}
5151

5252
for (const [key, validator] of Object.entries(this.schema)) {
5353
const fieldValue = value[key]
5454
const fieldResult = validator.validate(fieldValue)
5555

56-
if (!fieldResult.valid) {
57-
fieldErrors.push(
58-
...fieldResult.errors.map(error => ({
59-
...error,
60-
field: key,
61-
message: `${key}: ${error.message}`,
62-
})),
63-
)
56+
if (!fieldResult.valid && fieldResult.errors[key]) {
57+
errors[key] = fieldResult.errors[key]
6458
}
6559
}
6660

67-
if (fieldErrors.length > 0) {
61+
if (Object.keys(errors).length > 0) {
6862
return {
6963
valid: false,
70-
errors: fieldErrors,
64+
errors,
7165
}
7266
}
7367
}

test/validation.test.ts

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ValidationError } from '../src/types'
12
import { describe, expect, test } from 'bun:test'
23
import { v } from '../src/validation'
34

@@ -101,6 +102,7 @@ describe('Validation Library', () => {
101102

102103
test('comprehensive password validation with multiple rules', () => {
103104
const validator = v.password()
105+
.field('password')
104106
.minLength(8)
105107
.maxLength(20)
106108
.hasUppercase()
@@ -110,10 +112,11 @@ describe('Validation Library', () => {
110112

111113
const result = validator.validate('weak')
112114
expect(result.valid).toBe(false)
113-
expect(result.errors.length).toBeGreaterThan(0)
115+
expect(Object.keys(result.errors)).toContain('password')
116+
expect(result.errors.password?.length).toBeGreaterThan(0)
114117

115118
// Check specific error messages
116-
const errorMessages = result.errors.map(e => e.message)
119+
const errorMessages = result.errors.password?.map((e: ValidationError) => e.message) || []
117120
expect(errorMessages).toContain('Password must be at least 8 characters long')
118121
expect(errorMessages).toContain('Password must contain at least one uppercase letter')
119122
expect(errorMessages).toContain('Password must contain at least one number')
@@ -122,7 +125,7 @@ describe('Validation Library', () => {
122125
// Test a valid password
123126
const validResult = validator.validate('MySecureP@ss123')
124127
expect(validResult.valid).toBe(true)
125-
expect(validResult.errors).toHaveLength(0)
128+
expect(Object.keys(validResult.errors)).toHaveLength(0)
126129
})
127130

128131
test('alphanumeric password validation', () => {
@@ -413,16 +416,16 @@ describe('Validation Library', () => {
413416

414417
test('complex object validation', () => {
415418
const validator = v.object().shape({
416-
name: v.string().min(2).max(50),
417-
email: v.string().email(),
418-
age: v.number().min(18).integer(),
419-
website: v.string().url().optional(),
420-
tags: v.array<string>().each(v.string()).optional(),
419+
name: v.string().min(2).max(50).field('name'),
420+
email: v.string().email().field('email'),
421+
age: v.number().min(18).integer().field('age'),
422+
website: v.string().url().optional().field('website'),
423+
tags: v.array<string>().each(v.string()).optional().field('tags'),
421424
address: v.object().shape({
422-
street: v.string(),
423-
city: v.string(),
424-
zip: v.string(),
425-
}).optional(),
425+
street: v.string().field('street'),
426+
city: v.string().field('city'),
427+
zip: v.string().field('zip'),
428+
}).optional().field('address'),
426429
})
427430

428431
expect(validator.test({
@@ -452,32 +455,37 @@ describe('Validation Library', () => {
452455
})
453456

454457
expect(result.valid).toBe(false)
455-
expect(result.errors).toHaveLength(5)
458+
expect(Object.keys(result.errors).length).toBe(5)
456459

457460
// Check specific error messages
458-
const errorMessages = result.errors.map(e => e.message)
459-
expect(errorMessages).toContain('name: Must be at least 2 characters long') // name too short
460-
expect(errorMessages).toContain('email: Must be a valid email address') // invalid email
461-
expect(errorMessages).toContain('age: Must be at least 18') // age too young
462-
expect(errorMessages).toContain('website: Must be a valid URL') // invalid URL
463-
expect(errorMessages).toContain('tags: Each item in array is invalid') // invalid array item type
461+
expect(result.errors.name?.[0].message).toBe('Must be at least 2 characters long')
462+
expect(result.errors.email?.[0].message).toBe('Must be a valid email address')
463+
expect(result.errors.age?.[0].message).toBe('Must be at least 18')
464+
expect(result.errors.website?.[0].message).toBe('Must be a valid URL')
465+
expect(result.errors.tags?.[0].message).toBe('Each item in array is invalid')
464466
})
465467
})
466468

467469
describe('Validation Results', () => {
468470
test('validate returns detailed results', () => {
469-
const validator = v.string().min(5).max(10)
471+
const validator = v.string().field('name').min(5).max(10)
470472
const result = validator.validate('hi')
471473
expect(result.valid).toBe(false)
472-
expect(result.errors).toHaveLength(1)
473-
expect(result.errors[0].message).toBe('Must be at least 5 characters long')
474+
expect(Object.keys(result.errors)).toContain('name')
475+
expect(result.errors.name?.length).toBe(1)
476+
expect(result.errors.name?.[0].message).toBe('Must be at least 5 characters long')
474477
})
475478

476479
test('multiple validation errors', () => {
477-
const validator = v.string().min(5).max(10).alphanumeric()
480+
const validator = v.string().field('username').min(5).max(10).alphanumeric()
478481
const result = validator.validate('hi!')
479482
expect(result.valid).toBe(false)
480-
expect(result.errors).toHaveLength(2)
483+
expect(Object.keys(result.errors)).toContain('username')
484+
expect(result.errors.username?.length).toBeGreaterThan(1)
485+
486+
const errorMessages = result.errors.username?.map((e: ValidationError) => e.message) || []
487+
expect(errorMessages).toContain('Must be at least 5 characters long')
488+
expect(errorMessages).toContain('Must only contain letters and numbers')
481489
})
482490
})
483491

@@ -486,21 +494,20 @@ describe('Validation Library', () => {
486494
const result = v.custom(
487495
(value: string) => value.startsWith('test-'),
488496
'Must start with "test-"',
489-
).validate('test-123')
497+
).field('custom').validate('invalid-123')
490498

491-
expect(result.valid).toBe(true)
492-
expect(result.errors).toHaveLength(0)
499+
expect(result.valid).toBe(false)
500+
expect(result.errors.custom?.[0].message).toBe('Must start with "test-"')
493501
})
494502

495503
test('should fail with custom error message', () => {
496504
const result = v.custom(
497505
(value: string) => value.startsWith('test-'),
498506
'Must start with "test-"',
499-
).validate('invalid-123')
507+
).field('custom').validate('invalid-123')
500508

501509
expect(result.valid).toBe(false)
502-
expect(result.errors).toHaveLength(1)
503-
expect(result.errors[0].message).toBe('Must start with "test-"')
510+
expect(result.errors.custom?.[0].message).toBe('Must start with "test-"')
504511
})
505512

506513
test('should handle optional values', () => {
@@ -510,7 +517,6 @@ describe('Validation Library', () => {
510517
).optional().validate(undefined)
511518

512519
expect(result.valid).toBe(true)
513-
expect(result.errors).toHaveLength(0)
514520
})
515521
})
516522

0 commit comments

Comments
 (0)