validate/identifiers.go   A
last analyzed

Size/Duplication

Total Lines 172
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 27
eloc 72
dl 0
loc 172
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A validate.convertUUIDError 0 11 4
A validate.newCharSet 0 8 2
B validate.ULID 0 21 6
A validate.AllowUUIDVersions 0 3 2
A validate.DenyNilUUID 0 3 2
A validate.AllowNonCanonicalUUIDFormats 0 3 2
A validate.charSet.Contains 0 4 1
B validate.UUID 0 29 8
1
package validate
2
3
import (
4
	"errors"
5
6
	"github.com/muonsoft/validation/internal/uuid"
7
)
8
9
var (
10
	ErrTooShort               = errors.New("too short")
11
	ErrTooLong                = errors.New("too long")
12
	ErrTooLarge               = errors.New("too large")
13
	ErrInvalidCharacters      = errors.New("invalid characters")
14
	ErrInvalidHyphenPlacement = errors.New("invalid hyphen placement")
15
	ErrInvalidVersion         = errors.New("invalid version")
16
	ErrIsNil                  = errors.New("is nil")
17
)
18
19
var ulidChars = newCharSet("0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz")
20
21
// ULID validates whether the value is a valid ULID (Universally Unique Lexicographically Sortable Identifier).
22
// See https://github.com/ulid/spec for ULID specifications.
23
//
24
// Possible errors:
25
//   - [ErrTooShort] on values with length less than 26;
26
//   - [ErrTooLong] on values with length greater than 26;
27
//   - [ErrInvalidCharacters] on values with unexpected characters;
28
//   - [ErrTooLarge] on too big value (larger than '7ZZZZZZZZZZZZZZZZZZZZZZZZZ').
29
func ULID(value string) error {
30
	if len(value) < 26 {
31
		return ErrTooShort
32
	}
33
	if len(value) > 26 {
34
		return ErrTooLong
35
	}
36
37
	for _, c := range value {
38
		if !ulidChars.Contains(c) {
39
			return ErrInvalidCharacters
40
		}
41
	}
42
43
	// Largest valid ULID is '7ZZZZZZZZZZZZZZZZZZZZZZZZZ'
44
	// See https://github.com/ulid/spec#overflow-errors-when-parsing-base32-strings
45
	if value[0] > '7' {
46
		return ErrTooLarge
47
	}
48
49
	return nil
50
}
51
52
// UUID validates whether a string value is a valid UUID (also known as GUID).
53
//
54
// By default, it uses strict mode and checks the UUID as specified in RFC 4122.
55
// To parse additional formats, use the [AllowNonCanonicalUUIDFormats] option.
56
//
57
// In addition, it checks if the UUID version matches one of
58
// the registered versions: 1, 2, 3, 4, 5, 6 or 7.
59
// Use [AllowUUIDVersions] to validate for a specific set of versions.
60
//
61
// Nil UUID ("00000000-0000-0000-0000-000000000000") values are considered as valid.
62
// Use [DenyNilUUID] to disallow nil value.
63
//
64
// Possible errors:
65
//   - [ErrTooShort] on values with length less than 36 (or 32 for non-canonical formats);
66
//   - [ErrTooLong] on values with length greater than 36 (or 45 for non-canonical formats);
67
//   - [ErrInvalidCharacters] on values with unexpected characters;
68
//   - [ErrInvalidHyphenPlacement] on invalid placements of hyphens;
69
//   - [ErrInvalidVersion] on a restricted versions;
70
//   - [ErrIsNil] on nil value (if [DenyNilUUID] options is enabled);
71
//   - [ErrInvalid] on other cases;
72
//
73
// See http://tools.ietf.org/html/rfc4122.
74
func UUID(value string, options ...func(o *UUIDOptions)) error {
75
	opts := &UUIDOptions{}
76
	for _, set := range options {
77
		set(opts)
78
	}
79
80
	var (
81
		u   uuid.UUID
82
		err error
83
	)
84
85
	if opts.isNonStrict {
86
		u, err = uuid.FromString(value)
87
	} else {
88
		u, err = uuid.CanonicalFromString(value)
89
	}
90
	if err != nil {
91
		return convertUUIDError(err)
92
	}
93
94
	if !u.IsNil() && !u.ValidVersion(opts.versions...) {
95
		return ErrInvalidVersion
96
	}
97
98
	if opts.isNotNil && u.IsNil() {
99
		return ErrIsNil
100
	}
101
102
	return nil
103
}
104
105
// AllowNonCanonicalUUIDFormats used to enable parsing UUID value from non-canonical formats.
106
//
107
// Following formats are supported:
108
//   - "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
109
//   - "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}",
110
//   - "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8"
111
//   - "6ba7b8109dad11d180b400c04fd430c8"
112
//   - "{6ba7b8109dad11d180b400c04fd430c8}",
113
//   - "urn:uuid:6ba7b8109dad11d180b400c04fd430c8".
114
func AllowNonCanonicalUUIDFormats() func(o *UUIDOptions) {
115
	return func(o *UUIDOptions) {
116
		o.isNonStrict = true
117
	}
118
}
119
120
// AllowUUIDVersions used to set valid versions of the UUID value.
121
// If the versions are empty, the UUID will be checked for compliance with the default
122
// registered versions: 1, 2, 3, 4, 5, 6 or 7.
123
func AllowUUIDVersions(versions ...byte) func(o *UUIDOptions) {
124
	return func(o *UUIDOptions) {
125
		o.versions = versions
126
	}
127
}
128
129
// DenyNilUUID used to treat nil UUID ("00000000-0000-0000-0000-000000000000") value as invalid.
130
func DenyNilUUID() func(o *UUIDOptions) {
131
	return func(o *UUIDOptions) {
132
		o.isNotNil = true
133
	}
134
}
135
136
// UUIDOptions are used to set up validation process of the [UUID] function.
137
type UUIDOptions struct {
138
	versions    []byte
139
	isNonStrict bool
140
	isNotNil    bool
141
}
142
143
//nolint:errorlint
144
func convertUUIDError(err error) error {
145
	switch err {
146
	case uuid.ErrTooShort:
147
		return ErrTooShort
148
	case uuid.ErrTooLong:
149
		return ErrTooLong
150
	case uuid.ErrInvalidHyphenPlacement:
151
		return ErrInvalidHyphenPlacement
152
	}
153
154
	return ErrInvalid
155
}
156
157
type charSet map[rune]struct{}
158
159
func (s charSet) Contains(c rune) bool {
160
	_, exist := s[c]
161
162
	return exist
163
}
164
165
func newCharSet(s string) charSet {
166
	chars := make(charSet, len(s))
167
168
	for _, c := range s {
169
		chars[c] = struct{}{}
170
	}
171
172
	return chars
173
}
174