Code

< 40 %
40-60 %
> 60 %
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