feat: support merging multiple subscriptions
This commit is contained in:
169
internal/automation/patch.go
Normal file
169
internal/automation/patch.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user