it.IPConstraint.WithInvalidMessage   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 4
dl 0
loc 4
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
nop 2
1
package it
2
3
import (
4
	"context"
5
	"errors"
6
	"net"
7
	"net/url"
8
	"regexp"
9
10
	"github.com/muonsoft/validation"
11
	"github.com/muonsoft/validation/is"
12
	"github.com/muonsoft/validation/validate"
13
)
14
15
// IsEmail is used for simplified validation of an email address. It allows all values
16
// with an "@" symbol in, and a "." in the second host part of the email address.
17 1
func IsEmail() validation.StringFuncConstraint {
18
	return validation.OfStringBy(is.Email).
19
		WithError(validation.ErrInvalidEmail).
20
		WithMessage(validation.ErrInvalidEmail.Message())
21
}
22
23
// IsHTML5Email is used for validation of an email address based on pattern for HTML5
24
// (see https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address).
25
func IsHTML5Email() validation.StringFuncConstraint {
26
	return validation.OfStringBy(is.HTML5Email).
27
		WithError(validation.ErrInvalidEmail).
28 1
		WithMessage(validation.ErrInvalidEmail.Message())
29
}
30
31
// IsHostname validates that a value is a valid hostname. It checks that:
32
//   - each label within a valid hostname may be no more than 63 octets long;
33
//   - the total length of the hostname must not exceed 255 characters;
34
//   - hostname is fully qualified and include its top-level domain name
35
//     (for instance, example.com is valid but example is not);
36
//   - checks for reserved top-level domains according to RFC 2606
37
//     (hostnames containing them are not considered valid:
38
//     .example, .invalid, .localhost, and .test).
39
//
40
// If you do not want to check for top-level domains use [IsLooseHostname] version of constraint.
41
func IsHostname() validation.StringFuncConstraint {
42
	return validation.OfStringBy(is.StrictHostname).
43
		WithError(validation.ErrInvalidHostname).
44
		WithMessage(validation.ErrInvalidHostname.Message())
45
}
46
47 1
// IsLooseHostname validates that a value is a valid hostname. It checks that:
48
//   - each label within a valid hostname may be no more than 63 octets long;
49
//   - the total length of the hostname must not exceed 255 characters.
50
func IsLooseHostname() validation.StringFuncConstraint {
51
	return validation.OfStringBy(is.Hostname).
52
		WithError(validation.ErrInvalidHostname).
53
		WithMessage(validation.ErrInvalidHostname.Message())
54
}
55
56
// URLConstraint is used to validate URL string. This constraint doesn’t check that the host of the
57
// given URL really exists, because the information of the DNS records is not reliable.
58
//
59 1
// This constraint doesn't check the length of the URL. Use [LengthConstraint] to check the length of the given value.
60
type URLConstraint struct {
61
	isIgnored                   bool
62
	supportsRelativeSchema      bool
63
	schemas                     []string
64
	hosts                       []string
65
	hostPattern                 *regexp.Regexp
66
	restrictions                []func(u *url.URL) error
67
	groups                      []string
68
	invalidErr                  error
69
	prohibitedErr               error
70
	invalidMessageTemplate      string
71
	invalidMessageParameters    validation.TemplateParameterList
72
	prohibitedMessageTemplate   string
73
	prohibitedMessageParameters validation.TemplateParameterList
74
}
75
76
// IsURL creates a [URLConstraint] to validate an URL. By default, constraint checks
77
// only for the http:// and https:// schemas. Use the WithSchemas method to configure
78
// the list of expected schemas. Also, you can use WithRelativeSchema to enable support
79
// of the relative schema (without schema, e.g. "//example.com").
80
func IsURL() URLConstraint {
81
	return URLConstraint{
82
		schemas:                   []string{"http", "https"},
83
		invalidErr:                validation.ErrInvalidURL,
84
		invalidMessageTemplate:    validation.ErrInvalidURL.Message(),
85
		prohibitedErr:             validation.ErrProhibitedURL,
86 1
		prohibitedMessageTemplate: validation.ErrProhibitedURL.Message(),
87
	}
88
}
89
90
// WithRelativeSchema enables support of relative URL schema, which means that URL value
91
// may be treated as relative (without schema, e.g. "//example.com").
92
func (c URLConstraint) WithRelativeSchema() URLConstraint {
93
	c.supportsRelativeSchema = true
94
	return c
95 1
}
96 1
97
// WithSchemas is used to set up a list of accepted schemas. For example, if you also consider the ftp:// type URLs
98
// to be valid, redefine the schemas list, listing http, https, and also ftp.
99 1
// If the list is empty, then an error will be returned.
100
func (c URLConstraint) WithSchemas(schemas ...string) URLConstraint {
101
	c.schemas = schemas
102
	return c
103
}
104 1
105
// WithHosts is used to set up a list of accepted hosts. If the list is empty, then any host will be treated as valid.
106
func (c URLConstraint) WithHosts(hosts ...string) URLConstraint {
107
	c.hosts = hosts
108
	return c
109
}
110 1
111 1
// WithHostMatches is used to set up restricted host validation by regexp pattern.
112
// If pattern is nil, then validation will be ignored.
113
func (c URLConstraint) WithHostMatches(pattern *regexp.Regexp) URLConstraint {
114
	c.hostPattern = pattern
115
	return c
116
}
117
118 1
// WithRestriction is used to additionally check parsed URL by callback function.
119 1
func (c URLConstraint) WithRestriction(isAllowed func(u *url.URL) bool) URLConstraint {
120
	c.restrictions = append(c.restrictions, func(u *url.URL) error {
121
		if isAllowed(u) {
122
			return nil
123
		}
124 1
125 1
		return validate.ErrProhibited
126
	})
127
128
	return c
129
}
130
131
// WithError overrides default error for produced violation.
132
func (c URLConstraint) WithError(err error) URLConstraint {
133 1
	c.invalidErr = err
134 1
	return c
135 1
}
136
137
// WithMessage sets the violation message template. You can set custom template parameters
138
// for injecting its values into the final message. Also, you can use default parameters:
139
//
140
//	{{ value }} - the current (invalid) value.
141 1
func (c URLConstraint) WithMessage(template string, parameters ...validation.TemplateParameter) URLConstraint {
142 1
	c.invalidMessageTemplate = template
143
	c.invalidMessageParameters = parameters
144
	return c
145
}
146
147 1
// WithProhibitedError overrides default error for produced violation on a prohibited value.
148 1
func (c URLConstraint) WithProhibitedError(err error) URLConstraint {
149
	c.prohibitedErr = err
150
	return c
151
}
152 1
153 1
// WithProhibitedMessage sets the violation message template for violation on a prohibited value.
154
// You can set custom template parameters for injecting its values into the final message.
155
// Also, you can use default parameters:
156 1
//
157 1
//	{{ value }} - the current (invalid) value.
158 1
func (c URLConstraint) WithProhibitedMessage(template string, parameters ...validation.TemplateParameter) URLConstraint {
159
	c.prohibitedMessageTemplate = template
160 1
	c.prohibitedMessageParameters = parameters
161 1
	return c
162
}
163
164 1
// When enables conditional validation of this constraint. If the expression evaluates to false,
165
// then the constraint will be ignored.
166
func (c URLConstraint) When(condition bool) URLConstraint {
167
	c.isIgnored = !condition
168
	return c
169
}
170
171
// WhenGroups enables conditional validation of the constraint by using the validation groups.
172
func (c URLConstraint) WhenGroups(groups ...string) URLConstraint {
173
	c.groups = groups
174
	return c
175
}
176
177
func (c URLConstraint) ValidateString(ctx context.Context, validator *validation.Validator, value *string) error {
178
	if len(c.schemas) == 0 {
179
		return validator.CreateConstraintError("URLConstraint", "empty list of schemas")
180
	}
181
	if c.isIgnored || validator.IsIgnoredForGroups(c.groups...) || value == nil || *value == "" {
182
		return nil
183
	}
184
185
	if err := validate.URL(*value, c.getRestrictions()...); err != nil {
186
		if errors.Is(err, validate.ErrRestrictedHost) || errors.Is(err, validate.ErrProhibited) {
187
			return c.newProhibitedViolation(ctx, validator, *value)
188
		}
189
190
		return c.newInvalidViolation(ctx, validator, *value)
191
	}
192
193 1
	return nil
194
}
195
196
func (c URLConstraint) getRestrictions() []func(u *url.URL) error {
197
	schemas := c.schemas
198 1
	if c.supportsRelativeSchema {
199
		schemas = append(schemas, "")
200
	}
201
202
	restrictions := []func(u *url.URL) error{validate.RestrictURLSchemas(schemas...)}
203 1
	if len(c.hosts) > 0 {
204
		restrictions = append(restrictions, validate.RestrictURLHosts(c.hosts...))
205
	}
206
	if c.hostPattern != nil {
207 1
		restrictions = append(restrictions, validate.RestrictURLHostByPattern(c.hostPattern))
208
	}
209
	restrictions = append(restrictions, c.restrictions...)
210
211
	return restrictions
212
}
213
214
func (c URLConstraint) newInvalidViolation(ctx context.Context, validator *validation.Validator, value string) error {
215
	return validator.BuildViolation(ctx, c.invalidErr, c.invalidMessageTemplate).
216
		WithParameters(
217
			c.invalidMessageParameters.Prepend(
218 1
				validation.TemplateParameter{Key: "{{ value }}", Value: value},
219
			)...,
220
		).
221
		Create()
222
}
223
224
func (c URLConstraint) newProhibitedViolation(ctx context.Context, validator *validation.Validator, value string) error {
225
	return validator.BuildViolation(ctx, c.prohibitedErr, c.prohibitedMessageTemplate).
226
		WithParameters(
227
			c.prohibitedMessageParameters.Prepend(
228
				validation.TemplateParameter{Key: "{{ value }}", Value: value},
229 1
			)...,
230 1
		).
231
		Create()
232
}
233
234
// IPConstraint is used to validate IP address. You can check for different versions
235 1
// and restrict some ranges by additional options.
236 1
type IPConstraint struct {
237
	isIgnored    bool
238
	validate     func(value string, restrictions ...func(ip net.IP) error) error
239
	restrictions []func(ip net.IP) error
240
241 1
	groups []string
242 1
243
	invalidErr    error
244
	prohibitedErr error
245
246
	invalidMessageTemplate      string
247 1
	invalidMessageParameters    validation.TemplateParameterList
248 1
	prohibitedMessageTemplate   string
249
	prohibitedMessageParameters validation.TemplateParameterList
250
}
251
252
// IsIP creates an IPConstraint to validate an IP address (IPv4 or IPv6).
253
func IsIP() IPConstraint {
254
	return newIPConstraint(validate.IP)
255
}
256
257 1
// IsIPv4 creates an IPConstraint to validate an IPv4 address.
258 1
func IsIPv4() IPConstraint {
259 1
	return newIPConstraint(validate.IPv4)
260
}
261
262
// IsIPv6 creates an IPConstraint to validate an IPv4 address.
263
func IsIPv6() IPConstraint {
264
	return newIPConstraint(validate.IPv6)
265
}
266
267
func newIPConstraint(validate func(value string, restrictions ...func(ip net.IP) error) error) IPConstraint {
268 1
	return IPConstraint{
269 1
		validate:                  validate,
270 1
		invalidErr:                validation.ErrInvalidIP,
271
		prohibitedErr:             validation.ErrProhibitedIP,
272
		invalidMessageTemplate:    validation.ErrInvalidIP.Message(),
273
		prohibitedMessageTemplate: validation.ErrProhibitedIP.Message(),
274
	}
275
}
276 1
277 1
// DenyPrivateIP denies using of private IPs according to RFC 1918 (IPv4 addresses)
278
// and RFC 4193 (IPv6 addresses).
279
func (c IPConstraint) DenyPrivateIP() IPConstraint {
280
	c.restrictions = append(c.restrictions, validate.DenyPrivateIP())
281
	return c
282 1
}
283 1
284
// DenyIP can be used to deny custom range of IP addresses.
285
func (c IPConstraint) DenyIP(restrict func(ip net.IP) bool) IPConstraint {
286
	c.restrictions = append(c.restrictions, func(ip net.IP) error {
287 1
		if restrict(ip) {
288 1
			return validate.ErrProhibited
289
		}
290
		return nil
291 1
	})
292
293
	return c
294
}
295 1
296 1
// WithInvalidError overrides default underlying error for violation produced on invalid IP case.
297 1
func (c IPConstraint) WithInvalidError(err error) IPConstraint {
298
	c.invalidErr = err
299
	return c
300 1
}
301 1
302
// WithProhibitedError overrides default underlying error for violation produced on prohibited IP case.
303 1
func (c IPConstraint) WithProhibitedError(err error) IPConstraint {
304 1
	c.prohibitedErr = err
305 1
	return c
306
}
307 1
308 1
// WithInvalidMessage sets the violation message template for invalid IP case.
309
// You can set custom template parameters for injecting its values into the final message.
310
// Also, you can use default parameters:
311 1
//
312
//	{{ value }} - the current (invalid) value.
313
func (c IPConstraint) WithInvalidMessage(template string, parameters ...validation.TemplateParameter) IPConstraint {
314
	c.invalidMessageTemplate = template
315
	c.invalidMessageParameters = parameters
316
	return c
317
}
318
319
// WithProhibitedMessage sets the violation message template for prohibited IP case.
320
// You can set custom template parameters for injecting its values into the final message.
321
// Also, you can use default parameters:
322
//
323
//	{{ value }} - the current (invalid) value.
324
func (c IPConstraint) WithProhibitedMessage(template string, parameters ...validation.TemplateParameter) IPConstraint {
325
	c.prohibitedMessageTemplate = template
326
	c.prohibitedMessageParameters = parameters
327
	return c
328
}
329
330
// When enables conditional validation of this constraint. If the expression evaluates to false,
331
// then the constraint will be ignored.
332
func (c IPConstraint) When(condition bool) IPConstraint {
333
	c.isIgnored = !condition
334
	return c
335
}
336
337
// WhenGroups enables conditional validation of the constraint by using the validation groups.
338
func (c IPConstraint) WhenGroups(groups ...string) IPConstraint {
339
	c.groups = groups
340
	return c
341
}
342
343
func (c IPConstraint) ValidateString(ctx context.Context, validator *validation.Validator, value *string) error {
344
	if c.isIgnored || validator.IsIgnoredForGroups(c.groups...) || value == nil || *value == "" {
345
		return nil
346
	}
347
348
	return c.validateIP(ctx, validator, *value)
349
}
350
351
func (c IPConstraint) validateIP(ctx context.Context, validator *validation.Validator, value string) error {
352
	err := c.validate(value, c.restrictions...)
353
	if err == nil {
354
		return nil
355
	}
356
357
	var builder *validation.ViolationBuilder
358
	var parameters validation.TemplateParameterList
359
360
	if errors.Is(err, validate.ErrProhibited) {
361
		builder = validator.BuildViolation(ctx, c.prohibitedErr, c.prohibitedMessageTemplate)
362
		parameters = c.prohibitedMessageParameters
363
	} else {
364
		builder = validator.BuildViolation(ctx, c.invalidErr, c.invalidMessageTemplate)
365
		parameters = c.invalidMessageParameters
366
	}
367
368
	return builder.
369
		WithParameters(
370
			parameters.Prepend(
371
				validation.TemplateParameter{Key: "{{ value }}", Value: value},
372
			)...,
373
		).
374
		Create()
375
}
376