Passed
Pull Request — main (#52)
by Igor
01:50
created

validation.ViolationList.Error   A

Complexity

Conditions 5

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 13
dl 0
loc 20
ccs 11
cts 11
cp 1
crap 5
rs 9.2833
c 0
b 0
f 0
nop 0
1
package validation
2
3
import (
4
	"bytes"
5
	"encoding/json"
6
	"errors"
7
	"fmt"
8
	"strings"
9
10
	"golang.org/x/text/language"
11
)
12
13
// Violation is the abstraction for validator errors. You can use your own implementations on the application
14
// side to use it for your needs. In order for the validator to generate application violations,
15
// it is necessary to implement the ViolationFactory interface and inject it into the validator.
16
// You can do this by using the SetViolationFactory option in the NewValidator constructor.
17
type Violation interface {
18
	error
19
20
	// Code is unique, short, and semantic string that can be used to programmatically
21
	// test for specific violation. All "code" values are defined in the "github.com/muonsoft/validation/code" package
22
	// and are protected by backward compatibility rules.
23
	Code() string
24
25
	// Is can be used to check that the violation contains one of the specific codes.
26
	// For an empty list, it should always returns false.
27
	Is(codes ...string) bool
28
29
	// Message is a translated message with injected values from constraint. It can be used to show
30
	// a description of a violation to the end-user. Possible values for build-in constraints
31
	// are defined in the "github.com/muonsoft/validation/message" package and can be changed at any time,
32
	// even in patch versions.
33
	Message() string
34
35
	// MessageTemplate is a template for rendering message. Alongside parameters it can be used to
36
	// render the message on the client-side of the library.
37
	MessageTemplate() string
38
39
	// Parameters is the map of the template variables and their values provided by the specific constraint.
40
	Parameters() []TemplateParameter
41
42
	// PropertyPath is a path that points to the violated property.
43
	// See PropertyPath type description for more info.
44
	PropertyPath() *PropertyPath
45
}
46
47
// ViolationFactory is the abstraction that can be used to create custom violations on the application side.
48
// Use the SetViolationFactory option on the NewValidator constructor to inject your own factory into the validator.
49
type ViolationFactory interface {
50
	// CreateViolation creates a new instance of Violation.
51
	CreateViolation(
52
		code,
53
		messageTemplate string,
54
		pluralCount int,
55
		parameters []TemplateParameter,
56
		propertyPath *PropertyPath,
57
		lang language.Tag,
58
	) Violation
59
}
60
61
// NewViolationFunc is an adapter that allows you to use ordinary functions as a ViolationFactory.
62
type NewViolationFunc func(
63
	code,
64
	messageTemplate string,
65
	pluralCount int,
66
	parameters []TemplateParameter,
67
	propertyPath *PropertyPath,
68
	lang language.Tag,
69
) Violation
70
71
// CreateViolation creates a new instance of a Violation.
72
func (f NewViolationFunc) CreateViolation(
73
	code,
74
	messageTemplate string,
75
	pluralCount int,
76
	parameters []TemplateParameter,
77
	propertyPath *PropertyPath,
78
	lang language.Tag,
79
) Violation {
80 1
	return f(code, messageTemplate, pluralCount, parameters, propertyPath, lang)
81
}
82
83
// ViolationList is a linked list of violations. It is the usual type of error that is returned from a validator.
84
type ViolationList struct {
85
	len   int
86
	first *ViolationListElement
87
	last  *ViolationListElement
88
}
89
90
// ViolationListElement points to violation build by validator. It also implements
91
// Violation and can be used as a proxy to underlying violation.
92
type ViolationListElement struct {
93
	next      *ViolationListElement
94
	violation Violation
95
}
96
97
// NewViolationList creates a new ViolationList, that can be immediately populated with
98
// variadic arguments of violations.
99
func NewViolationList(violations ...Violation) *ViolationList {
100 1
	list := &ViolationList{}
101 1
	list.Append(violations...)
102
103 1
	return list
104
}
105
106
// Len returns length of the linked list.
107
func (list *ViolationList) Len() int {
108 1
	return list.len
109
}
110
111
// First returns the first element of the linked list.
112
func (list *ViolationList) First() *ViolationListElement {
113 1
	return list.first
114
}
115
116
// Last returns the last element of the linked list.
117
func (list *ViolationList) Last() *ViolationListElement {
118 1
	return list.last
119
}
120
121
// Append appends violations to the end of the linked list.
122
func (list *ViolationList) Append(violations ...Violation) {
123 1
	for i := range violations {
124 1
		element := &ViolationListElement{violation: violations[i]}
125 1
		if list.first == nil {
126 1
			list.first = element
127 1
			list.last = element
128
		} else {
129 1
			list.last.next = element
130 1
			list.last = element
131
		}
132
	}
133
134 1
	list.len += len(violations)
135
}
136
137
// Join is used to append the given violation list to the end of the current list.
138
func (list *ViolationList) Join(violations *ViolationList) {
139 1
	if violations == nil || violations.len == 0 {
140 1
		return
141
	}
142
143 1
	if list.first == nil {
144 1
		list.first = violations.first
145 1
		list.last = violations.last
146
	} else {
147 1
		list.last.next = violations.first
148 1
		list.last = violations.last
149
	}
150
151 1
	list.len += violations.len
152
}
153
154
// Error returns a formatted list of errors as a string.
155
func (list *ViolationList) Error() string {
156 1
	if list.len == 0 {
157 1
		return "the list of violations is empty, it looks like you forgot to use the AsError method somewhere"
158
	}
159
160 1
	var s strings.Builder
161 1
	s.Grow(32 * list.len)
162
163 1
	i := 0
164 1
	for e := list.first; e != nil; e = e.next {
165 1
		v := e.violation
166 1
		if i > 0 {
167 1
			s.WriteString("; ")
168
		}
169 1
		if iv, ok := v.(*internalViolation); ok {
170 1
			iv.writeToBuilder(&s)
171
		} else {
172
			s.WriteString(v.Error())
173
		}
174 1
		i++
175
	}
176
177 1
	return s.String()
178
}
179
180
// AppendFromError appends a single violation or a slice of violations into the end of a given slice.
181
// If an error does not implement the Violation or ViolationList interface, it will return an error itself.
182
// Otherwise nil will be returned.
183
func (list *ViolationList) AppendFromError(err error) error {
184 1
	if violation, ok := UnwrapViolation(err); ok {
185 1
		list.Append(violation)
186 1
	} else if violationList, ok := UnwrapViolationList(err); ok {
187 1
		list.Join(violationList)
188 1
	} else if err != nil {
189 1
		return err
190
	}
191
192 1
	return nil
193
}
194
195
// Has can be used to check that at least one of the violations contains one of the specific codes.
196
// For an empty list of codes, it should always returns false.
197
func (list *ViolationList) Has(codes ...string) bool {
198 1
	for e := list.first; e != nil; e = e.next {
199 1
		if e.violation.Is(codes...) {
200 1
			return true
201
		}
202
	}
203
204 1
	return false
205
}
206
207
// Filter returns a new list of violations with violations of given codes.
208
func (list *ViolationList) Filter(codes ...string) *ViolationList {
209 1
	filtered := &ViolationList{}
210
211 1
	for e := list.first; e != nil; e = e.next {
212 1
		if e.violation.Is(codes...) {
213 1
			filtered.Append(e.violation)
214
		}
215
	}
216
217 1
	return filtered
218
}
219
220
// AsError converts the list of violations to an error. This method correctly handles cases where
221
// the list of violations is empty. It returns nil on an empty list, indicating that the validation was successful.
222
func (list *ViolationList) AsError() error {
223 1
	if list.len == 0 {
224 1
		return nil
225
	}
226
227 1
	return list
228
}
229
230
// AsSlice converts underlying linked list into slice of Violation.
231
func (list *ViolationList) AsSlice() []Violation {
232 1
	violations := make([]Violation, list.len)
233
234 1
	i := 0
235 1
	for e := list.first; e != nil; e = e.next {
236 1
		violations[i] = e.violation
237 1
		i++
238
	}
239
240 1
	return violations
241
}
242
243
// MarshalJSON marshals the linked list into JSON. Usually, you should use
244
// json.Marshal function for marshaling purposes.
245
func (list *ViolationList) MarshalJSON() ([]byte, error) {
246 1
	b := bytes.Buffer{}
247 1
	b.WriteRune('[')
248 1
	for e := list.first; e != nil; e = e.next {
249 1
		data, err := json.Marshal(e.violation)
250 1
		if err != nil {
251
			return nil, fmt.Errorf("failed to marshal violation: %w", err)
252
		}
253 1
		b.Write(data)
254 1
		if e.next != nil {
255 1
			b.WriteRune(',')
256
		}
257
	}
258 1
	b.WriteRune(']')
259
260 1
	return b.Bytes(), nil
261
}
262
263
// Next returns next element of the linked list.
264
func (element *ViolationListElement) Next() *ViolationListElement {
265 1
	return element.next
266
}
267
268
// Violation returns underlying violation value.
269
func (element *ViolationListElement) Violation() Violation {
270 1
	return element.violation
271
}
272
273
func (element *ViolationListElement) Error() string {
274 1
	return element.violation.Error()
275
}
276
277
func (element *ViolationListElement) Code() string {
278
	return element.violation.Code()
279
}
280
281
func (element *ViolationListElement) Is(codes ...string) bool {
282
	return element.violation.Is(codes...)
283
}
284
285
func (element *ViolationListElement) Message() string {
286
	return element.violation.Message()
287
}
288
289
func (element *ViolationListElement) MessageTemplate() string {
290
	return element.violation.MessageTemplate()
291
}
292
293
func (element *ViolationListElement) Parameters() []TemplateParameter {
294
	return element.violation.Parameters()
295
}
296
297
func (element *ViolationListElement) PropertyPath() *PropertyPath {
298 1
	return element.violation.PropertyPath()
299
}
300
301
// IsViolation can be used to verify that the error implements the Violation interface.
302
func IsViolation(err error) bool {
303 1
	var violation Violation
304
305 1
	return errors.As(err, &violation)
306
}
307
308
// IsViolationList can be used to verify that the error implements the ViolationList.
309
func IsViolationList(err error) bool {
310 1
	var violations *ViolationList
311
312 1
	return errors.As(err, &violations)
313
}
314
315
// UnwrapViolation is a short function to unwrap Violation from the error.
316
func UnwrapViolation(err error) (Violation, bool) {
317 1
	var violation Violation
318
319 1
	as := errors.As(err, &violation)
320
321 1
	return violation, as
322
}
323
324
// UnwrapViolationList is a short function to unwrap ViolationList from the error.
325
func UnwrapViolationList(err error) (*ViolationList, bool) {
326 1
	var violations *ViolationList
327
328 1
	as := errors.As(err, &violations)
329
330 1
	return violations, as
331
}
332
333
type internalViolation struct {
334
	code            string
335
	message         string
336
	messageTemplate string
337
	parameters      []TemplateParameter
338
	propertyPath    *PropertyPath
339
}
340
341
func (v internalViolation) Is(codes ...string) bool {
342 1
	for _, code := range codes {
343 1
		if v.code == code {
344 1
			return true
345
		}
346
	}
347
348 1
	return false
349
}
350
351
func (v internalViolation) Error() string {
352 1
	var s strings.Builder
353 1
	s.Grow(32)
354 1
	v.writeToBuilder(&s)
355
356 1
	return s.String()
357
}
358
359
func (v internalViolation) writeToBuilder(s *strings.Builder) {
360 1
	s.WriteString("violation")
361 1
	if v.propertyPath != nil {
362 1
		s.WriteString(" at '" + v.propertyPath.String() + "'")
363
	}
364 1
	s.WriteString(": " + v.message)
365
}
366
367
func (v internalViolation) Code() string {
368 1
	return v.code
369
}
370
371
func (v internalViolation) Message() string {
372 1
	return v.message
373
}
374
375
func (v internalViolation) MessageTemplate() string {
376
	return v.messageTemplate
377
}
378
379
func (v internalViolation) Parameters() []TemplateParameter {
380
	return v.parameters
381
}
382
383
func (v internalViolation) PropertyPath() *PropertyPath {
384 1
	return v.propertyPath
385
}
386
387
func (v internalViolation) MarshalJSON() ([]byte, error) {
388 1
	return json.Marshal(struct {
389
		Code         string        `json:"code"`
390
		Message      string        `json:"message"`
391
		PropertyPath *PropertyPath `json:"propertyPath,omitempty"`
392
	}{
393
		Code:         v.code,
394
		Message:      v.message,
395
		PropertyPath: v.propertyPath,
396
	})
397
}
398
399
type internalViolationFactory struct {
400
	translator *Translator
401
}
402
403
func newViolationFactory(translator *Translator) *internalViolationFactory {
404 1
	return &internalViolationFactory{translator: translator}
405
}
406
407
func (factory *internalViolationFactory) CreateViolation(
408
	code,
409
	messageTemplate string,
410
	pluralCount int,
411
	parameters []TemplateParameter,
412
	propertyPath *PropertyPath,
413
	lang language.Tag,
414
) Violation {
415 1
	message := factory.translator.translate(lang, messageTemplate, pluralCount)
416
417 1
	return &internalViolation{
418
		code:            code,
419
		message:         renderMessage(message, parameters),
420
		messageTemplate: messageTemplate,
421
		parameters:      parameters,
422
		propertyPath:    propertyPath,
423
	}
424
}
425
426
// ViolationBuilder used to build an instance of a Violation.
427
type ViolationBuilder struct {
428
	code            string
429
	messageTemplate string
430
	pluralCount     int
431
	parameters      []TemplateParameter
432
	propertyPath    *PropertyPath
433
	language        language.Tag
434
435
	violationFactory ViolationFactory
436
}
437
438
// NewViolationBuilder creates a new ViolationBuilder.
439
func NewViolationBuilder(factory ViolationFactory) *ViolationBuilder {
440 1
	return &ViolationBuilder{violationFactory: factory}
441
}
442
443
// BuildViolation creates a new ViolationBuilder for composing Violation object fluently.
444
func (b *ViolationBuilder) BuildViolation(code, message string) *ViolationBuilder {
445 1
	return &ViolationBuilder{
446
		code:             code,
447
		messageTemplate:  message,
448
		violationFactory: b.violationFactory,
449
	}
450
}
451
452
// SetParameters sets template parameters that can be injected into the violation message.
453
func (b *ViolationBuilder) SetParameters(parameters ...TemplateParameter) *ViolationBuilder {
454 1
	b.parameters = parameters
455
456 1
	return b
457
}
458
459
// AddParameter adds one parameter into a slice of parameters.
460
func (b *ViolationBuilder) AddParameter(name, value string) *ViolationBuilder {
461 1
	b.parameters = append(b.parameters, TemplateParameter{Key: name, Value: value})
462
463 1
	return b
464
}
465
466
// SetPropertyPath sets a property path of violated attribute.
467
func (b *ViolationBuilder) SetPropertyPath(path *PropertyPath) *ViolationBuilder {
468 1
	b.propertyPath = path
469
470 1
	return b
471
}
472
473
// SetPluralCount sets a plural number that will be used for message pluralization during translations.
474
func (b *ViolationBuilder) SetPluralCount(pluralCount int) *ViolationBuilder {
475 1
	b.pluralCount = pluralCount
476
477 1
	return b
478
}
479
480
// SetLanguage sets language that will be used to translate the violation message.
481
func (b *ViolationBuilder) SetLanguage(tag language.Tag) *ViolationBuilder {
482 1
	b.language = tag
483
484 1
	return b
485
}
486
487
// CreateViolation creates a new violation with given parameters and returns it.
488
// Violation is created by calling the CreateViolation method of the ViolationFactory.
489
func (b *ViolationBuilder) CreateViolation() Violation {
490 1
	return b.violationFactory.CreateViolation(
491
		b.code,
492
		b.messageTemplate,
493
		b.pluralCount,
494
		b.parameters,
495
		b.propertyPath,
496
		b.language,
497
	)
498
}
499