···77 return vt.Type == ConcreteTypeBool
78}
7980+func (vt ValueType) IsEnum() bool {
81 return len(vt.Enum) > 0
82}
83···631 return false
632}
633634+// go maps behavior in templates make this necessary,
635+// indexing a map and getting `set` in return is apparently truthy
636+func (s LabelState) ContainsLabelAndVal(l, v string) bool {
637+ if valset, exists := s.inner[l]; exists {
638+ if _, exists := valset[v]; exists {
639+ return true
640+ }
641+ }
642+643+ return false
644+}
645+646+func (s LabelState) GetValSet(l string) set {
647+ if valset, exists := s.inner[l]; exists {
648+ return valset
649+ } else {
650+ return make(set)
651+ }
652}
653654type LabelApplicationCtx struct {
···735 _ = c.ApplyLabelOp(state, o)
736 }
737}
738+739+// IsInverse checks if one label operation is the inverse of another
740+// returns true if one is an add and the other is a delete with the same key and value
741+func (op1 LabelOp) IsInverse(op2 LabelOp) bool {
742+ if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue {
743+ return false
744+ }
745+746+ return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) ||
747+ (op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)
748+}
749+750+// removes pairs of label operations that are inverses of each other
751+// from the given slice. the function preserves the order of remaining operations.
752+func ReduceLabelOps(ops []LabelOp) []LabelOp {
753+ if len(ops) <= 1 {
754+ return ops
755+ }
756+757+ keep := make([]bool, len(ops))
758+ for i := range keep {
759+ keep[i] = true
760+ }
761+762+ for i := range ops {
763+ if !keep[i] {
764+ continue
765+ }
766+767+ for j := i + 1; j < len(ops); j++ {
768+ if !keep[j] {
769+ continue
770+ }
771+772+ if ops[i].IsInverse(ops[j]) {
773+ keep[i] = false
774+ keep[j] = false
775+ break // move to next i since this one is now eliminated
776+ }
777+ }
778+ }
779+780+ // build result slice with only kept operations
781+ var result []LabelOp
782+ for i, op := range ops {
783+ if keep[i] {
784+ result = append(result, op)
785+ }
786+ }
787+788+ return result
789+}
···36 }
3738 if !label.ValueType.IsConcreteType() {
39- return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType)
40 }
4142- if label.ValueType.IsNull() && label.ValueType.IsEnumType() {
043 return fmt.Errorf("null type cannot be used in conjunction with enum type")
00000000000000044 }
4546 // validate scope (nsid format)
···116func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
117 valueType := labelDef.ValueType
118000000119 switch valueType.Type {
120 case db.ConcreteTypeNull:
121 // For null type, value should be empty
···125126 case db.ConcreteTypeString:
127 // For string type, validate enum constraints if present
128- if valueType.IsEnumType() {
129 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
130 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
131 }
···153 return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue)
154 }
155156- if valueType.IsEnumType() {
157 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
158 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
159 }
···165 }
166167 // validate enum constraints if present (though uncommon for booleans)
168- if valueType.IsEnumType() {
169 if !slices.Contains(valueType.Enum, labelOp.OperandValue) {
170 return fmt.Errorf("value %q is not in allowed enum values %v", labelOp.OperandValue, valueType.Enum)
171 }
···36 }
3738 if !label.ValueType.IsConcreteType() {
39+ return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", label.ValueType.Type)
40 }
4142+ // 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 }
6162 // validate scope (nsid format)
···132func (v *Validator) validateOperandValue(labelDef *db.LabelDefinition, labelOp *db.LabelOp) error {
133 valueType := labelDef.ValueType
134135+ // 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
···147148 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 }
···175 return fmt.Errorf("value %q is not a valid integer", labelOp.OperandValue)
176 }
177178+ 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 }
···187 }
188189 // 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 }