···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+}