Passed
Push — main ( b3a0a2...40c57f )
by Rushan
51s queued 11s
created

validation.ViolationList.AsError   A

Complexity

Conditions 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
dl 0
loc 6
ccs 3
cts 3
cp 1
crap 2
rs 10
c 0
b 0
f 0
nop 0
1
package validation
2
3
import (
4
	"encoding/json"
5
	"errors"
6
	"strings"
7
8
	"golang.org/x/text/language"
9
)
10
11
// Violation is the abstraction for validator errors. You can use your own implementations on the application
12
// side to use it for your needs. In order for the validator to generate application violations,
13
// it is necessary to implement the ViolationFactory interface and inject it into the validator.
14
// You can do this by using the SetViolationFactory option in the NewValidator constructor.
15
type Violation interface {
16
	error
17
18
	// Code is unique, short, and semantic string that can be used to programmatically
19
	// test for specific violation. All "code" values are defined in the "github.com/muonsoft/validation/code" package
20
	// and are protected by backward compatibility rules.
21
	Code() string
22
23
	// Is can be used to check that the violation contains one of the specific codes.
24
	// For an empty list, it should always returns false.
25
	Is(codes ...string) bool
26
27
	// Message is a translated message with injected values from constraint. It can be used to show
28
	// a description of a violation to the end-user. Possible values for build-in constraints
29
	// are defined in the "github.com/muonsoft/validation/message" package and can be changed at any time,
30
	// even in patch versions.
31
	Message() string
32
33
	// MessageTemplate is a template for rendering message. Alongside parameters it can be used to
34
	// render the message on the client-side of the library.
35
	MessageTemplate() string
36
37
	// Parameters is the map of the template variables and their values provided by the specific constraint.
38
	Parameters() []TemplateParameter
39
40
	// PropertyPath is a path that points to the violated property.
41
	// See PropertyPath type description for more info.
42
	PropertyPath() PropertyPath
43
}
44
45
// ViolationFactory is the abstraction that can be used to create custom violations on the application side.
46
// Use the SetViolationFactory option on the NewValidator constructor to inject your own factory into the validator.
47
type ViolationFactory interface {
48
	// CreateViolation creates a new instance of Violation.
49
	CreateViolation(
50
		code,
51
		messageTemplate string,
52
		pluralCount int,
53
		parameters []TemplateParameter,
54
		propertyPath PropertyPath,
55
		lang language.Tag,
56
	) Violation
57
}
58
59
// ViolationList is a slice of violations. It is the usual type of error that is returned from a validator.
60
type ViolationList []Violation
61
62
// NewViolationFunc is an adapter that allows you to use ordinary functions as a ViolationFactory.
63
type NewViolationFunc func(
64
	code,
65
	messageTemplate string,
66
	pluralCount int,
67
	parameters []TemplateParameter,
68
	propertyPath PropertyPath,
69
	lang language.Tag,
70
) Violation
71
72
// CreateViolation creates a new instance of a Violation.
73
func (f NewViolationFunc) CreateViolation(
74
	code,
75
	messageTemplate string,
76
	pluralCount int,
77
	parameters []TemplateParameter,
78
	propertyPath PropertyPath,
79
	lang language.Tag,
80
) Violation {
81 1
	return f(code, messageTemplate, pluralCount, parameters, propertyPath, lang)
82
}
83
84
// Error returns a formatted list of errors as a string.
85
func (violations ViolationList) Error() string {
86 1
	if len(violations) == 0 {
87 1
		return "the list of violations is empty, it looks like you forgot to use the AsError method somewhere"
88
	}
89
90 1
	var s strings.Builder
91 1
	s.Grow(32 * len(violations))
92
93 1
	for i, v := range violations {
94 1
		if i > 0 {
95 1
			s.WriteString("; ")
96
		}
97 1
		if iv, ok := v.(*internalViolation); ok {
98 1
			iv.writeToBuilder(&s)
99
		} else {
100 1
			s.WriteString(v.Error())
101
		}
102
	}
103
104 1
	return s.String()
105
}
106
107
// AppendFromError appends a single violation or a slice of violations into the end of a given slice.
108
// If an error does not implement the Violation or ViolationList interface, it will return an error itself.
109
// Otherwise nil will be returned.
110
//
111
// Example
112
//  violations := make(ViolationList, 0)
113
//  err := violations.AppendFromError(previousError)
114
//  if err != nil {
115
//      // this error is not a violation, processing must fail
116
//      return err
117
//  }
118
//  // violations contain appended violations from the previousError and can be processed further
119
func (violations *ViolationList) AppendFromError(err error) error {
120 1
	if violation, ok := UnwrapViolation(err); ok {
121 1
		*violations = append(*violations, violation)
122 1
	} else if violationList, ok := UnwrapViolationList(err); ok {
123 1
		*violations = append(*violations, violationList...)
124 1
	} else if err != nil {
125 1
		return err
126
	}
127
128 1
	return nil
129
}
130
131
// Has can be used to check that at least one of the violations contains one of the specific codes.
132
// For an empty list of codes, it should always returns false.
133
func (violations ViolationList) Has(codes ...string) bool {
134 1
	for _, violation := range violations {
135 1
		if violation.Is(codes...) {
136 1
			return true
137
		}
138
	}
139
140 1
	return false
141
}
142
143
// Filter returns a new list of violations with violations of given codes.
144
func (violations ViolationList) Filter(codes ...string) ViolationList {
145 1
	filtered := make(ViolationList, 0, len(violations))
146
147 1
	for _, violation := range violations {
148 1
		if violation.Is(codes...) {
149 1
			filtered = append(filtered, violation)
150
		}
151
	}
152
153 1
	return filtered
154
}
155
156
// AsError converts the list of violations to an error. This method correctly handles cases where
157
// the list of violations is empty. It returns nil on an empty list, indicating that the validation was successful.
158
func (violations ViolationList) AsError() error {
159 1
	if len(violations) == 0 {
160 1
		return nil
161
	}
162
163 1
	return violations
164
}
165
166
// IsViolation can be used to verify that the error implements the Violation interface.
167
func IsViolation(err error) bool {
168 1
	var violation Violation
169
170 1
	return errors.As(err, &violation)
171
}
172
173
// IsViolationList can be used to verify that the error implements the ViolationList.
174
func IsViolationList(err error) bool {
175 1
	var violations ViolationList
176
177 1
	return errors.As(err, &violations)
178
}
179
180
// UnwrapViolation is a short function to unwrap Violation from the error.
181
func UnwrapViolation(err error) (Violation, bool) {
182 1
	var violation Violation
183
184 1
	as := errors.As(err, &violation)
185
186 1
	return violation, as
187
}
188
189
// UnwrapViolationList is a short function to unwrap ViolationList from the error.
190
func UnwrapViolationList(err error) (ViolationList, bool) {
191 1
	var violations ViolationList
192
193 1
	as := errors.As(err, &violations)
194
195 1
	return violations, as
196
}
197
198
type internalViolation struct {
199
	code            string
200
	message         string
201
	messageTemplate string
202
	parameters      []TemplateParameter
203
	propertyPath    PropertyPath
204
}
205
206
func (v internalViolation) Is(codes ...string) bool {
207 1
	for _, code := range codes {
208 1
		if v.code == code {
209 1
			return true
210
		}
211
	}
212
213 1
	return false
214
}
215
216
func (v internalViolation) Error() string {
217 1
	var s strings.Builder
218 1
	s.Grow(32)
219 1
	v.writeToBuilder(&s)
220
221 1
	return s.String()
222
}
223
224
func (v internalViolation) writeToBuilder(s *strings.Builder) {
225 1
	s.WriteString("violation")
226 1
	if len(v.propertyPath) > 0 {
227 1
		s.WriteString(" at '" + v.propertyPath.String() + "'")
228
	}
229 1
	s.WriteString(": " + v.message)
230
}
231
232
func (v internalViolation) Code() string {
233 1
	return v.code
234
}
235
236
func (v internalViolation) Message() string {
237 1
	return v.message
238
}
239
240
func (v internalViolation) MessageTemplate() string {
241
	return v.messageTemplate
242
}
243
244
func (v internalViolation) Parameters() []TemplateParameter {
245
	return v.parameters
246
}
247
248
func (v internalViolation) PropertyPath() PropertyPath {
249 1
	return v.propertyPath
250
}
251
252
func (v internalViolation) MarshalJSON() ([]byte, error) {
253 1
	return json.Marshal(struct {
254
		Code         string       `json:"code"`
255
		Message      string       `json:"message"`
256
		PropertyPath PropertyPath `json:"propertyPath,omitempty"`
257
	}{
258
		Code:         v.code,
259
		Message:      v.message,
260
		PropertyPath: v.propertyPath,
261
	})
262
}
263
264
type internalViolationFactory struct {
265
	translator *Translator
266
}
267
268
func newViolationFactory(translator *Translator) *internalViolationFactory {
269 1
	return &internalViolationFactory{translator: translator}
270
}
271
272
func (factory *internalViolationFactory) CreateViolation(
273
	code,
274
	messageTemplate string,
275
	pluralCount int,
276
	parameters []TemplateParameter,
277
	propertyPath PropertyPath,
278
	lang language.Tag,
279
) Violation {
280 1
	message := factory.translator.translate(lang, messageTemplate, pluralCount)
281
282 1
	return &internalViolation{
283
		code:            code,
284
		message:         renderMessage(message, parameters),
285
		messageTemplate: messageTemplate,
286
		parameters:      parameters,
287
		propertyPath:    propertyPath,
288
	}
289
}
290
291
// ViolationBuilder used to build an instance of a Violation.
292
type ViolationBuilder struct {
293
	code            string
294
	messageTemplate string
295
	pluralCount     int
296
	parameters      []TemplateParameter
297
	propertyPath    PropertyPath
298
	language        language.Tag
299
300
	violationFactory ViolationFactory
301
}
302
303
// NewViolationBuilder creates a new ViolationBuilder.
304
func NewViolationBuilder(factory ViolationFactory) *ViolationBuilder {
305 1
	return &ViolationBuilder{violationFactory: factory}
306
}
307
308
// BuildViolation creates a new ViolationBuilder for composing Violation object fluently.
309
func (b *ViolationBuilder) BuildViolation(code, message string) *ViolationBuilder {
310 1
	return &ViolationBuilder{
311
		code:             code,
312
		messageTemplate:  message,
313
		violationFactory: b.violationFactory,
314
	}
315
}
316
317
// SetParameters sets parameters that can be injected into the violation message.
318
func (b *ViolationBuilder) SetParameters(parameters []TemplateParameter) *ViolationBuilder {
319 1
	b.parameters = parameters
320
321 1
	return b
322
}
323
324
// AddParameter adds one parameter into a slice of parameters.
325
func (b *ViolationBuilder) AddParameter(name, value string) *ViolationBuilder {
326 1
	b.parameters = append(b.parameters, TemplateParameter{Key: name, Value: value})
327
328 1
	return b
329
}
330
331
// SetPropertyPath sets a property path of violated attribute.
332
func (b *ViolationBuilder) SetPropertyPath(path PropertyPath) *ViolationBuilder {
333 1
	b.propertyPath = path
334
335 1
	return b
336
}
337
338
// SetPluralCount sets a plural number that will be used for message pluralization during translations.
339
func (b *ViolationBuilder) SetPluralCount(pluralCount int) *ViolationBuilder {
340 1
	b.pluralCount = pluralCount
341
342 1
	return b
343
}
344
345
// SetLanguage sets language that will be used to translate the violation message.
346
func (b *ViolationBuilder) SetLanguage(tag language.Tag) *ViolationBuilder {
347 1
	b.language = tag
348
349 1
	return b
350
}
351
352
// CreateViolation creates a new violation with given parameters and returns it.
353
// Violation is created by calling the CreateViolation method of the ViolationFactory.
354
func (b *ViolationBuilder) CreateViolation() Violation {
355 1
	return b.violationFactory.CreateViolation(
356
		b.code,
357
		b.messageTemplate,
358
		b.pluralCount,
359
		b.parameters,
360
		b.propertyPath,
361
		b.language,
362
	)
363
}
364