Files
tessera/services/data_service/internal/domain/identifier.go

132 lines
3.0 KiB
Go

// Package domain defines external message identifiers.
package domain
import (
"errors"
"sort"
"strings"
)
var ErrBadIdentifier = errors.New("identifier: invalid format")
// Identifier is a lightweight wrapper around the canonical key.
type Identifier struct{ key string }
// NewIdentifier builds a canonical key: "namespace::l1.l2[param=v;...] .l3".
// Labels and params are sorted for determinism.
func NewIdentifier(namespace string, labels map[string]map[string]string) Identifier {
var b strings.Builder
// rough prealloc: ns + "::" + avg label + some params
b.Grow(len(namespace) + 2 + 10*len(labels) + 20)
// namespace
b.WriteString(namespace)
b.WriteString("::")
// sort label names for stable output
labelNames := make([]string, 0, len(labels))
for name := range labels {
labelNames = append(labelNames, name)
}
sort.Strings(labelNames)
for i, name := range labelNames {
if i > 0 {
b.WriteByte('.')
}
b.WriteString(name)
// params (sorted)
params := labels[name]
if len(params) > 0 {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
b.WriteByte('[')
for j, k := range keys {
if j > 0 {
b.WriteByte(';')
}
b.WriteString(k)
b.WriteByte('=')
b.WriteString(params[k])
}
b.WriteByte(']')
}
}
return Identifier{key: b.String()}
}
// NewIdentifierFromRaw wraps a raw key without validation.
func NewIdentifierFromRaw(raw string) Identifier { return Identifier{key: raw} }
// Key returns the canonical key string.
func (id Identifier) Key() string { return id.key }
// Parse returns namespace and labels from Key.
// Format: "namespace::label1.label2[param=a;foo=bar].label3"
func (id Identifier) Parse() (string, map[string]map[string]string, error) {
k := id.key
i := strings.Index(k, "::")
if i <= 0 {
return "", nil, ErrBadIdentifier
}
ns := k[:i]
if ns == "" {
return "", nil, ErrBadIdentifier
}
raw := k[i+2:]
lbls := make(map[string]map[string]string, 8)
if raw == "" {
return ns, lbls, nil
}
for tok := range strings.SplitSeq(raw, ".") {
if tok == "" {
continue
}
name, params, err := parseLabel(tok)
if err != nil || name == "" {
return "", nil, ErrBadIdentifier
}
lbls[name] = params
}
return ns, lbls, nil
}
// parseLabel parses "name" or "name[k=v;...]" into (name, params).
func parseLabel(tok string) (string, map[string]string, error) {
lb := strings.IndexByte(tok, '[')
if lb == -1 {
return tok, map[string]string{}, nil
}
rb := strings.LastIndexByte(tok, ']')
if rb == -1 || rb < lb {
return "", nil, ErrBadIdentifier
}
name := tok[:lb]
paramStr := tok[lb+1 : rb]
params := map[string]string{}
if paramStr == "" {
return name, params, nil
}
for pair := range strings.SplitSeq(paramStr, ";") {
if pair == "" {
continue
}
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 || kv[0] == "" {
return "", nil, ErrBadIdentifier
}
params[kv[0]] = kv[1]
}
return name, params, nil
}