Refactor identifier handling: replace Provider and Subject fields with a single Key field in Identifier struct, update related message structures, and adjust parsing logic

This commit is contained in:
2025-08-17 06:16:49 +00:00
parent ef4a28fb29
commit 2484a33945
9 changed files with 290 additions and 165 deletions

View File

@@ -1,6 +1,181 @@
package domain
type Identifier struct {
Provider string
Subject string
import (
"errors"
"fmt"
"regexp"
"sort"
"strings"
)
const (
prefixRaw = "raw::"
prefixInternal = "internal::"
)
// Identifier is a canonical representation of a data stream identifier.
type Identifier struct{ key string }
func (id Identifier) IsRaw() bool { return strings.HasPrefix(id.key, prefixRaw) }
func (id Identifier) IsInternal() bool { return strings.HasPrefix(id.key, prefixInternal) }
func (id Identifier) Key() string { return id.key }
func (id Identifier) ProviderSubject() (provider, subject string, ok bool) {
if !id.IsRaw() {
return "", "", false
}
body := strings.TrimPrefix(id.key, prefixRaw)
prov, subj, ok := strings.Cut(body, ".")
return prov, subj, ok
}
func (id Identifier) InternalParts() (venue, stream, symbol string, params map[string]string, ok bool) {
if !id.IsInternal() {
return "", "", "", nil, false
}
body := strings.TrimPrefix(id.key, prefixInternal)
before, bracket, _ := strings.Cut(body, "[")
parts := strings.Split(before, ".")
if len(parts) != 3 {
return "", "", "", nil, false
}
return parts[0], parts[1], parts[2], decodeParams(strings.TrimSuffix(bracket, "]")), true
}
func RawID(provider, subject string) (Identifier, error) {
p := strings.ToLower(strings.TrimSpace(provider))
s := strings.TrimSpace(subject)
if err := validateComponent("provider", p, false); err != nil {
return Identifier{}, err
}
if err := validateComponent("subject", s, true); err != nil {
return Identifier{}, err
}
return Identifier{key: prefixRaw + p + "." + s}, nil
}
func InternalID(venue, stream, symbol string, params map[string]string) (Identifier, error) {
v := strings.ToLower(strings.TrimSpace(venue))
t := strings.ToLower(strings.TrimSpace(stream))
sym := strings.ToUpper(strings.TrimSpace(symbol))
if err := validateComponent("venue", v, false); err != nil {
return Identifier{}, err
}
if err := validateComponent("stream", t, false); err != nil {
return Identifier{}, err
}
if err := validateComponent("symbol", sym, false); err != nil {
return Identifier{}, err
}
paramStr, err := encodeParams(params) // "k=v;..." or ""
if err != nil {
return Identifier{}, err
}
if paramStr == "" {
paramStr = "[]"
} else {
paramStr = "[" + paramStr + "]"
}
return Identifier{key: prefixInternal + v + "." + t + "." + sym + paramStr}, nil
}
func ParseIdentifier(s string) (Identifier, error) {
s = strings.TrimSpace(s)
switch {
case strings.HasPrefix(s, prefixRaw):
// raw::provider.subject
body := strings.TrimPrefix(s, prefixRaw)
prov, subj, ok := strings.Cut(body, ".")
if !ok {
return Identifier{}, errors.New("invalid raw identifier: missing '.'")
}
return RawID(prov, subj)
case strings.HasPrefix(s, prefixInternal):
// internal::venue.stream.symbol[...]
body := strings.TrimPrefix(s, prefixInternal)
before, bracket, _ := strings.Cut(body, "[")
parts := strings.Split(before, ".")
if len(parts) != 3 {
return Identifier{}, errors.New("invalid internal identifier: need venue.stream.symbol")
}
params := decodeParams(strings.TrimSuffix(bracket, "]"))
return InternalID(parts[0], parts[1], parts[2], params)
}
return Identifier{}, errors.New("unknown identifier prefix")
}
var (
segDisallow = regexp.MustCompile(`[ \t\r\n\[\]]`) // forbid whitespace/brackets in fixed segments
dotDisallow = regexp.MustCompile(`[.]`) // fixed segments cannot contain '.'
)
// allowAny=true (for subject) skips dot checks but still forbids whitespace/brackets.
func validateComponent(name, v string, allowAny bool) error {
if v == "" {
return fmt.Errorf("%s cannot be empty", name)
}
if allowAny {
if segDisallow.MatchString(v) {
return fmt.Errorf("%s contains illegal chars [] or whitespace", name)
}
return nil
}
if segDisallow.MatchString(v) || dotDisallow.MatchString(v) {
return fmt.Errorf("%s contains illegal chars (dot/brackets/whitespace)", name)
}
return nil
}
// encodeParams renders sorted k=v pairs separated by ';'.
func encodeParams(params map[string]string) (string, error) {
if len(params) == 0 {
return "", nil
}
keys := make([]string, 0, len(params))
for k := range params {
k = strings.ToLower(strings.TrimSpace(k))
if k == "" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
out := make([]string, 0, len(keys))
for _, k := range keys {
v := strings.TrimSpace(params[k])
// prevent breaking delimiters
if strings.ContainsAny(k, ";]") || strings.ContainsAny(v, ";]") {
return "", fmt.Errorf("param %q contains illegal ';' or ']'", k)
}
out = append(out, k+"="+v)
}
return strings.Join(out, ";"), nil
}
func decodeParams(s string) map[string]string {
s = strings.TrimSpace(s)
if s == "" {
return map[string]string{}
}
out := make(map[string]string, 4)
for _, p := range strings.Split(s, ";") {
if p == "" {
continue
}
kv := strings.SplitN(p, "=", 2)
if len(kv) != 2 {
continue
}
k := strings.ToLower(strings.TrimSpace(kv[0]))
v := strings.TrimSpace(kv[1])
if k != "" {
out[k] = v
}
}
return out
}