···7777 return vt.Type == ConcreteTypeBool
7878}
79798080-func (vt ValueType) IsEnumType() bool {
8080+func (vt ValueType) IsEnum() bool {
8181 return len(vt.Enum) > 0
8282}
8383···631631 return false
632632}
633633634634-func (s *LabelState) GetValSet(l string) set {
635635- return s.inner[l]
634634+// go maps behavior in templates make this necessary,
635635+// indexing a map and getting `set` in return is apparently truthy
636636+func (s LabelState) ContainsLabelAndVal(l, v string) bool {
637637+ if valset, exists := s.inner[l]; exists {
638638+ if _, exists := valset[v]; exists {
639639+ return true
640640+ }
641641+ }
642642+643643+ return false
644644+}
645645+646646+func (s LabelState) GetValSet(l string) set {
647647+ if valset, exists := s.inner[l]; exists {
648648+ return valset
649649+ } else {
650650+ return make(set)
651651+ }
636652}
637653638654type LabelApplicationCtx struct {
···719735 _ = c.ApplyLabelOp(state, o)
720736 }
721737}
738738+739739+// IsInverse checks if one label operation is the inverse of another
740740+// returns true if one is an add and the other is a delete with the same key and value
741741+func (op1 LabelOp) IsInverse(op2 LabelOp) bool {
742742+ if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue {
743743+ return false
744744+ }
745745+746746+ return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) ||
747747+ (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)
748748+}
749749+750750+// removes pairs of label operations that are inverses of each other
751751+// from the given slice. the function preserves the order of remaining operations.
752752+func ReduceLabelOps(ops []LabelOp) []LabelOp {
753753+ if len(ops) <= 1 {
754754+ return ops
755755+ }
756756+757757+ keep := make([]bool, len(ops))
758758+ for i := range keep {
759759+ keep[i] = true
760760+ }
761761+762762+ for i := range ops {
763763+ if !keep[i] {
764764+ continue
765765+ }
766766+767767+ for j := i + 1; j < len(ops); j++ {
768768+ if !keep[j] {
769769+ continue
770770+ }
771771+772772+ if ops[i].IsInverse(ops[j]) {
773773+ keep[i] = false
774774+ keep[j] = false
775775+ break // move to next i since this one is now eliminated
776776+ }
777777+ }
778778+ }
779779+780780+ // build result slice with only kept operations
781781+ var result []LabelOp
782782+ for i, op := range ops {
783783+ if keep[i] {
784784+ result = append(result, op)
785785+ }
786786+ }
787787+788788+ return result
789789+}
···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")
4545+ }
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")
4460 }
45614662 // validate scope (nsid format)
···116132func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
117133 valueType := labelDef.ValueType
118134135135+ // this is permitted, it "unsets" a label
136136+ if labelOp.OperandValue == "" {
137137+ labelOp.Operation = db.LabelOperationDel
138138+ return nil
139139+ }
140140+119141 switch valueType.Type {
120142 case db.ConcreteTypeNull:
121143 // For null type, value should be empty
···125147126148 case db.ConcreteTypeString:
127149 // For string type, validate enum constraints if present
128128- if valueType.IsEnumType() {
150150+ if valueType.IsEnum() {
129151 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
130152 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
131153 }
···153175 return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue)
154176 }
155177156156- if valueType.IsEnumType() {
178178+ if valueType.IsEnum() {
157179 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
158180 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
159181 }
···165187 }
166188167189 // validate enum constraints if present (though uncommon for booleans)
168168- if valueType.IsEnumType() {
190190+ if valueType.IsEnum() {
169191 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
170192 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
171193 }