1 | package validation |
||
2 | |||
3 | import ( |
||
4 | "context" |
||
5 | "fmt" |
||
6 | "time" |
||
7 | |||
8 | "github.com/muonsoft/language" |
||
9 | "github.com/muonsoft/validation/message/translations" |
||
10 | "golang.org/x/text/message/catalog" |
||
11 | ) |
||
12 | |||
13 | // Validator is the root validation service. It can be created by [NewValidator] constructor. |
||
14 | // Also, you can use singleton version from the package [github.com/muonsoft/validation/validator]. |
||
15 | type Validator struct { |
||
16 | propertyPath *PropertyPath |
||
17 | language language.Tag |
||
18 | translator Translator |
||
19 | violationFactory ViolationFactory |
||
20 | groups []string |
||
21 | } |
||
22 | |||
23 | // Translator is used to translate violation messages. By default, validator uses an implementation from |
||
24 | // [github.com/muonsoft/validation/message/translations] package based on [golang.org/x/text] package. |
||
25 | // You can set up your own implementation by using [SetTranslator] option. |
||
26 | type Translator interface { |
||
27 | Translate(tag language.Tag, message string, pluralCount int) string |
||
28 | } |
||
29 | |||
30 | // ValidatorOptions is a temporary structure for collecting functional options [ValidatorOption]. |
||
31 | type ValidatorOptions struct { |
||
32 | translatorOptions []translations.TranslatorOption |
||
33 | translator Translator |
||
34 | violationFactory ViolationFactory |
||
35 | 1 | } |
|
36 | |||
37 | func newValidatorOptions() *ValidatorOptions { |
||
38 | return &ValidatorOptions{} |
||
39 | } |
||
40 | |||
41 | // ValidatorOption is a base type for configuration options used to create a new instance of [Validator]. |
||
42 | type ValidatorOption func(options *ValidatorOptions) error |
||
43 | |||
44 | // NewValidator is a constructor for creating an instance of [Validator]. |
||
45 | // You can configure it by using the [ValidatorOption]. |
||
46 | 1 | func NewValidator(options ...ValidatorOption) (*Validator, error) { |
|
47 | var err error |
||
48 | 1 | ||
49 | 1 | opts := newValidatorOptions() |
|
50 | 1 | for _, setOption := range options { |
|
51 | 1 | err = setOption(opts) |
|
52 | 1 | if err != nil { |
|
53 | return nil, err |
||
54 | } |
||
55 | } |
||
56 | 1 | ||
57 | 1 | if opts.translator != nil && len(opts.translatorOptions) > 0 { |
|
58 | return nil, errTranslatorOptionsDenied |
||
59 | 1 | } |
|
60 | 1 | if opts.translator == nil { |
|
61 | 1 | opts.translator, err = translations.NewTranslator(opts.translatorOptions...) |
|
62 | 1 | if err != nil { |
|
63 | return nil, fmt.Errorf("set up default translator: %w", err) |
||
64 | } |
||
65 | 1 | } |
|
66 | 1 | if opts.violationFactory == nil { |
|
67 | opts.violationFactory = NewViolationFactory(opts.translator) |
||
68 | } |
||
69 | 1 | ||
70 | validator := &Validator{ |
||
71 | translator: opts.translator, |
||
72 | violationFactory: opts.violationFactory, |
||
73 | } |
||
74 | |||
75 | 1 | return validator, nil |
|
76 | } |
||
77 | |||
78 | // DefaultLanguage option is used to set up the default language for translation of violation messages. |
||
79 | 1 | func DefaultLanguage(tag language.Tag) ValidatorOption { |
|
80 | return func(options *ValidatorOptions) error { |
||
81 | options.translatorOptions = append(options.translatorOptions, translations.DefaultLanguage(tag)) |
||
82 | |||
83 | return nil |
||
84 | 1 | } |
|
85 | 1 | } |
|
86 | |||
87 | 1 | // Translations option is used to load translation messages into the validator. |
|
88 | // |
||
89 | // By default, all violation messages are generated in the English language with pluralization capabilities. |
||
90 | // To use a custom language you have to load translations on validator initialization. |
||
91 | // Built-in translations are available in the sub-packages of the package [github.com/muonsoft/message/translations]. |
||
92 | // The translation mechanism is provided by the [golang.org/x/text] package (be aware, it has no stable version yet). |
||
93 | func Translations(messages map[language.Tag]map[string]catalog.Message) ValidatorOption { |
||
94 | return func(options *ValidatorOptions) error { |
||
95 | options.translatorOptions = append(options.translatorOptions, translations.SetTranslations(messages)) |
||
96 | |||
97 | return nil |
||
98 | 1 | } |
|
99 | 1 | } |
|
100 | |||
101 | 1 | // SetTranslator option is used to set up the custom implementation of message violation translator. |
|
102 | func SetTranslator(translator Translator) ValidatorOption { |
||
103 | return func(options *ValidatorOptions) error { |
||
104 | options.translator = translator |
||
105 | |||
106 | return nil |
||
107 | 1 | } |
|
108 | 1 | } |
|
109 | |||
110 | 1 | // SetViolationFactory option can be used to override the mechanism of violation creation. |
|
111 | func SetViolationFactory(factory ViolationFactory) ValidatorOption { |
||
112 | return func(options *ValidatorOptions) error { |
||
113 | options.violationFactory = factory |
||
114 | |||
115 | return nil |
||
116 | 1 | } |
|
117 | 1 | } |
|
118 | |||
119 | 1 | // Validate is the main validation method. It accepts validation arguments that can be |
|
120 | // used to tune up the validation process or to pass values of a specific type. |
||
121 | func (validator *Validator) Validate(ctx context.Context, arguments ...Argument) error { |
||
122 | execContext := &executionContext{} |
||
123 | for _, argument := range arguments { |
||
124 | argument.setUp(execContext) |
||
125 | } |
||
126 | |||
127 | violations := &ViolationList{} |
||
128 | for _, validate := range execContext.validations { |
||
129 | 1 | vs, err := validate(ctx, validator) |
|
130 | 1 | if err != nil { |
|
131 | 1 | return err |
|
132 | } |
||
133 | violations.Join(vs) |
||
134 | 1 | } |
|
135 | |||
136 | 1 | return violations.AsError() |
|
137 | } |
||
138 | |||
139 | // ValidateBool is an alias for validating a single boolean value. |
||
140 | func (validator *Validator) ValidateBool(ctx context.Context, value bool, constraints ...BoolConstraint) error { |
||
141 | return validator.Validate(ctx, Bool(value, constraints...)) |
||
142 | } |
||
143 | 1 | ||
144 | 1 | // ValidateInt is an alias for validating a single integer value. |
|
145 | 1 | func (validator *Validator) ValidateInt(ctx context.Context, value int, constraints ...NumberConstraint[int]) error { |
|
146 | 1 | return validator.Validate(ctx, Number(value, constraints...)) |
|
147 | 1 | } |
|
148 | |||
149 | // ValidateFloat is an alias for validating a single float value. |
||
150 | func (validator *Validator) ValidateFloat(ctx context.Context, value float64, constraints ...NumberConstraint[float64]) error { |
||
151 | 1 | return validator.Validate(ctx, Number(value, constraints...)) |
|
152 | 1 | } |
|
153 | 1 | ||
154 | 1 | // ValidateString is an alias for validating a single string value. |
|
155 | 1 | func (validator *Validator) ValidateString(ctx context.Context, value string, constraints ...StringConstraint) error { |
|
156 | return validator.Validate(ctx, String(value, constraints...)) |
||
157 | 1 | } |
|
158 | |||
159 | // ValidateStrings is an alias for validating slice of strings. |
||
160 | 1 | func (validator *Validator) ValidateStrings(ctx context.Context, values []string, constraints ...ComparablesConstraint[string]) error { |
|
161 | return validator.Validate(ctx, Comparables(values, constraints...)) |
||
162 | } |
||
163 | |||
164 | // ValidateCountable is an alias for validating a single countable value (an array, slice, or map). |
||
165 | 1 | func (validator *Validator) ValidateCountable(ctx context.Context, count int, constraints ...CountableConstraint) error { |
|
166 | return validator.Validate(ctx, Countable(count, constraints...)) |
||
167 | } |
||
168 | |||
169 | // ValidateTime is an alias for validating a single time value. |
||
170 | 1 | func (validator *Validator) ValidateTime(ctx context.Context, value time.Time, constraints ...TimeConstraint) error { |
|
171 | return validator.Validate(ctx, Time(value, constraints...)) |
||
172 | } |
||
173 | |||
174 | // ValidateEachString is an alias for validating each value of a strings slice. |
||
175 | 1 | func (validator *Validator) ValidateEachString(ctx context.Context, values []string, constraints ...StringConstraint) error { |
|
176 | return validator.Validate(ctx, EachString(values, constraints...)) |
||
177 | } |
||
178 | |||
179 | // ValidateIt is an alias for validating value that implements the [Validatable] interface. |
||
180 | 1 | func (validator *Validator) ValidateIt(ctx context.Context, validatable Validatable) error { |
|
181 | return validator.Validate(ctx, Valid(validatable)) |
||
182 | } |
||
183 | |||
184 | // WithGroups is used to execute conditional validation based on validation groups. It creates |
||
185 | // a new context validator with a given set of groups. |
||
186 | // |
||
187 | // By default, when validating an object all constraints of it will be checked whether or not |
||
188 | // they pass. In some cases, however, you will need to validate an object against |
||
189 | // only some specific group of constraints. To do this, you can organize each constraint |
||
190 | 1 | // into one or more validation groups and then apply validation against one group of constraints. |
|
191 | // |
||
192 | // Validation groups are working together only with validation groups passed |
||
193 | // to a constraint by WhenGroups() method. This method is implemented in all built-in constraints. |
||
194 | // If you want to use validation groups for your own constraints do not forget to implement |
||
195 | 1 | // this method in your constraint. |
|
196 | // |
||
197 | // Be careful, empty groups are considered as the default group. Its value is equal to the [DefaultGroup] ("default"). |
||
198 | func (validator *Validator) WithGroups(groups ...string) *Validator { |
||
199 | v := validator.copy() |
||
200 | 1 | v.groups = groups |
|
201 | |||
202 | return v |
||
203 | } |
||
204 | |||
205 | 1 | // IsAppliedForGroups compares current validation groups and constraint groups. If one of the validator groups |
|
206 | // intersects with the constraint groups, the validation process should be applied (returns true). |
||
207 | // Empty groups are treated as [DefaultGroup]. To create a new validator with the validation groups |
||
208 | // use the [Validator.WithGroups] method. |
||
209 | func (validator *Validator) IsAppliedForGroups(groups ...string) bool { |
||
210 | 1 | if len(validator.groups) == 0 { |
|
211 | if len(groups) == 0 { |
||
212 | return true |
||
213 | } |
||
214 | for _, g := range groups { |
||
215 | 1 | if g == DefaultGroup { |
|
216 | return true |
||
217 | } |
||
218 | } |
||
219 | } |
||
220 | |||
221 | for _, g1 := range validator.groups { |
||
222 | if len(groups) == 0 { |
||
223 | 1 | if g1 == DefaultGroup { |
|
224 | 1 | return true |
|
225 | } |
||
226 | } |
||
227 | 1 | for _, g2 := range groups { |
|
228 | if g1 == g2 { |
||
229 | return true |
||
230 | } |
||
231 | } |
||
232 | } |
||
233 | |||
234 | return false |
||
235 | } |
||
236 | |||
237 | // IsIgnoredForGroups is the reverse condition for applying validation groups |
||
238 | // to the [Validator.IsAppliedForGroups] method. It is recommended to use this method in |
||
239 | // every validation method of the constraint. |
||
240 | func (validator *Validator) IsIgnoredForGroups(groups ...string) bool { |
||
241 | return !validator.IsAppliedForGroups(groups...) |
||
242 | } |
||
243 | |||
244 | // CreateConstraintError creates a new [ConstraintError], which can be used to stop validation process |
||
245 | 1 | // if constraint is not properly configured. |
|
246 | func (validator *Validator) CreateConstraintError(constraintName, description string) *ConstraintError { |
||
247 | return &ConstraintError{ |
||
248 | ConstraintName: constraintName, |
||
249 | Path: validator.propertyPath, |
||
250 | Description: description, |
||
251 | 1 | } |
|
252 | } |
||
253 | |||
254 | // WithLanguage method creates a new context validator with a given language tag. All created violations |
||
255 | // will be translated into this language. |
||
256 | 1 | // |
|
257 | // The priority of language selection methods: |
||
258 | // |
||
259 | // - [Validator.WithLanguage] has the highest priority and will override any other options; |
||
260 | // - if the validator language is not specified, the validator will try to get the language from the context; |
||
261 | 1 | // - in all other cases, the default language specified in the translator will be used. |
|
262 | func (validator *Validator) WithLanguage(tag language.Tag) *Validator { |
||
263 | v := validator.copy() |
||
264 | v.language = tag |
||
265 | |||
266 | 1 | return v |
|
267 | } |
||
268 | |||
269 | // At method creates a new context validator with appended property path. |
||
270 | func (validator *Validator) At(path ...PropertyPathElement) *Validator { |
||
271 | v := validator.copy() |
||
272 | v.propertyPath = v.propertyPath.With(path...) |
||
273 | |||
274 | return v |
||
275 | } |
||
276 | |||
277 | // AtProperty method creates a new context validator with appended property name to the property path. |
||
278 | func (validator *Validator) AtProperty(name string) *Validator { |
||
279 | v := validator.copy() |
||
280 | v.propertyPath = v.propertyPath.WithProperty(name) |
||
281 | |||
282 | return v |
||
283 | } |
||
284 | |||
285 | // AtIndex method creates a new context validator with appended array index to the property path. |
||
286 | func (validator *Validator) AtIndex(index int) *Validator { |
||
287 | v := validator.copy() |
||
288 | v.propertyPath = v.propertyPath.WithIndex(index) |
||
289 | |||
290 | return v |
||
291 | } |
||
292 | |||
293 | // CreateViolation can be used to quickly create a custom violation on the client-side. |
||
294 | func (validator *Validator) CreateViolation(ctx context.Context, err error, message string, path ...PropertyPathElement) Violation { |
||
295 | return validator.BuildViolation(ctx, err, message).At(path...).Create() |
||
296 | } |
||
297 | |||
298 | // BuildViolation can be used to build a custom violation on the client-side. |
||
299 | func (validator *Validator) BuildViolation(ctx context.Context, err error, message string) *ViolationBuilder { |
||
300 | b := NewViolationBuilder(validator.violationFactory).BuildViolation(err, message) |
||
301 | b = b.SetPropertyPath(validator.propertyPath) |
||
302 | |||
303 | if validator.language != language.Und { |
||
304 | b = b.WithLanguage(validator.language) |
||
305 | } else if ctx != nil { |
||
306 | b = b.WithLanguage(language.FromContext(ctx)) |
||
307 | } |
||
308 | |||
309 | return b |
||
310 | } |
||
311 | |||
312 | // BuildViolationList can be used to build a custom violation list on the client-side. |
||
313 | func (validator *Validator) BuildViolationList(ctx context.Context) *ViolationListBuilder { |
||
314 | b := NewViolationListBuilder(validator.violationFactory) |
||
315 | b = b.SetPropertyPath(validator.propertyPath) |
||
316 | |||
317 | if validator.language != language.Und { |
||
318 | b = b.WithLanguage(validator.language) |
||
319 | } else if ctx != nil { |
||
320 | b = b.WithLanguage(language.FromContext(ctx)) |
||
321 | } |
||
322 | |||
323 | return b |
||
324 | } |
||
325 | |||
326 | func (validator *Validator) copy() *Validator { |
||
327 | return &Validator{ |
||
328 | propertyPath: validator.propertyPath, |
||
329 | language: validator.language, |
||
330 | translator: validator.translator, |
||
331 | violationFactory: validator.violationFactory, |
||
332 | groups: validator.groups, |
||
333 | } |
||
334 | } |
||
335 |