github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/kv/record_match.go (about)

     1  package kv
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sort"
     7  	"strings"
     8  
     9  	"google.golang.org/protobuf/proto"
    10  	"google.golang.org/protobuf/reflect/protoreflect"
    11  )
    12  
    13  type Record struct {
    14  	Partition string      `json:"partition"`
    15  	Key       string      `json:"key"`
    16  	Value     interface{} `json:"value"`
    17  }
    18  
    19  type MatchRecord struct {
    20  	PartitionPattern string
    21  	PathPattern      string
    22  	MessageType      protoreflect.MessageType
    23  }
    24  
    25  var recordsMatches []MatchRecord
    26  
    27  var ErrPatternAlreadyRegistered = errors.New("pattern already registered")
    28  
    29  // RegisterType - Register a pb message type to parse the data, according to a path regex
    30  // All objects which match the path regex will be parsed as that type
    31  // A nil type parses the value as a plain string
    32  func RegisterType(partitionPattern, pathPattern string, mt protoreflect.MessageType) error {
    33  	// verify we are not trying to add the same record twice
    34  	for _, rec := range recordsMatches {
    35  		if rec.PartitionPattern == partitionPattern && rec.PathPattern == pathPattern {
    36  			return ErrPatternAlreadyRegistered
    37  		}
    38  	}
    39  
    40  	// add the new record
    41  	recordsMatches = append(recordsMatches, MatchRecord{
    42  		PartitionPattern: partitionPattern,
    43  		PathPattern:      pathPattern,
    44  		MessageType:      mt,
    45  	})
    46  
    47  	// make sure that we keep the longest pattern first
    48  	sort.Slice(recordsMatches, func(i, j int) bool {
    49  		if recordsMatches[i].PartitionPattern == recordsMatches[j].PartitionPattern {
    50  			return recordsMatches[i].PathPattern > recordsMatches[j].PathPattern
    51  		}
    52  		return recordsMatches[i].PartitionPattern > recordsMatches[j].PartitionPattern
    53  	})
    54  	return nil
    55  }
    56  
    57  func MustRegisterType(partitionPattern, pathPattern string, mt protoreflect.MessageType) {
    58  	err := RegisterType(partitionPattern, pathPattern, mt)
    59  	if err != nil {
    60  		panic(fmt.Errorf("%w: partition '%s', path '%s'", err, partitionPattern, pathPattern))
    61  	}
    62  }
    63  
    64  // FindMessageTypeRecord lookup proto message type based on the partition and path.
    65  //
    66  //	Can return nil in case the value is not matched
    67  func FindMessageTypeRecord(partition, path string) protoreflect.MessageType {
    68  	for _, r := range recordsMatches {
    69  		partitionMatch := patternMatch(r.PartitionPattern, partition)
    70  		if !partitionMatch {
    71  			continue
    72  		}
    73  
    74  		pathMatch := patternMatch(r.PathPattern, path)
    75  		if pathMatch {
    76  			return r.MessageType
    77  		}
    78  	}
    79  	return nil
    80  }
    81  
    82  // patternMatch reports str matches a pattern. The pattern uses '/' as separator of each part. Part can match specific value or any ('*').
    83  // Match works as prefix match as it will verify the given pattern and return success when pattern is completed no matter of the value holds more parts.
    84  //
    85  // Examples:
    86  //
    87  //	patternMatch("repo", "repo") // true
    88  //	patternMatch("repo", "repo/something") // true
    89  //	patternMatch("repo", "repository") // false
    90  //	patternMatch("*", "repository") // true
    91  //	patternMatch("repo/*/branches", "repo") // false
    92  //	patternMatch("repo/*/branches", "repo/branch1") // false
    93  //	patternMatch("repo/*/branches", "repo/branch1/branches") // true
    94  //	patternMatch("repo/*/branches", "repo/branch1/branches/objects") // true
    95  func patternMatch(pattern string, str string) bool {
    96  	pp := strings.Split(pattern, "/")
    97  	v := strings.SplitN(str, "/", len(pp)+1)
    98  	if len(v) < len(pp) {
    99  		// value doesn't hold enough parts
   100  		return false
   101  	}
   102  	// match each part to the pattern
   103  	for i, p := range pp {
   104  		if p != "*" && p != v[i] {
   105  			return false
   106  		}
   107  	}
   108  	return true
   109  }
   110  
   111  func NewRecord(partition, path string, rawValue []byte) (*Record, error) {
   112  	mt := FindMessageTypeRecord(partition, path)
   113  
   114  	var value interface{}
   115  	if mt == nil {
   116  		value = rawValue
   117  	} else {
   118  		msg := mt.New().Interface()
   119  		err := proto.Unmarshal(rawValue, msg)
   120  		if err != nil {
   121  			return nil, err
   122  		}
   123  		value = msg
   124  	}
   125  
   126  	return &Record{
   127  		Partition: partition,
   128  		Key:       path,
   129  		Value:     value,
   130  	}, nil
   131  }