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) } }