Skip to content

Commit 65590b5

Browse files
chore: implement custom messagaes
1 parent b1e3198 commit 65590b5

File tree

7 files changed

+174
-20
lines changed

7 files changed

+174
-20
lines changed

examples/field-messages.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { MessageProvider, setMessagesProvider, v } from '../src'
2+
3+
// Set up custom messages with field-specific overrides
4+
setMessagesProvider(new MessageProvider({
5+
// Global messages
6+
'required': 'The {{ field }} field is required',
7+
'email': 'Please provide a valid email address',
8+
'min': 'Must be at least {min} characters',
9+
'max': 'Must be at most {max} characters',
10+
11+
// Field-specific messages
12+
'username.required': 'Please choose a username for your account',
13+
'username.min': 'Username must be at least {min} characters long',
14+
'username.max': 'Username cannot exceed {max} characters',
15+
'email.required': 'Email address is required to create your account',
16+
'password.required': 'A password is required for your account',
17+
'password.min': 'Password must be at least {min} characters for security',
18+
}))
19+
20+
// Create a user validator
21+
const userValidator = v.object().shape({
22+
username: v.string().min(3).max(20).required(),
23+
email: v.string().email().required(),
24+
password: v.string().min(8).required(),
25+
})
26+
27+
// Test validation with field-specific messages
28+
const invalidUser = {
29+
username: 'ab', // Too short
30+
email: 'invalid-email',
31+
password: '123', // Too short
32+
}
33+
34+
const result = userValidator.validate(invalidUser)
35+
36+
if (!result.valid) {
37+
console.error('Validation errors:')
38+
Object.entries(result.errors).forEach(([field, errors]) => {
39+
console.error(`${field}:`)
40+
errors.forEach((error: any) => {
41+
console.error(` - ${error.message}`)
42+
})
43+
})
44+
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
export * from './config'
22
// Export validator library
33
export { default as validator } from './lib'
4+
export * from './messages'
45
export * from './types'
5-
export { v } from './validation'
66

7+
export { v } from './validation'
78
export * from './validators'

src/messages.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
export interface MessageProviderType {
2+
getMessage: (rule: string, field?: string, params?: Record<string, any>) => string
3+
setMessage: (rule: string, message: string, field?: string) => void
4+
setMessages: (messages: Record<string, string>) => void
5+
}
6+
7+
export class MessageProvider implements MessageProviderType {
8+
private messages: Map<string, string> = new Map()
9+
10+
constructor(messages?: Record<string, string>) {
11+
if (messages) {
12+
this.setMessages(messages)
13+
}
14+
}
15+
16+
getMessage(rule: string, field?: string, params?: Record<string, any>): string {
17+
let message: string | undefined
18+
19+
// First try field-specific message
20+
if (field) {
21+
const fieldSpecificKey = `${field}.${rule}`
22+
message = this.messages.get(fieldSpecificKey)
23+
}
24+
25+
// Fall back to general rule message
26+
if (!message) {
27+
message = this.messages.get(rule)
28+
}
29+
30+
// Fall back to default message
31+
if (!message) {
32+
message = this.getDefaultMessage(rule)
33+
}
34+
35+
// Replace parameters in the message
36+
if (params) {
37+
message = this.replaceParams(message, params)
38+
}
39+
40+
return message
41+
}
42+
43+
setMessage(rule: string, message: string, field?: string): void {
44+
const key = field ? `${field}.${rule}` : rule
45+
this.messages.set(key, message)
46+
}
47+
48+
setMessages(messages: Record<string, string>): void {
49+
Object.entries(messages).forEach(([key, message]) => {
50+
this.messages.set(key, message)
51+
})
52+
}
53+
54+
private getDefaultMessage(rule: string): string {
55+
const defaults: Record<string, string> = {
56+
required: 'This field is required',
57+
string: 'Must be a string',
58+
number: 'Must be a number',
59+
integer: 'Must be an integer',
60+
float: 'Must be a float',
61+
boolean: 'Must be a boolean',
62+
array: 'Must be an array',
63+
object: 'Must be an object',
64+
email: 'Must be a valid email address',
65+
url: 'Must be a valid URL',
66+
min: 'Must be at least {min}',
67+
max: 'Must be at most {max}',
68+
length: 'Must be exactly {length}',
69+
matches: 'Must match pattern {pattern}',
70+
equals: 'Must be equal to {value}',
71+
alphanumeric: 'Must only contain letters and numbers',
72+
alpha: 'Must only contain letters',
73+
numeric: 'Must only contain numbers',
74+
positive: 'Must be positive',
75+
negative: 'Must be negative',
76+
date: 'Must be a valid date',
77+
datetime: 'Must be a valid datetime',
78+
time: 'Must be a valid time',
79+
timestamp: 'Must be a valid timestamp',
80+
unix: 'Must be a valid Unix timestamp',
81+
json: 'Must be valid JSON',
82+
enum: 'Must be one of: {values}',
83+
custom: 'Validation failed',
84+
}
85+
86+
return defaults[rule] || 'Validation failed'
87+
}
88+
89+
private replaceParams(message: string, params: Record<string, any>): string {
90+
return message.replace(/\{([^}]+)\}/g, (_, key) => {
91+
const value = key.split('.').reduce((obj: any, k: string) => obj?.[k], params)
92+
return value !== undefined ? String(value) : `{${key}}`
93+
})
94+
}
95+
}
96+
97+
// Global messages provider instance
98+
let _globalMessagesProvider: MessageProviderType = new MessageProvider()
99+
100+
// Function to set the global messages provider
101+
export function setMessagesProvider(provider: MessageProvider): void {
102+
_globalMessagesProvider = provider
103+
}
104+
105+
// Function to get the current messages provider
106+
export function getMessagesProvider(): MessageProviderType {
107+
return _globalMessagesProvider
108+
}

src/types/base.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ export interface ValidationConfig {
5151
}
5252

5353
export interface LengthValidator<T> {
54-
min: (length: number) => T
55-
max: (length: number) => T
56-
length: (length: number) => T
54+
min: (length: number, message?: string) => T
55+
max: (length: number, message?: string) => T
56+
length: (length: number, message?: string) => T
5757
}
5858

5959
export type ValidationNames = 'base' |

src/validators/base.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ValidationError, ValidationErrorMap, ValidationNames, ValidationResult, ValidationRule, Validator } from '../types'
2+
import { getMessagesProvider } from '../messages'
23

34
export abstract class BaseValidator<T> implements Validator<T> {
45
protected rules: ValidationRule<T>[] = []
@@ -22,6 +23,11 @@ export abstract class BaseValidator<T> implements Validator<T> {
2223
return this
2324
}
2425

26+
setFieldName(fieldName: string): this {
27+
this.fieldName = fieldName
28+
return this
29+
}
30+
2531
protected addRule(rule: ValidationRule<T>): this {
2632
this.rules.push(rule)
2733
return this
@@ -37,17 +43,19 @@ export abstract class BaseValidator<T> implements Validator<T> {
3743
}
3844

3945
if (this.isRequired && (value === undefined || value === null || value === '')) {
40-
const error = { message: 'This field is required' }
46+
const messagesProvider = getMessagesProvider()
47+
const message = messagesProvider.getMessage('required', this.fieldName)
48+
const error = { message }
4149
return this.isPartOfShape
4250
? { valid: false, errors: { [this.fieldName]: [error] } }
4351
: { valid: false, errors: [error] }
4452
}
4553

4654
for (const rule of this.rules) {
4755
if (!rule.test(value)) {
48-
errors.push({
49-
message: this.formatMessage(rule.message, rule.params ?? {}),
50-
})
56+
const messagesProvider = getMessagesProvider()
57+
const message = messagesProvider.getMessage(rule.name, this.fieldName, rule.params)
58+
errors.push({ message })
5159
}
5260
}
5361

@@ -71,11 +79,4 @@ export abstract class BaseValidator<T> implements Validator<T> {
7179
test(value: T): boolean {
7280
return this.validate(value).valid
7381
}
74-
75-
private formatMessage(message: string, params: Record<string, any>): string {
76-
return message.replace(/\{([^}]+)\}/g, (_, key) => {
77-
const value = key.split('.').reduce((obj: any, k: string) => obj?.[k], params)
78-
return value !== undefined ? String(value) : `{${key}}`
79-
})
80-
}
8182
}

src/validators/objects.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class ObjectValidator<T extends Record<string, any>> extends BaseValidato
2525
shape(schema: Record<string, Validator<any>>): this {
2626
this.schema = Object.entries(schema).reduce((acc, [key, validator]) => {
2727
if (validator instanceof BaseValidator) {
28-
acc[key] = validator.setIsPartOfShape(true)
28+
acc[key] = validator.setIsPartOfShape(true).setFieldName(key)
2929
}
3030
else {
3131
acc[key] = validator

src/validators/strings.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export class StringValidator extends BaseValidator<string> implements StringVali
2626
return false
2727
return value.length >= length
2828
},
29-
message: 'Must be at least {length} characters long',
30-
params: { length },
29+
message: 'Must be at least {min} characters long',
30+
params: { min: length },
3131
})
3232
}
3333

@@ -39,8 +39,8 @@ export class StringValidator extends BaseValidator<string> implements StringVali
3939
return false
4040
return value.length <= length
4141
},
42-
message: 'Must be at most {length} characters long',
43-
params: { length },
42+
message: 'Must be at most {max} characters long',
43+
params: { max: length },
4444
})
4545
}
4646

0 commit comments

Comments
 (0)