this repo has no description
1package validator 2 3import ( 4 "context" 5 "fmt" 6 "regexp" 7 "strings" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "golang.org/x/exp/slices" 11 "tangled.sh/tangled.sh/core/api/tangled" 12 "tangled.sh/tangled.sh/core/appview/db" 13) 14 15var ( 16 // Label name should be alphanumeric with hyphens/underscores, but not start/end with them 17 labelNameRegex = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$`) 18 // Color should be a valid hex color 19 colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`) 20 // You can only label issues and pulls presently 21 validScopes = []syntax.NSID{tangled.RepoIssueNSID, tangled.RepoPullNSID} 22) 23 24func (v *Validator) ValidateLabelDefinition(label *db.LabelDefinition) error { 25 if label.Name == "" { 26 return fmt.Errorf("label name is empty") 27 } 28 if len(label.Name) > 40 { 29 return fmt.Errorf("label name too long (max 40 graphemes)") 30 } 31 if len(label.Name) < 1 { 32 return fmt.Errorf("label name too short (min 1 grapheme)") 33 } 34 if !labelNameRegex.MatchString(label.Name) { 35 return fmt.Errorf("label name contains invalid characters (use only letters, numbers, hyphens, and underscores)") 36 } 37 38 if !label.ValueType.IsConcreteType() { 39 return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType.Type) 40 } 41 42 // null type checks: cannot be enums, multiple or explicit format 43 if label.ValueType.IsNull() && label.ValueType.IsEnum() { 44 return fmt.Errorf("null type cannot be used in conjunction with enum type") 45 } 46 if label.ValueType.IsNull() && label.Multiple { 47 return fmt.Errorf("null type labels cannot be multiple") 48 } 49 if label.ValueType.IsNull() && !label.ValueType.IsAnyFormat() { 50 return fmt.Errorf("format cannot be used in conjunction with null type") 51 } 52 53 // format checks: cannot be used with enum, or integers 54 if !label.ValueType.IsAnyFormat() && label.ValueType.IsEnum() { 55 return fmt.Errorf("enum types cannot be used in conjunction with format specification") 56 } 57 58 if !label.ValueType.IsAnyFormat() && !label.ValueType.IsString() { 59 return fmt.Errorf("format specifications are only permitted on string types") 60 } 61 62 // validate scope (nsid format) 63 if label.Scope == "" { 64 return fmt.Errorf("scope is required") 65 } 66 if _, err := syntax.ParseNSID(string(label.Scope)); err != nil { 67 return fmt.Errorf("failed to parse scope: %w", err) 68 } 69 if !slices.Contains(validScopes, label.Scope) { 70 return fmt.Errorf("invalid scope: scope must be one of %q", validScopes) 71 } 72 73 // validate color if provided 74 if label.Color != nil { 75 color := strings.TrimSpace(*label.Color) 76 if color == "" { 77 // empty color is fine, set to nil 78 label.Color = nil 79 } else { 80 if !colorRegex.MatchString(color) { 81 return fmt.Errorf("color must be a valid hex color (e.g. #79FFE1 or #000)") 82 } 83 // expand 3-digit hex to 6-digit hex 84 if len(color) == 4 { // #ABC 85 color = fmt.Sprintf("#%c%c%c%c%c%c", color[1], color[1], color[2], color[2], color[3], color[3]) 86 } 87 // convert to uppercase for consistency 88 color = strings.ToUpper(color) 89 label.Color = &color 90 } 91 } 92 93 return nil 94} 95 96func (v *Validator) ValidateLabelOp(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error { 97 if labelDef == nil { 98 return fmt.Errorf("label definition is required") 99 } 100 if labelOp == nil { 101 return fmt.Errorf("label operation is required") 102 } 103 104 expectedKey := labelDef.AtUri().String() 105 if labelOp.OperandKey != expectedKey { 106 return fmt.Errorf("operand key %q does not match label definition URI %q", labelOp.OperandKey, expectedKey) 107 } 108 109 if labelOp.Operation != db.LabelOperationAdd && labelOp.Operation != db.LabelOperationDel { 110 return fmt.Errorf("invalid operation: %q (must be 'add' or 'del')", labelOp.Operation) 111 } 112 113 if labelOp.Subject == "" { 114 return fmt.Errorf("subject URI is required") 115 } 116 if _, err := syntax.ParseATURI(string(labelOp.Subject)); err != nil { 117 return fmt.Errorf("invalid subject URI: %w", err) 118 } 119 120 if err := v.validateOperandValue(labelDef, labelOp); err != nil { 121 return fmt.Errorf("invalid operand value: %w", err) 122 } 123 124 // Validate performed time is not zero/invalid 125 if labelOp.PerformedAt.IsZero() { 126 return fmt.Errorf("performed_at timestamp is required") 127 } 128 129 return nil 130} 131 132func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error { 133 valueType := labelDef.ValueType 134 135 // this is permitted, it "unsets" a label 136 if labelOp.OperandValue == "" { 137 labelOp.Operation = db.LabelOperationDel 138 return nil 139 } 140 141 switch valueType.Type { 142 case db.ConcreteTypeNull: 143 // For null type, value should be empty 144 if labelOp.OperandValue != "null" { 145 return fmt.Errorf("null type requires empty value, got %q", labelOp.OperandValue) 146 } 147 148 case db.ConcreteTypeString: 149 // For string type, validate enum constraints if present 150 if valueType.IsEnum() { 151 if !slices.Contains(valueType.Enum, labelOp.OperandValue) { 152 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum) 153 } 154 } 155 156 switch valueType.Format { 157 case db.ValueTypeFormatDid: 158 id, err := v.resolver.ResolveIdent(context.Background(), labelOp.OperandValue) 159 if err != nil { 160 return fmt.Errorf("failed to resolve did/handle: %w", err) 161 } 162 163 labelOp.OperandValue = id.DID.String() 164 165 case db.ValueTypeFormatAny, "": 166 default: 167 return fmt.Errorf("unsupported format constraint: %q", valueType.Format) 168 } 169 170 case db.ConcreteTypeInt: 171 if labelOp.OperandValue == "" { 172 return fmt.Errorf("integer type requires non-empty value") 173 } 174 if _, err := fmt.Sscanf(labelOp.OperandValue, "%d", new(int)); err != nil { 175 return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue) 176 } 177 178 if valueType.IsEnum() { 179 if !slices.Contains(valueType.Enum, labelOp.OperandValue) { 180 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum) 181 } 182 } 183 184 case db.ConcreteTypeBool: 185 if labelOp.OperandValue != "true" && labelOp.OperandValue != "false" { 186 return fmt.Errorf("boolean type requires value to be 'true' or 'false', got %q", labelOp.OperandValue) 187 } 188 189 // validate enum constraints if present (though uncommon for booleans) 190 if valueType.IsEnum() { 191 if !slices.Contains(valueType.Enum, labelOp.OperandValue) { 192 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum) 193 } 194 } 195 196 default: 197 return fmt.Errorf("unsupported value type: %q", valueType.Type) 198 } 199 200 return nil 201}