feat: support merging multiple subscriptions

This commit is contained in:
2026-07-19 23:58:02 +08:00
parent fb4d927220
commit 087869d84f
17 changed files with 1341 additions and 60 deletions

View File

@ -0,0 +1,169 @@
package automation
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// pathStep is one navigation step through a parsed patch path: either a map
// key or a list index.
type pathStep interface{ isPathStep() }
type mapKeyStep struct{ key string }
type indexStep struct{ index int }
func (mapKeyStep) isPathStep() {}
func (indexStep) isPathStep() {}
var (
segmentPattern = regexp.MustCompile(`^([^\[\]]*)((?:\[\d+\])*)$`)
indexPattern = regexp.MustCompile(`\[(\d+)\]`)
)
// parsePath parses a dot/bracket path like "proxy-groups[0].proxies" or
// "dns.nameserver" into a flat sequence of map-key/index steps.
func parsePath(path string) ([]pathStep, error) {
if strings.TrimSpace(path) == "" {
return nil, fmt.Errorf("empty path")
}
var steps []pathStep
for _, tok := range strings.Split(path, ".") {
m := segmentPattern.FindStringSubmatch(tok)
if m == nil {
return nil, fmt.Errorf("invalid path segment %q in path %q", tok, path)
}
key, idxPart := m[1], m[2]
if key == "" && idxPart == "" {
return nil, fmt.Errorf("invalid path segment %q in path %q", tok, path)
}
if key != "" {
steps = append(steps, mapKeyStep{key: key})
}
for _, im := range indexPattern.FindAllStringSubmatch(idxPart, -1) {
n, _ := strconv.Atoi(im[1])
steps = append(steps, indexStep{index: n})
}
}
return steps, nil
}
// applyPatch parses and applies a single PatchRule to root in place.
func applyPatch(root map[string]interface{}, p PatchRule) error {
steps, err := parsePath(p.Path)
if err != nil {
return err
}
_, err = applyAtStep(root, steps, p.Op, p.Value)
return err
}
// applyAtStep recursively resolves steps against cur, applies op/value once
// the path is exhausted, and returns the (possibly new) value that should
// replace cur in its parent container. Intermediate map keys that don't
// exist yet are auto-created as empty maps; intermediate list indices that
// don't exist are an error (lists can't be safely auto-vivified by index).
func applyAtStep(cur interface{}, steps []pathStep, op string, value interface{}) (interface{}, error) {
if len(steps) == 0 {
return applyOp(cur, op, value)
}
step, rest := steps[0], steps[1:]
switch s := step.(type) {
case mapKeyStep:
m, ok := cur.(map[string]interface{})
if !ok {
if cur == nil {
m = map[string]interface{}{}
} else {
return nil, fmt.Errorf("cannot descend into %q: not a map (got %T)", s.key, cur)
}
}
newChild, err := applyAtStep(m[s.key], rest, op, value)
if err != nil {
return nil, err
}
m[s.key] = newChild
return m, nil
case indexStep:
list, ok := cur.([]interface{})
if !ok {
return nil, fmt.Errorf("cannot index [%d]: not a list (got %T)", s.index, cur)
}
if s.index < 0 || s.index >= len(list) {
return nil, fmt.Errorf("index [%d] out of range (length %d)", s.index, len(list))
}
newChild, err := applyAtStep(list[s.index], rest, op, value)
if err != nil {
return nil, err
}
list[s.index] = newChild
return list, nil
default:
return nil, fmt.Errorf("unknown path step type %T", step)
}
}
// applyOp applies op to the value currently at the target location.
func applyOp(cur interface{}, op string, value interface{}) (interface{}, error) {
switch op {
case "replace":
return value, nil
case "append":
return combine(cur, value, true)
case "prepend":
return combine(cur, value, false)
default:
return nil, fmt.Errorf("unknown op %q (expected append, prepend, or replace)", op)
}
}
// combine implements append/prepend semantics: extending a list, merging a
// map (new keys win either way — maps have no order), or taking value
// as-is if nothing is there yet. Appending/prepending onto a scalar is an
// error.
func combine(cur, value interface{}, appendMode bool) (interface{}, error) {
switch c := cur.(type) {
case nil:
return value, nil
case []interface{}:
var items []interface{}
if v, ok := value.([]interface{}); ok {
items = v
} else {
items = []interface{}{value}
}
out := make([]interface{}, 0, len(c)+len(items))
if appendMode {
out = append(out, c...)
out = append(out, items...)
} else {
out = append(out, items...)
out = append(out, c...)
}
return out, nil
case map[string]interface{}:
v, ok := value.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("cannot append/prepend a %T into a map", value)
}
merged := make(map[string]interface{}, len(c)+len(v))
for k, vv := range c {
merged[k] = vv
}
for k, vv := range v {
merged[k] = vv
}
return merged, nil
default:
return nil, fmt.Errorf("cannot append/prepend to a scalar value (%T)", cur)
}
}