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