+77
appview/validator/label.go
+77
appview/validator/label.go
···
1
+
package validator
2
+
3
+
import (
4
+
"fmt"
5
+
"regexp"
6
+
"strings"
7
+
8
+
"github.com/bluesky-social/indigo/atproto/syntax"
9
+
"golang.org/x/exp/slices"
10
+
"tangled.sh/tangled.sh/core/api/tangled"
11
+
"tangled.sh/tangled.sh/core/appview/db"
12
+
)
13
+
14
+
var (
15
+
// Label name should be alphanumeric with hyphens/underscores, but not start/end with them
16
+
labelNameRegex = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$`)
17
+
// Color should be a valid hex color
18
+
colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`)
19
+
// You can only label issues and pulls presently
20
+
validScopes = []syntax.NSID{tangled.RepoIssueNSID, tangled.RepoPullNSID}
21
+
)
22
+
23
+
func (v *Validator) ValidateLabelDefinition(label *db.LabelDefinition) error {
24
+
if label.Name == "" {
25
+
return fmt.Errorf("label name is empty")
26
+
}
27
+
if len(label.Name) > 40 {
28
+
return fmt.Errorf("label name too long (max 40 graphemes)")
29
+
}
30
+
if len(label.Name) < 1 {
31
+
return fmt.Errorf("label name too short (min 1 grapheme)")
32
+
}
33
+
if !labelNameRegex.MatchString(label.Name) {
34
+
return fmt.Errorf("label name contains invalid characters (use only letters, numbers, hyphens, and underscores)")
35
+
}
36
+
37
+
if !label.ValueType.IsConcreteType() {
38
+
return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType)
39
+
}
40
+
41
+
if label.ValueType.IsNull() && label.ValueType.IsEnumType() {
42
+
return fmt.Errorf("null type cannot be used in conjunction with enum type")
43
+
}
44
+
45
+
// validate scope (nsid format)
46
+
if label.Scope == "" {
47
+
return fmt.Errorf("scope is required")
48
+
}
49
+
if _, err := syntax.ParseNSID(string(label.Scope)); err != nil {
50
+
return fmt.Errorf("failed to parse scope: %w", err)
51
+
}
52
+
if !slices.Contains(validScopes, label.Scope) {
53
+
return fmt.Errorf("invalid scope: scope must be one of %q", validScopes)
54
+
}
55
+
56
+
// validate color if provided
57
+
if label.Color != nil {
58
+
color := strings.TrimSpace(*label.Color)
59
+
if color == "" {
60
+
// empty color is fine, set to nil
61
+
label.Color = nil
62
+
} else {
63
+
if !colorRegex.MatchString(color) {
64
+
return fmt.Errorf("color must be a valid hex color (e.g. #79FFE1 or #000)")
65
+
}
66
+
// expand 3-digit hex to 6-digit hex
67
+
if len(color) == 4 { // #ABC
68
+
color = fmt.Sprintf("#%c%c%c%c%c%c", color[1], color[1], color[2], color[2], color[3], color[3])
69
+
}
70
+
// convert to uppercase for consistency
71
+
color = strings.ToUpper(color)
72
+
label.Color = &color
73
+
}
74
+
}
75
+
76
+
return nil
77
+
}