package yqlib import ( "fmt" "container/list" "github.com/elliotchance/orderedmap" "gopkg.in/yaml.v3" ) type TraversePreferences struct { DontFollowAlias bool } func Splat(d *dataTreeNavigator, matches *list.List) (*list.List, error) { preferences := &TraversePreferences{DontFollowAlias: true} splatOperation := &Operation{OperationType: TraversePath, Value: "[]", Preferences: preferences} splatTreeNode := &PathTreeNode{Operation: splatOperation} return TraversePathOperator(d, matches, splatTreeNode) } func TraversePathOperator(d *dataTreeNavigator, matchMap *list.List, pathNode *PathTreeNode) (*list.List, error) { log.Debugf("-- Traversing") var matchingNodeMap = list.New() var newNodes []*CandidateNode var err error for el := matchMap.Front(); el != nil; el = el.Next() { newNodes, err = traverse(d, el.Value.(*CandidateNode), pathNode.Operation) if err != nil { return nil, err } for _, n := range newNodes { matchingNodeMap.PushBack(n) } } return matchingNodeMap, nil } func traverse(d *dataTreeNavigator, matchingNode *CandidateNode, operation *Operation) ([]*CandidateNode, error) { log.Debug("Traversing %v", NodeToString(matchingNode)) value := matchingNode.Node if value.Tag == "!!null" && operation.Value != "[]" { log.Debugf("Guessing kind") // we must ahve added this automatically, lets guess what it should be now switch operation.Value.(type) { case int, int64: log.Debugf("probably an array") value.Kind = yaml.SequenceNode default: log.Debugf("probably a map") value.Kind = yaml.MappingNode } value.Tag = "" } switch value.Kind { case yaml.MappingNode: log.Debug("its a map with %v entries", len(value.Content)/2) var newMatches = orderedmap.NewOrderedMap() err := traverseMap(newMatches, matchingNode, operation) if err != nil { return nil, err } if newMatches.Len() == 0 { //no matches, create one automagically valueNode := &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode, Value: "null"} node := matchingNode.Node node.Content = append(node.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: operation.StringValue}, valueNode) candidateNode := &CandidateNode{ Node: valueNode, Path: append(matchingNode.Path, operation.StringValue), Document: matchingNode.Document, } newMatches.Set(candidateNode.GetKey(), candidateNode) } arrayMatches := make([]*CandidateNode, newMatches.Len()) i := 0 for el := newMatches.Front(); el != nil; el = el.Next() { arrayMatches[i] = el.Value.(*CandidateNode) i++ } return arrayMatches, nil case yaml.SequenceNode: log.Debug("its a sequence of %v things!", len(value.Content)) return traverseArray(matchingNode, operation) case yaml.AliasNode: log.Debug("its an alias!") matchingNode.Node = matchingNode.Node.Alias return traverse(d, matchingNode, operation) case yaml.DocumentNode: log.Debug("digging into doc node") return traverse(d, &CandidateNode{ Node: matchingNode.Node.Content[0], Document: matchingNode.Document}, operation) default: return nil, nil } } func keyMatches(key *yaml.Node, pathNode *Operation) bool { return pathNode.Value == "[]" || Match(key.Value, pathNode.StringValue) } func traverseMap(newMatches *orderedmap.OrderedMap, candidate *CandidateNode, operation *Operation) error { // value.Content is a concatenated array of key, value, // so keys are in the even indexes, values in odd. // merge aliases are defined first, but we only want to traverse them // if we don't find a match directly on this node first. //TODO ALIASES, auto creation? node := candidate.Node followAlias := true if operation.Preferences != nil { followAlias = !operation.Preferences.(*TraversePreferences).DontFollowAlias } var contents = node.Content for index := 0; index < len(contents); index = index + 2 { key := contents[index] value := contents[index+1] log.Debug("checking %v (%v)", key.Value, key.Tag) //skip the 'merge' tag, find a direct match first if key.Tag == "!!merge" && followAlias { log.Debug("Merge anchor") err := traverseMergeAnchor(newMatches, candidate, value, operation) if err != nil { return err } } else if keyMatches(key, operation) { log.Debug("MATCHED") candidateNode := &CandidateNode{ Node: value, Path: append(candidate.Path, key.Value), Document: candidate.Document, } newMatches.Set(candidateNode.GetKey(), candidateNode) } } return nil } func traverseMergeAnchor(newMatches *orderedmap.OrderedMap, originalCandidate *CandidateNode, value *yaml.Node, operation *Operation) error { switch value.Kind { case yaml.AliasNode: candidateNode := &CandidateNode{ Node: value.Alias, Path: originalCandidate.Path, Document: originalCandidate.Document, } return traverseMap(newMatches, candidateNode, operation) case yaml.SequenceNode: for _, childValue := range value.Content { err := traverseMergeAnchor(newMatches, originalCandidate, childValue, operation) if err != nil { return err } } } return nil } func traverseArray(candidate *CandidateNode, operation *Operation) ([]*CandidateNode, error) { log.Debug("operation Value %v", operation.Value) if operation.Value == "[]" { var contents = candidate.Node.Content var newMatches = make([]*CandidateNode, len(contents)) var index int64 for index = 0; index < int64(len(contents)); index = index + 1 { newMatches[index] = &CandidateNode{ Document: candidate.Document, Path: append(candidate.Path, index), Node: contents[index], } } return newMatches, nil } switch operation.Value.(type) { case int64: index := operation.Value.(int64) indexToUse := index contentLength := int64(len(candidate.Node.Content)) for contentLength <= index { candidate.Node.Content = append(candidate.Node.Content, &yaml.Node{Tag: "!!null", Kind: yaml.ScalarNode, Value: "null"}) contentLength = int64(len(candidate.Node.Content)) } if indexToUse < 0 { indexToUse = contentLength + indexToUse } if indexToUse < 0 { return nil, fmt.Errorf("Index [%v] out of range, array size is %v", index, contentLength) } return []*CandidateNode{&CandidateNode{ Node: candidate.Node.Content[indexToUse], Document: candidate.Document, Path: append(candidate.Path, index), }}, nil default: log.Debug("argument not an int (%v), no array matches", operation.Value) return nil, nil } }