Passed
Pull Request — main (#22)
by Igor
01:48
created

violations.go   A

Size/Duplication

Total Lines 325
Duplicated Lines 0 %

Test Coverage

Coverage 96.88%

Importance

Changes 0
Metric Value
cc 36
eloc 172
dl 0
loc 325
ccs 62
cts 64
cp 0.9688
crap 36.0393
rs 9.52
c 0
b 0
f 0

26 Methods

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