/
slug.go
206 lines (166 loc) · 5.03 KB
/
slug.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
package slug
import (
"fmt"
"regexp"
"strings"
"k8s.io/apimachinery/pkg/util/validation"
"github.com/werf/werf/pkg/util"
)
const slugSeparator = "-"
var (
DefaultSlugMaxSize = 42 // legacy
dockerTagRegexp = regexp.MustCompile(`^[\w][\w.-]*$`)
dockerTagMaxSize = 128
projectNameRegex = regexp.MustCompile(`^(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$`)
projectNameMaxSize = 50
kubernetesNamespaceMaxSize = 63
helmReleaseMaxSize = 53
)
func Slug(data string) string {
return slugify(data)
}
func LimitedSlug(data string, slugMaxSize int) string {
if len(data) == 0 || slugify(data) == data && len(data) <= slugMaxSize {
return data
}
return slug(data, slugMaxSize)
}
func Project(name string) string {
if err := validateProject(name); err != nil {
res := slugify(name)
if len(res) > projectNameMaxSize {
res = res[:projectNameMaxSize]
}
return res
}
return name
}
func ValidateProject(name string) error {
return validateProject(name)
}
func validateProject(name string) error {
if shouldNotBeSlugged(name, projectNameRegex, projectNameMaxSize) {
return nil
}
return fmt.Errorf("project name should comply with regex %q and be maximum %d chars", projectNameRegex, projectNameMaxSize)
}
func DockerTag(name string) string {
if err := ValidateDockerTag(name); err != nil {
return slug(name, dockerTagMaxSize)
}
return name
}
func ValidateDockerTag(name string) error {
if shouldNotBeSlugged(name, dockerTagRegexp, dockerTagMaxSize) {
return nil
}
return fmt.Errorf(`%q is not a valid docker tag
- a tag name must be valid ASCII and may contain lowercase and uppercase letters, digits, underscores, periods and dashes;
- a tag name may not start with a period or a dash and may contain a maximum of 128 characters.`, name)
}
func KubernetesNamespace(name string) string {
if err := validateKubernetesNamespace(name); err != nil {
return slug(name, kubernetesNamespaceMaxSize)
}
return name
}
func ValidateKubernetesNamespace(namespace string) error {
return validateKubernetesNamespace(namespace)
}
func validateKubernetesNamespace(name string) error {
errorMsgPrefix := fmt.Sprintf("kubernetes namespace should be a valid DNS-1123 subdomain")
if len(name) == 0 {
return nil
} else if len(name) > kubernetesNamespaceMaxSize {
return fmt.Errorf("%s: %q is %d chars long", errorMsgPrefix, name, len(name))
} else if msgs := validation.IsDNS1123Subdomain(name); len(msgs) > 0 {
return fmt.Errorf("%s: %s", errorMsgPrefix, strings.Join(msgs, ", "))
}
return nil
}
func HelmRelease(name string) string {
if err := validateHelmRelease(name); err != nil {
return slug(name, helmReleaseMaxSize)
}
return name
}
func ValidateHelmRelease(name string) error {
return validateHelmRelease(name)
}
func validateHelmRelease(name string) error {
errorMsgPrefix := fmt.Sprintf("helm release name should be a valid DNS-1123 subdomain and be maximum %d chars", helmReleaseMaxSize)
if len(name) == 0 {
return nil
} else if len(name) > helmReleaseMaxSize {
return fmt.Errorf("%s: %q is %d chars long", errorMsgPrefix, name, len(name))
} else if msgs := validation.IsDNS1123Subdomain(name); len(msgs) > 0 {
return fmt.Errorf("%s: %s", errorMsgPrefix, strings.Join(msgs, ", "))
}
return nil
}
func shouldNotBeSlugged(data string, regexp *regexp.Regexp, maxSize int) bool {
return len(data) == 0 || regexp.Match([]byte(data)) && len(data) <= maxSize
}
func slug(data string, maxSize int) string {
sluggedData := slugify(data)
murmurHash := util.MurmurHash(data)
var slugParts []string
if sluggedData != "" {
croppedSluggedData := cropSluggedData(sluggedData, murmurHash, maxSize)
if strings.HasPrefix(croppedSluggedData, "-") {
slugParts = append(slugParts, croppedSluggedData[:len(croppedSluggedData)-1])
} else {
slugParts = append(slugParts, croppedSluggedData)
}
}
slugParts = append(slugParts, murmurHash)
consistentUniqSlug := strings.Join(slugParts, slugSeparator)
return consistentUniqSlug
}
func cropSluggedData(data string, hash string, maxSize int) string {
var index int
maxLength := maxSize - len(hash) - len(slugSeparator)
if len(data) > maxLength {
index = maxLength
} else {
index = len(data)
}
return data[:index]
}
func slugify(data string) string {
var result []rune
var isCursorDash bool
var isPreviousDash bool
var isStartedDash, isDoubledDash bool
isResultEmpty := true
for _, r := range data {
cursor := algorithm(string(r))
if cursor == "" {
continue
}
isCursorDash = cursor == "-"
isStartedDash = isCursorDash && isResultEmpty
isDoubledDash = isCursorDash && !isResultEmpty && isPreviousDash
if isStartedDash || isDoubledDash {
continue
}
result = append(result, []rune(cursor)...)
isPreviousDash = isCursorDash
isResultEmpty = false
}
isEndedDash := !isResultEmpty && isCursorDash
if isEndedDash {
return string(result[:len(result)-1])
}
return string(result)
}
func algorithm(data string) string {
var result string
for ind := range data {
char, ok := mapping[string([]rune(data)[ind])]
if ok {
result += char
}
}
return result
}