···1818 // Color should be a valid hex color
1919 colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`)
2020 // You can only label issues and pulls presently
2121- validScopes = []syntax.NSID{tangled.RepoIssueNSID, tangled.RepoPullNSID}
2121+ validScopes = []string{tangled.RepoIssueNSID, tangled.RepoPullNSID}
2222)
23232424func (v *Validator) ValidateLabelDefinition(label *db.LabelDefinition) error {
···3636 }
37373838 if !label.ValueType.IsConcreteType() {
3939- return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType)
3939+ return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType.Type)
4040 }
41414242- if label.ValueType.IsNull() && label.ValueType.IsEnumType() {
4242+ // null type checks: cannot be enums, multiple or explicit format
4343+ if label.ValueType.IsNull() && label.ValueType.IsEnum() {
4344 return fmt.Errorf("null type cannot be used in conjunction with enum type")
4445 }
4646+ if label.ValueType.IsNull() && label.Multiple {
4747+ return fmt.Errorf("null type labels cannot be multiple")
4848+ }
4949+ if label.ValueType.IsNull() && !label.ValueType.IsAnyFormat() {
5050+ return fmt.Errorf("format cannot be used in conjunction with null type")
5151+ }
5252+5353+ // format checks: cannot be used with enum, or integers
5454+ if !label.ValueType.IsAnyFormat() && label.ValueType.IsEnum() {
5555+ return fmt.Errorf("enum types cannot be used in conjunction with format specification")
5656+ }
5757+5858+ if !label.ValueType.IsAnyFormat() && !label.ValueType.IsString() {
5959+ return fmt.Errorf("format specifications are only permitted on string types")
6060+ }
45614662 // validate scope (nsid format)
4747- if label.Scope == "" {
6363+ if label.Scope == nil {
4864 return fmt.Errorf("scope is required")
4965 }
5050- if _, err := syntax.ParseNSID(string(label.Scope)); err != nil {
5151- return fmt.Errorf("failed to parse scope: %w", err)
5252- }
5353- if !slices.Contains(validScopes, label.Scope) {
5454- return fmt.Errorf("invalid scope: scope must be one of %q", validScopes)
6666+ for _, s := range label.Scope {
6767+ if _, err := syntax.ParseNSID(s); err != nil {
6868+ return fmt.Errorf("failed to parse scope: %w", err)
6969+ }
7070+ if !slices.Contains(validScopes, s) {
7171+ return fmt.Errorf("invalid scope: scope must be present in %q", validScopes)
7272+ }
5573 }
56745775 // validate color if provided
···116134func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
117135 valueType := labelDef.ValueType
118136137137+ // this is permitted, it "unsets" a label
138138+ if labelOp.OperandValue == "" {
139139+ labelOp.Operation = db.LabelOperationDel
140140+ return nil
141141+ }
142142+119143 switch valueType.Type {
120144 case db.ConcreteTypeNull:
121145 // For null type, value should be empty
···125149126150 case db.ConcreteTypeString:
127151 // For string type, validate enum constraints if present
128128- if valueType.IsEnumType() {
152152+ if valueType.IsEnum() {
129153 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
130154 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
131155 }
···153177 return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue)
154178 }
155179156156- if valueType.IsEnumType() {
180180+ if valueType.IsEnum() {
157181 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
158182 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
159183 }
···165189 }
166190167191 // validate enum constraints if present (though uncommon for booleans)
168168- if valueType.IsEnumType() {
192192+ if valueType.IsEnum() {
169193 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
170194 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
171195 }