banyand/queue/pub/selector.go (197 lines of code) (raw):
// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package pub
import (
"errors"
"fmt"
"strings"
"unicode"
)
// LabelSelector is a selector for labels.
type LabelSelector struct {
criteria []condition
}
// MatchFunc is a function that matches labels.
type MatchFunc func(labels map[string]string) bool
type condition struct {
Key string
Values []string
Op operator
}
type operator int
const (
opEquals operator = iota
opNotEquals
opIn
opNotIn
opExists
opNotExists
)
var emptyLabels = map[string]string{}
// ParseLabelSelector parses a label selector string.
func ParseLabelSelector(selector string) (*LabelSelector, error) {
if selector == "" {
return &LabelSelector{}, nil
}
var conditions []condition
for _, expr := range splitSelector(selector) {
expr = strings.TrimSpace(expr)
if expr == "" {
continue
}
cond, err := parseCondition(expr)
if err != nil {
return nil, fmt.Errorf("invalid selector %q: %w", selector, err)
}
conditions = append(conditions, cond)
}
return &LabelSelector{conditions}, nil
}
func splitSelector(selector string) []string {
var exprs []string
var current []rune
parenLevel := 0
for _, r := range selector {
switch r {
case '(':
parenLevel++
case ')':
if parenLevel > 0 {
parenLevel--
}
case ',':
if parenLevel == 0 {
exprs = append(exprs, strings.TrimSpace(string(current)))
current = []rune{}
continue
}
}
current = append(current, r)
}
if len(current) > 0 {
exprs = append(exprs, strings.TrimSpace(string(current)))
}
return exprs
}
func parseCondition(expr string) (condition, error) {
if strings.HasPrefix(expr, "!") {
key := strings.TrimSpace(expr[1:])
if err := validateLabelKey(key); err != nil {
return condition{}, err
}
return condition{Key: key, Op: opNotExists}, nil
}
var op operator
var key, valuesStr string
switch {
case strings.Contains(expr, "!="):
parts := strings.SplitN(expr, "!=", 2)
key = strings.TrimSpace(parts[0])
valuesStr = strings.TrimSpace(parts[1])
op = opNotEquals
case strings.Contains(expr, "="):
parts := strings.SplitN(expr, "=", 2)
key = strings.TrimSpace(parts[0])
valuesStr = strings.TrimSpace(parts[1])
op = opEquals
case strings.Contains(expr, " notin "):
parts := strings.SplitN(expr, " notin ", 2)
key = strings.TrimSpace(parts[0])
valuesStr = strings.TrimSpace(parts[1])
op = opNotIn
case strings.Contains(expr, " in "):
parts := strings.SplitN(expr, " in ", 2)
key = strings.TrimSpace(parts[0])
valuesStr = strings.TrimSpace(parts[1])
op = opIn
default:
if err := validateLabelKey(expr); err != nil {
return condition{}, err
}
return condition{Key: expr, Op: opExists}, nil
}
if err := validateLabelKey(key); err != nil {
return condition{}, err
}
values, err := parseValues(valuesStr, op)
if err != nil {
return condition{}, err
}
return condition{
Key: key,
Op: op,
Values: values,
}, nil
}
func parseValues(valuesStr string, op operator) ([]string, error) {
if op == opIn || op == opNotIn {
if !strings.HasPrefix(valuesStr, "(") || !strings.HasSuffix(valuesStr, ")") {
return nil, fmt.Errorf("set operations require parenthesized values")
}
valuesStr = strings.Trim(valuesStr, "()")
}
var values []string
for _, v := range strings.Split(valuesStr, ",") {
v = strings.TrimSpace(v)
if v == "" {
continue
}
values = append(values, v)
}
if len(values) == 0 {
return nil, errors.New("empty value list")
}
return values, nil
}
func validateLabelKey(key string) error {
if key == "" {
return errors.New("empty key")
}
for _, c := range key {
if !unicode.IsLower(c) && !unicode.IsDigit(c) && c != '-' && c != '_' && c != '.' {
return fmt.Errorf("invalid key %q: must match [a-z0-9-_.]", key)
}
}
return nil
}
// Matches returns true if the labels match the selector.
func (s *LabelSelector) Matches(labels map[string]string) bool {
if labels == nil {
labels = emptyLabels
}
for _, req := range s.criteria {
if !req.matches(labels) {
return false
}
}
return true
}
func (r condition) matches(labels map[string]string) bool {
value, exists := labels[r.Key]
switch r.Op {
case opExists:
return exists
case opNotExists:
return !exists
case opEquals:
return exists && value == r.Values[0]
case opNotEquals:
return !exists || value != r.Values[0]
case opIn:
if !exists {
return false
}
for _, v := range r.Values {
if v == value {
return true
}
}
return false
case opNotIn:
if !exists {
return true
}
for _, v := range r.Values {
if v == value {
return false
}
}
return true
default:
return false
}
}