Code

< 40 %
40-60 %
> 60 %
1
package validation
2
3
import (
4
	"bytes"
5
	"encoding/json"
6
	"errors"
7
	"fmt"
8
	"io"
9
	"strconv"
10
	"strings"
11
12
	"golang.org/x/text/language"
13
)
14
15
// Violation is the abstraction for validator errors. You can use your own implementations on the application
16
// side to use it for your needs. In order for the validator to generate application violations,
17
// it is necessary to implement the [ViolationFactory] interface and inject it into the validator.
18
// You can do this by using the [SetViolationFactory] option in the [NewValidator] constructor.
19
type Violation interface {
20
	error
21
	// Unwrap returns underlying static error. This error can be used as a unique, short, and semantic code
22
	// that can be used to test for specific violation by [errors.Is] from standard library.
23
	Unwrap() error
24
25
	// Is can be used to check that the violation contains one of the specific static errors.
26
	Is(target error) bool
27
28
	// Message is a translated message with injected values from constraint. It can be used to show
29
	// a description of a violation to the end-user. Possible values for build-in constraints
30
	// are defined in the [github.com/muonsoft/validation/message] package and can be changed at any time,
31
	// even in patch versions.
32
	Message() string
33
34
	// MessageTemplate is a template for rendering message. Alongside parameters it can be used to
35
	// render the message on the client-side of the library.
36
	MessageTemplate() string
37
38
	// Parameters is the map of the template variables and their values provided by the specific constraint.
39
	Parameters() []TemplateParameter
40
41
	// PropertyPath is a path that points to the violated property.
42
	// See [PropertyPath] type description for more info.
43
	PropertyPath() *PropertyPath
44
}
45
46
// ViolationFactory is the abstraction that can be used to create custom violations on the application side.
47
// Use the [SetViolationFactory] option on the [NewValidator] constructor to inject your own factory into the validator.
48
type ViolationFactory interface {
49
	// CreateViolation creates a new instance of [Violation].
50
	CreateViolation(
51
		err error,
52
		messageTemplate string,
53
		pluralCount int,
54
		parameters []TemplateParameter,
55
		propertyPath *PropertyPath,
56
		lang language.Tag,
57
	) Violation
58
}
59
60
// NewViolationFunc is an adapter that allows you to use ordinary functions as a [ViolationFactory].
61
type NewViolationFunc func(
62
	err error,
63
	messageTemplate string,
64
	pluralCount int,
65
	parameters []TemplateParameter,
66
	propertyPath *PropertyPath,
67
	lang language.Tag,
68
) Violation
69
70
// CreateViolation creates a new instance of a [Violation].
71
func (f NewViolationFunc) CreateViolation(
72
	err error,
73
	messageTemplate string,
74
	pluralCount int,
75
	parameters []TemplateParameter,
76
	propertyPath *PropertyPath,
77
	lang language.Tag,
78
) Violation {
79
	return f(err, messageTemplate, pluralCount, parameters, propertyPath, lang)
80 1
}
81
82
// ViolationList is a linked list of violations. It is the usual type of error that is returned from a validator.
83
type ViolationList struct {
84
	len   int
85
	first *ViolationListElement
86
	last  *ViolationListElement
87
}
88
89
// ViolationListElement points to violation build by validator. It also implements
90
// [Violation] and can be used as a proxy to underlying violation.
91
type ViolationListElement struct {
92
	next      *ViolationListElement
93
	violation Violation
94
}
95
96
// NewViolationList creates a new [ViolationList], that can be immediately populated with
97
// variadic arguments of violations.
98
func NewViolationList(violations ...Violation) *ViolationList {
99
	list := &ViolationList{}
100 1
	list.Append(violations...)
101 1
102
	return list
103 1
}
104
105
// Len returns length of the linked list.
106
func (list *ViolationList) Len() int {
107
	if list == nil {
108 1
		return 0
109 1
	}
110
111
	return list.len
112 1
}
113
114
// ForEach can be used to iterate over [ViolationList] by a callback function. If callback returns
115
// any error, then it will be returned as a result of ForEach function.
116
func (list *ViolationList) ForEach(f func(i int, violation Violation) error) error {
117
	if list == nil {
118 1
		return nil
119 1
	}
120 1
121 1
	i := 0
122 1
	for e := list.first; e != nil; e = e.next {
123
		err := f(i, e.violation)
124 1
		if err != nil {
125
			return err
126
		}
127 1
		i++
128
	}
129
130
	return nil
131
}
132 1
133
// First returns the first element of the linked list.
134
func (list *ViolationList) First() *ViolationListElement {
135
	return list.first
136
}
137 1
138
// Last returns the last element of the linked list.
139
func (list *ViolationList) Last() *ViolationListElement {
140
	return list.last
141
}
142 1
143 1
// Append appends violations to the end of the linked list.
144 1
func (list *ViolationList) Append(violations ...Violation) {
145 1
	for i := range violations {
146 1
		element := &ViolationListElement{violation: violations[i]}
147
		if list.first == nil {
148 1
			list.first = element
149 1
			list.last = element
150
		} else {
151
			list.last.next = element
152
			list.last = element
153 1
		}
154
	}
155
156
	list.len += len(violations)
157
}
158 1
159 1
// Join is used to append the given violation list to the end of the current list.
160
func (list *ViolationList) Join(violations *ViolationList) {
161
	if violations == nil || violations.len == 0 {
162 1
		return
163 1
	}
164 1
165
	if list.first == nil {
166 1
		list.first = violations.first
167 1
		list.last = violations.last
168
	} else {
169
		list.last.next = violations.first
170 1
		list.last = violations.last
171
	}
172
173
	list.len += violations.len
174
}
175 1
176 1
// Error returns a formatted list of violations as a string.
177
func (list *ViolationList) Error() string {
178
	if list == nil || list.len == 0 {
179 1
		return "the list of violations is empty, it looks like you forgot to use the AsError method somewhere"
180 1
	}
181
182 1
	return list.String()
183 1
}
184 1
185 1
// String converts list of violations into a string.
186 1
func (list *ViolationList) String() string {
187
	return list.toString(" ")
188 1
}
189 1
190
// Format formats the list of violations according to the [fmt.Formatter] interface.
191
// Verbs '%v', '%s', '%q' formats violation list into a single line string delimited by space.
192
// Verb with flag '%+v' formats violation list into a multi-line string.
193 1
func (list *ViolationList) Format(f fmt.State, verb rune) {
194
	switch verb {
195
	case 'v':
196 1
		if f.Flag('+') {
197
			_, _ = io.WriteString(f, list.toString("\n\t"))
198
		} else {
199
			_, _ = io.WriteString(f, list.toString(" "))
200
		}
201
	case 's', 'q':
202
		_, _ = io.WriteString(f, list.toString(" "))
203 1
	}
204 1
}
205 1
206 1
func (list *ViolationList) toString(delimiter string) string {
207 1
	if list == nil || list.len == 0 {
208 1
		return ""
209
	}
210
	if list.len == 1 {
211 1
		return list.first.violation.Error()
212
	}
213
214
	var s strings.Builder
215
	s.Grow(32 * list.len)
216
	s.WriteString("violations:")
217 1
218 1
	i := 0
219 1
	for e := list.first; e != nil; e = e.next {
220
		v := e.violation
221
		if i > 0 {
222
			s.WriteString(";")
223 1
		}
224
		s.WriteString(delimiter)
225
		s.WriteString("#" + strconv.Itoa(i))
226
		if v.PropertyPath() != nil {
227
			s.WriteString(` at "` + v.PropertyPath().String() + `"`)
228 1
		}
229
		s.WriteString(`: "` + v.Message() + `"`)
230 1
		i++
231 1
	}
232 1
233
	return s.String()
234
}
235
236 1
// AppendFromError appends a single violation or a slice of violations into the end of a given slice.
237
// If an error does not implement the [Violation] or [ViolationList] interface, it will return an error itself.
238
// Otherwise nil will be returned.
239
func (list *ViolationList) AppendFromError(err error) error {
240
	if violation, ok := UnwrapViolation(err); ok {
241
		list.Append(violation)
242 1
	} else if violationList, ok := UnwrapViolationList(err); ok {
243 1
		list.Join(violationList)
244
	} else if err != nil {
245
		return err
246 1
	}
247
248
	return nil
249
}
250
251 1
// Is used to check that at least one of the violations contains the specific static error.
252
func (list *ViolationList) Is(target error) bool {
253 1
	for e := list.first; e != nil; e = e.next {
254 1
		if e.violation.Is(target) {
255 1
			return true
256 1
		}
257
	}
258
259 1
	return false
260
}
261
262
// Filter returns a new list of violations with violations of given codes.
263
func (list *ViolationList) Filter(errs ...error) *ViolationList {
264
	filtered := &ViolationList{}
265 1
266 1
	for e := list.first; e != nil; e = e.next {
267 1
		for _, err := range errs {
268 1
			if e.violation.Is(err) {
269 1
				filtered.Append(e.violation)
270 1
			}
271
		}
272
	}
273 1
274 1
	return filtered
275 1
}
276
277 1
// AsError converts the list of violations to an error. This method correctly handles cases where
278
// the list of violations is empty. It returns nil on an empty list, indicating that the validation was successful.
279 1
func (list *ViolationList) AsError() error {
280
	if list == nil || list.len == 0 {
281 1
		return nil
282
	}
283
284
	return list
285
}
286 1
287
// AsSlice converts underlying linked list into slice of [Violation].
288
func (list *ViolationList) AsSlice() []Violation {
289
	violations := make([]Violation, list.len)
290
291 1
	i := 0
292
	for e := list.first; e != nil; e = e.next {
293
		violations[i] = e.violation
294
		i++
295 1
	}
296
297
	return violations
298
}
299 1
300
// MarshalJSON marshals the linked list into JSON. Usually, you should use
301
// [json.Marshal] function for marshaling purposes.
302
func (list *ViolationList) MarshalJSON() ([]byte, error) {
303
	b := bytes.Buffer{}
304
	b.WriteRune('[')
305
	i := 0
306
	for e := list.first; e != nil; e = e.next {
307 1
		data, err := json.Marshal(e.violation)
308
		if err != nil {
309
			return nil, fmt.Errorf("marshal violation at %d: %w", i, err)
310
		}
311
		b.Write(data)
312
		if e.next != nil {
313
			b.WriteRune(',')
314
		}
315
		i++
316
	}
317
	b.WriteRune(']')
318
319 1
	return b.Bytes(), nil
320
}
321
322
// Next returns the next element of the linked list.
323
func (element *ViolationListElement) Next() *ViolationListElement {
324 1
	return element.next
325
}
326 1
327
// Violation returns underlying violation value.
328
func (element *ViolationListElement) Violation() Violation {
329
	return element.violation
330
}
331 1
332
// Unwrap returns underlying static error. This error can be used as a unique, short, and semantic code
333 1
// that can be used to test for specific violation by [errors.Is] from standard library.
334
func (element *ViolationListElement) Unwrap() error {
335
	return element.violation.Unwrap()
336
}
337
338 1
func (element *ViolationListElement) Error() string {
339
	return element.violation.Error()
340 1
}
341
342 1
// Is can be used to check that the violation contains one of the specific static errors.
343
func (element *ViolationListElement) Is(target error) bool {
344
	return element.violation.Is(target)
345
}
346
347 1
func (element *ViolationListElement) Message() string {
348
	return element.violation.Message()
349 1
}
350
351 1
func (element *ViolationListElement) MessageTemplate() string {
352
	return element.violation.MessageTemplate()
353
}
354
355
func (element *ViolationListElement) Parameters() []TemplateParameter {
356
	return element.violation.Parameters()
357
}
358
359
func (element *ViolationListElement) PropertyPath() *PropertyPath {
360
	return element.violation.PropertyPath()
361
}
362
363 1
// IsViolation can be used to verify that the error implements the [Violation] interface.
364 1
func IsViolation(err error) bool {
365 1
	var violation Violation
366
367
	return errors.As(err, &violation)
368
}
369 1
370
// IsViolationList can be used to verify that the error implements the [ViolationList].
371
func IsViolationList(err error) bool {
372
	var violations *ViolationList
373 1
374 1
	return errors.As(err, &violations)
375 1
}
376
377 1
// UnwrapViolation is a short function to unwrap [Violation] from the error.
378
func UnwrapViolation(err error) (Violation, bool) {
379
	var violation Violation
380
381 1
	as := errors.As(err, &violation)
382 1
383 1
	return violation, as
384
}
385 1
386
// UnwrapViolationList is a short function to unwrap [ViolationList] from the error.
387
func UnwrapViolationList(err error) (*ViolationList, bool) {
388
	var violations *ViolationList
389 1
390
	as := errors.As(err, &violations)
391
392
	return violations, as
393 1
}
394
395
type internalViolation struct {
396
	err             error
397
	message         string
398
	messageTemplate string
399
	parameters      []TemplateParameter
400
	propertyPath    *PropertyPath
401
}
402
403
func (v *internalViolation) Unwrap() error {
404
	return v.err
405 1
}
406
407
func (v *internalViolation) Is(target error) bool {
408
	return errors.Is(v.err, target)
409 1
}
410
411
func (v *internalViolation) Error() string {
412
	var s strings.Builder
413
	s.Grow(32)
414
	v.writeToBuilder(&s)
415
416
	return s.String()
417
}
418
419
func (v *internalViolation) writeToBuilder(s *strings.Builder) {
420
	s.WriteString("violation")
421
	if v.propertyPath != nil {
422
		s.WriteString(` at "` + v.propertyPath.String() + `"`)
423
	}
424
	s.WriteString(`: "` + v.message + `"`)
425 1
}
426
427
func (v *internalViolation) Message() string                 { return v.message }
428
func (v *internalViolation) MessageTemplate() string         { return v.messageTemplate }
429
func (v *internalViolation) Parameters() []TemplateParameter { return v.parameters }
430
func (v *internalViolation) PropertyPath() *PropertyPath     { return v.propertyPath }
431
432
func (v *internalViolation) MarshalJSON() ([]byte, error) {
433
	data := struct {
434
		Error        string        `json:"error,omitempty"`
435
		Message      string        `json:"message"`
436 1
		PropertyPath *PropertyPath `json:"propertyPath,omitempty"`
437
	}{
438 1
		Message:      v.message,
439 1
		PropertyPath: v.propertyPath,
440 1
	}
441
	if v.err != nil {
442
		data.Error = v.err.Error()
443
	}
444 1
445
	return json.Marshal(data)
446
}
447
448
// BuiltinViolationFactory used as a default factory for creating a violations.
449
// It translates and renders message templates.
450
type BuiltinViolationFactory struct {
451
	translator Translator
452
}
453
454
// NewViolationFactory creates a new [BuiltinViolationFactory] for creating a violations.
455
func NewViolationFactory(translator Translator) *BuiltinViolationFactory {
456
	return &BuiltinViolationFactory{translator: translator}
457
}
458
459
// CreateViolation creates a new instance of [Violation].
460
func (factory *BuiltinViolationFactory) CreateViolation(
461
	err error,
462
	messageTemplate string,
463
	pluralCount int,
464
	parameters []TemplateParameter,
465
	propertyPath *PropertyPath,
466
	lang language.Tag,
467 1
) Violation {
468
	message := factory.translator.Translate(lang, messageTemplate, pluralCount)
469
470
	for i := range parameters {
471
		if parameters[i].NeedsTranslation {
472 1
			parameters[i].Value = factory.translator.Translate(lang, parameters[i].Value, 0)
473
		}
474
	}
475
476
	return &internalViolation{
477
		err:             err,
478
		message:         renderMessage(message, parameters),
479
		messageTemplate: messageTemplate,
480
		parameters:      parameters,
481 1
		propertyPath:    propertyPath,
482
	}
483 1
}
484
485
// ViolationBuilder used to build an instance of a [Violation].
486
type ViolationBuilder struct {
487
	err             error
488 1
	messageTemplate string
489
	pluralCount     int
490 1
	parameters      []TemplateParameter
491
	propertyPath    *PropertyPath
492
	language        language.Tag
493
494
	violationFactory ViolationFactory
495 1
}
496
497 1
// NewViolationBuilder creates a new [ViolationBuilder].
498
func NewViolationBuilder(factory ViolationFactory) *ViolationBuilder {
499
	return &ViolationBuilder{violationFactory: factory}
500
}
501
502 1
// BuildViolation creates a new [ViolationBuilder] for composing [Violation] object fluently.
503
func (b *ViolationBuilder) BuildViolation(err error, message string) *ViolationBuilder {
504 1
	return &ViolationBuilder{
505
		err:              err,
506
		messageTemplate:  message,
507
		violationFactory: b.violationFactory,
508
	}
509 1
}
510
511 1
// SetPropertyPath resets a base property path of violated attributes.
512
func (b *ViolationBuilder) SetPropertyPath(path *PropertyPath) *ViolationBuilder {
513
	b.propertyPath = path
514
515
	return b
516
}
517 1
518
// WithParameters sets template parameters that can be injected into the violation message.
519
func (b *ViolationBuilder) WithParameters(parameters ...TemplateParameter) *ViolationBuilder {
520
	b.parameters = parameters
521
522
	return b
523
}
524
525
// WithParameter adds one parameter into a slice of parameters.
526
func (b *ViolationBuilder) WithParameter(name, value string) *ViolationBuilder {
527
	b.parameters = append(b.parameters, TemplateParameter{Key: name, Value: value})
528
529
	return b
530
}
531
532
// At appends a property path of violated attribute.
533
func (b *ViolationBuilder) At(path ...PropertyPathElement) *ViolationBuilder {
534
	b.propertyPath = b.propertyPath.With(path...)
535
536
	return b
537
}
538
539
// AtProperty adds a property name to property path of violated attribute.
540
func (b *ViolationBuilder) AtProperty(propertyName string) *ViolationBuilder {
541
	b.propertyPath = b.propertyPath.WithProperty(propertyName)
542
543
	return b
544
}
545
546
// AtIndex adds an array index to property path of violated attribute.
547
func (b *ViolationBuilder) AtIndex(index int) *ViolationBuilder {
548
	b.propertyPath = b.propertyPath.WithIndex(index)
549
550
	return b
551
}
552
553
// WithPluralCount sets a plural number that will be used for message pluralization during translations.
554
func (b *ViolationBuilder) WithPluralCount(pluralCount int) *ViolationBuilder {
555
	b.pluralCount = pluralCount
556
557
	return b
558
}
559
560
// WithLanguage sets language that will be used to translate the violation message.
561
func (b *ViolationBuilder) WithLanguage(tag language.Tag) *ViolationBuilder {
562
	b.language = tag
563
564
	return b
565
}
566
567
// Create creates a new violation with given parameters and returns it.
568
// Violation is created by calling the [ViolationFactory.CreateViolation].
569
func (b *ViolationBuilder) Create() Violation {
570
	return b.violationFactory.CreateViolation(
571
		b.err,
572
		b.messageTemplate,
573
		b.pluralCount,
574
		b.parameters,
575
		b.propertyPath,
576
		b.language,
577
	)
578
}
579
580
// ViolationListBuilder is used to build a [ViolationList] by fluent interface.
581
type ViolationListBuilder struct {
582
	violations       *ViolationList
583
	violationFactory ViolationFactory
584
585
	propertyPath *PropertyPath
586
	language     language.Tag
587
}
588
589
// ViolationListElementBuilder is used to build [Violation] that will be added into [ViolationList]
590
// of the [ViolationListBuilder].
591
type ViolationListElementBuilder struct {
592
	listBuilder *ViolationListBuilder
593
594
	err             error
595
	messageTemplate string
596
	pluralCount     int
597
	parameters      []TemplateParameter
598
	propertyPath    *PropertyPath
599
}
600
601
// NewViolationListBuilder creates a new [ViolationListBuilder].
602
func NewViolationListBuilder(factory ViolationFactory) *ViolationListBuilder {
603
	return &ViolationListBuilder{violationFactory: factory, violations: NewViolationList()}
604
}
605
606
// BuildViolation initiates a builder for violation that will be added into [ViolationList].
607
func (b *ViolationListBuilder) BuildViolation(err error, message string) *ViolationListElementBuilder {
608
	return &ViolationListElementBuilder{
609
		listBuilder:     b,
610
		err:             err,
611
		messageTemplate: message,
612
		propertyPath:    b.propertyPath,
613
	}
614
}
615
616
// AddViolation can be used to quickly add a new violation using only code, message
617
// and optional property path elements.
618
func (b *ViolationListBuilder) AddViolation(err error, message string, path ...PropertyPathElement) *ViolationListBuilder {
619
	return b.add(err, message, 0, nil, b.propertyPath.With(path...))
620
}
621
622
// SetPropertyPath resets a base property path of violated attributes.
623
func (b *ViolationListBuilder) SetPropertyPath(path *PropertyPath) *ViolationListBuilder {
624
	b.propertyPath = path
625
626
	return b
627
}
628
629
// At appends a property path of violated attribute.
630
func (b *ViolationListBuilder) At(path ...PropertyPathElement) *ViolationListBuilder {
631
	b.propertyPath = b.propertyPath.With(path...)
632
633
	return b
634
}
635
636
// AtProperty adds a property name to the base property path of violated attributes.
637
func (b *ViolationListBuilder) AtProperty(propertyName string) *ViolationListBuilder {
638
	b.propertyPath = b.propertyPath.WithProperty(propertyName)
639
640
	return b
641
}
642
643
// AtIndex adds an array index to the base property path of violated attributes.
644
func (b *ViolationListBuilder) AtIndex(index int) *ViolationListBuilder {
645
	b.propertyPath = b.propertyPath.WithIndex(index)
646
647
	return b
648
}
649
650
// Create returns a [ViolationList] with built violations.
651
func (b *ViolationListBuilder) Create() *ViolationList {
652
	return b.violations
653
}
654
655
func (b *ViolationListBuilder) add(
656
	err error,
657
	template string,
658
	count int,
659
	parameters []TemplateParameter,
660
	path *PropertyPath,
661
) *ViolationListBuilder {
662
	b.violations.Append(b.violationFactory.CreateViolation(
663
		err,
664
		template,
665
		count,
666
		parameters,
667
		path,
668
		b.language,
669
	))
670
671
	return b
672
}
673
674
// WithLanguage sets language that will be used to translate the violation message.
675
func (b *ViolationListBuilder) WithLanguage(tag language.Tag) *ViolationListBuilder {
676
	b.language = tag
677
678
	return b
679
}
680
681
// WithParameters sets template parameters that can be injected into the violation message.
682
func (b *ViolationListElementBuilder) WithParameters(parameters ...TemplateParameter) *ViolationListElementBuilder {
683
	b.parameters = parameters
684
685
	return b
686
}
687
688
// WithParameter adds one parameter into a slice of parameters.
689
func (b *ViolationListElementBuilder) WithParameter(name, value string) *ViolationListElementBuilder {
690
	b.parameters = append(b.parameters, TemplateParameter{Key: name, Value: value})
691
692
	return b
693
}
694
695
// At appends a property path of violated attribute.
696
func (b *ViolationListElementBuilder) At(path ...PropertyPathElement) *ViolationListElementBuilder {
697
	b.propertyPath = b.propertyPath.With(path...)
698
699
	return b
700
}
701
702
// AtProperty adds a property name to property path of violated attribute.
703
func (b *ViolationListElementBuilder) AtProperty(propertyName string) *ViolationListElementBuilder {
704
	b.propertyPath = b.propertyPath.WithProperty(propertyName)
705
706
	return b
707
}
708
709
// AtIndex adds an array index to property path of violated attribute.
710
func (b *ViolationListElementBuilder) AtIndex(index int) *ViolationListElementBuilder {
711
	b.propertyPath = b.propertyPath.WithIndex(index)
712
713
	return b
714
}
715
716
// WithPluralCount sets a plural number that will be used for message pluralization during translations.
717
func (b *ViolationListElementBuilder) WithPluralCount(pluralCount int) *ViolationListElementBuilder {
718
	b.pluralCount = pluralCount
719
720
	return b
721
}
722
723
// Add creates a [Violation] and appends it into the end of the [ViolationList].
724
// It returns a [ViolationListBuilder] to continue process of creating a [ViolationList].
725
func (b *ViolationListElementBuilder) Add() *ViolationListBuilder {
726
	return b.listBuilder.add(b.err, b.messageTemplate, b.pluralCount, b.parameters, b.propertyPath)
727
}
728
729
func unwrapViolationList(err error) (*ViolationList, error) {
730
	violations := NewViolationList()
731
	fatal := violations.AppendFromError(err)
732
	if fatal != nil {
733
		return nil, fatal
734
	}
735
736
	return violations, nil
737
}
738