Resolve "Deprecate Providers package in favor of Worker"
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
// Package domain defines external message identifiers.
|
||||
package domain
|
||||
|
||||
import (
|
||||
@@ -9,56 +8,96 @@ import (
|
||||
|
||||
var ErrBadIdentifier = errors.New("identifier: invalid format")
|
||||
|
||||
// Identifier is a lightweight wrapper around the canonical key.
|
||||
// Identifier is an immutable canonical key.
|
||||
// Canonical form: "namespace::tag1[] . tag2[k=v;foo=bar] . tag3[]"
|
||||
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 {
|
||||
// NewIdentifier builds a canonical key with strict validation.
|
||||
// Tags and param keys are sorted. Tags with no params emit "name[]".
|
||||
// Rejects on: empty namespace, bad tag names, bad keys/values.
|
||||
func NewIdentifier(namespace string, tags map[string]map[string]string) (Identifier, error) {
|
||||
ns := strings.TrimSpace(namespace)
|
||||
if !validNamespace(ns) {
|
||||
return Identifier{}, ErrBadIdentifier
|
||||
}
|
||||
|
||||
// Validate and copy to protect immutability and reject dup keys early.
|
||||
clean := make(map[string]map[string]string, len(tags))
|
||||
for name, params := range tags {
|
||||
n := strings.TrimSpace(name)
|
||||
if !validIDTagName(n) {
|
||||
return Identifier{}, ErrBadIdentifier
|
||||
}
|
||||
if _, exists := clean[n]; exists {
|
||||
// impossible via map input, but keep the intent explicit
|
||||
return Identifier{}, ErrBadIdentifier
|
||||
}
|
||||
if len(params) == 0 {
|
||||
clean[n] = map[string]string{}
|
||||
continue
|
||||
}
|
||||
dst := make(map[string]string, len(params))
|
||||
for k, v := range params {
|
||||
kk := strings.TrimSpace(k)
|
||||
vv := strings.TrimSpace(v)
|
||||
if !validParamKey(kk) || !validIDParamValue(vv) {
|
||||
return Identifier{}, ErrBadIdentifier
|
||||
}
|
||||
if _, dup := dst[kk]; dup {
|
||||
return Identifier{}, ErrBadIdentifier
|
||||
}
|
||||
dst[kk] = vv
|
||||
}
|
||||
clean[n] = dst
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
// rough prealloc: ns + "::" + avg label + some params
|
||||
b.Grow(len(namespace) + 2 + 10*len(labels) + 20)
|
||||
// Rough capacity hint.
|
||||
b.Grow(len(ns) + 2 + 16*len(clean) + 32)
|
||||
|
||||
// namespace
|
||||
b.WriteString(namespace)
|
||||
b.WriteString(ns)
|
||||
b.WriteString("::")
|
||||
|
||||
// sort label names for stable output
|
||||
labelNames := make([]string, 0, len(labels))
|
||||
for name := range labels {
|
||||
labelNames = append(labelNames, name)
|
||||
// stable tag order
|
||||
names := make([]string, 0, len(clean))
|
||||
for n := range clean {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(labelNames)
|
||||
sort.Strings(names)
|
||||
|
||||
for i, name := range labelNames {
|
||||
for i, name := range names {
|
||||
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(']')
|
||||
params := clean[name]
|
||||
if len(params) == 0 {
|
||||
b.WriteString("[]")
|
||||
continue
|
||||
}
|
||||
|
||||
// stable param order
|
||||
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()}
|
||||
return Identifier{key: b.String()}, nil
|
||||
}
|
||||
|
||||
// NewIdentifierFromRaw wraps a raw key without validation.
|
||||
@@ -67,65 +106,156 @@ 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"
|
||||
// Parse returns namespace and tags from Key.
|
||||
// Accepts "tag" (bare) as "tag[]". Emits "name[]"/"[k=v;...]". First token wins on duplicates.
|
||||
func (id Identifier) Parse() (string, map[string]map[string]string, error) {
|
||||
k := id.key
|
||||
|
||||
// namespace
|
||||
i := strings.Index(k, "::")
|
||||
if i <= 0 {
|
||||
return "", nil, ErrBadIdentifier
|
||||
}
|
||||
ns := k[:i]
|
||||
if ns == "" {
|
||||
ns := strings.TrimSpace(k[:i])
|
||||
if !validNamespace(ns) {
|
||||
return "", nil, ErrBadIdentifier
|
||||
}
|
||||
raw := k[i+2:]
|
||||
|
||||
lbls := make(map[string]map[string]string, 8)
|
||||
tags := make(map[string]map[string]string, 8)
|
||||
if raw == "" {
|
||||
return ns, lbls, nil
|
||||
return ns, tags, nil
|
||||
}
|
||||
|
||||
for tok := range strings.SplitSeq(raw, ".") {
|
||||
tok = strings.TrimSpace(tok)
|
||||
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 == "" {
|
||||
lb := strings.IndexByte(tok, '[')
|
||||
if lb == -1 {
|
||||
// bare tag => empty params
|
||||
name := strings.TrimSpace(tok)
|
||||
if !validIDTagName(name) {
|
||||
return "", nil, ErrBadIdentifier
|
||||
}
|
||||
if _, exists := tags[name]; !exists {
|
||||
tags[name] = map[string]string{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
kv := strings.SplitN(pair, "=", 2)
|
||||
if len(kv) != 2 || kv[0] == "" {
|
||||
|
||||
rb := strings.LastIndexByte(tok, ']')
|
||||
if rb == -1 || rb < lb || rb != len(tok)-1 {
|
||||
return "", nil, ErrBadIdentifier
|
||||
}
|
||||
params[kv[0]] = kv[1]
|
||||
|
||||
name := strings.TrimSpace(tok[:lb])
|
||||
if !validIDTagName(name) {
|
||||
return "", nil, ErrBadIdentifier
|
||||
}
|
||||
// first tag wins
|
||||
if _, exists := tags[name]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
body := tok[lb+1 : rb]
|
||||
// forbid outer whitespace like "[ x=1 ]"
|
||||
if body != strings.TrimSpace(body) {
|
||||
return "", nil, ErrBadIdentifier
|
||||
}
|
||||
if body == "" {
|
||||
tags[name] = map[string]string{}
|
||||
continue
|
||||
}
|
||||
|
||||
// parse "k=v;foo=bar"
|
||||
params := make(map[string]string, 4)
|
||||
for pair := range strings.SplitSeq(body, ";") {
|
||||
pair = strings.TrimSpace(pair)
|
||||
if pair == "" {
|
||||
continue
|
||||
}
|
||||
kv := strings.SplitN(pair, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
return "", nil, ErrBadIdentifier
|
||||
}
|
||||
key := strings.TrimSpace(kv[0])
|
||||
val := strings.TrimSpace(kv[1])
|
||||
if !validParamKey(key) || !validIDParamValue(val) || val == "" {
|
||||
return "", nil, ErrBadIdentifier
|
||||
}
|
||||
// first key wins
|
||||
if _, dup := params[key]; !dup {
|
||||
params[key] = val
|
||||
}
|
||||
}
|
||||
tags[name] = params
|
||||
}
|
||||
return name, params, nil
|
||||
|
||||
return ns, tags, nil
|
||||
}
|
||||
|
||||
// --- validation helpers ---
|
||||
|
||||
func validNamespace(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '[', ']', ':':
|
||||
return false
|
||||
}
|
||||
if isSpace(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validIDTagName(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '[', ']', '.', ':': // added ':'
|
||||
return false
|
||||
}
|
||||
if isSpace(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validParamKey(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '[', ']', ';', '=':
|
||||
return false
|
||||
}
|
||||
if isSpace(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validIDParamValue(s string) bool {
|
||||
// allow spaces; forbid only bracket, pair, and kv delimiters
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '[', ']', ';', '=':
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isSpace(r rune) bool { return r == ' ' || r == '\t' || r == '\n' || r == '\r' }
|
||||
|
||||
Reference in New Issue
Block a user