Monorepo for Tangled tangled.org

appview/validator: add model validation for label defs

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 68bee501 df7a3879

verified
Changed files
+77
appview
validator
+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 + }