package tree import ( "fmt" "sigs.k8s.io/yaml" ) // Tree is a structured representation of a string keyed tree document such as // yaml or json. type Tree map[string]interface{} // ToYAML returns a yaml serialization of the Tree. func (t Tree) ToYAML() (string, error) { bytes, err := yaml.Marshal(t) if err != nil { return "", err } return string(bytes), nil } // String returns a yaml representation of the Tree or an error string if // serialization fails. func (t Tree) String() string { s, err := t.ToYAML() if err != nil { return err.Error() } return s } // GetString returns the string value at the given path func (t Tree) GetString(path ...string) (string, error) { if len(path) == 1 { // check if exists if val, ok := t[path[0]]; ok { // check if string if s, ok := val.(string); ok { return s, nil } return "", fmt.Errorf("expected string at node %s but found a different type", path[0]) } return "", fmt.Errorf("could not find node %s", path[0]) } // check if exists if val, ok := t[path[0]]; ok { // Check if its a Tree if valTree, ok := val.(Tree); ok { return valTree.GetString(path[1:]...) } return "", fmt.Errorf("expected Tree at node %s but found a different type", path[0]) } return "", fmt.Errorf("could not find node %s", path[0]) } // Diff returns the subset of other where its values differ from t. func (t Tree) Diff(other Tree) (Tree, error) { diff := make(Tree) for k, v := range other { tv, ok := t[k] if ok { tvt, tvIsTree := tv.(Tree) vt, vIsTree := v.(Tree) if tvIsTree && vIsTree { subdiff, err := tvt.Diff(vt) if err != nil { return nil, err } diff[k] = subdiff } else if !tvIsTree && !vIsTree { if !equal(v, tv) { diff[k] = v } } else { diff[k] = v } } else { diff[k] = v } } diff.Prune() return diff, nil } func equal(x interface{}, y interface{}) bool { xt, xIsTree := x.(Tree) yt, yIsTree := y.(Tree) if xIsTree && yIsTree { if len(xt) != len(yt) { return false } for k := range xt { if !equal(xt[k], yt[k]) { return false } } return true } if xIsTree || yIsTree { return false } xs, xIsSlice := x.([]interface{}) ys, yIsSlice := x.([]interface{}) if xIsSlice && yIsSlice { if len(xs) != len(ys) { return false } for i := range xs { if !equal(xs[i], ys[i]) { return false } } return true } if xIsSlice || yIsSlice { return false } return x == y } // Prune removes all empty subtrees. A subtree is considered empty if it does // not contain any leaf values. func (t Tree) Prune() { for k, v := range t { child, isTree := v.(Tree) if isTree { if child.Empty() { delete(t, k) } else { child.Prune() } } } } // Empty returns true iff the Tree contains no leaf values. func (t Tree) Empty() bool { for _, v := range t { child, isTree := v.(Tree) if !isTree { return false } if !child.Empty() { return false } } return true } // MarshalToTree marshals obj to yaml and then parses the resulting yaml as // a Tree. func MarshalToTree(obj interface{}) (Tree, error) { bytes, err := yaml.Marshal(obj) if err != nil { return nil, err } return BytesToTree(bytes) } // BytesToTree converts given bytes into a tree by Unmarshaling func BytesToTree(bytes []byte) (Tree, error) { tree := make(Tree) err := yaml.Unmarshal(bytes, &tree) if err != nil { return nil, err } tree.coerceToTree() return tree, nil } // Diff marshals two objects into their yaml representations and then performs // a diff on those Trees. It returns a Tree which represents all of the fields // in y which differ from x. func Diff(x interface{}, y interface{}) (Tree, error) { xTree, err := MarshalToTree(x) if err != nil { return nil, err } yTree, err := MarshalToTree(y) if err != nil { return nil, err } return xTree.Diff(yTree) } // coerceTreeValue accepts a value and returns a value where all child values // have been coerced to a Tree where such a coercion is possible func coerceTreeValue(v interface{}) interface{} { if vt, ok := v.(Tree); ok { vt.coerceToTree() } else if vm, ok := v.(map[string]interface{}); ok { tree := Tree(vm) tree.coerceToTree() return tree } else if va, ok := v.([]interface{}); ok { for i, v := range va { va[i] = coerceTreeValue(v) } } return v } // coerceToTree recursively casts all instances of map[string]interface{} into // Tree within this Tree. When a tree document is unmarshaled, the subtrees // will typically be unmarshaled as map[string]interface{} values. We cast // each of these into the Tree newtype so that the Tree type is used uniformly // throughout the tree. Will additionally recurse through arrays func (t Tree) coerceToTree() { for k, v := range t { t[k] = coerceTreeValue(v) } }