diff --git a/pkg/yqlib/context.go b/pkg/yqlib/context.go index fd3bdfc..921eb7c 100644 --- a/pkg/yqlib/context.go +++ b/pkg/yqlib/context.go @@ -12,6 +12,14 @@ type Context struct { DontAutoCreate bool } +func (n *Context) SingleReadonlyChildContext(candidate *CandidateNode) Context { + list := list.New() + list.PushBack(candidate) + newContext := n.ChildContext(list) + newContext.DontAutoCreate = true + return newContext +} + func (n *Context) SingleChildContext(candidate *CandidateNode) Context { list := list.New() list.PushBack(candidate) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 44dd3b2..065088f 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -20,9 +20,6 @@ type operationType struct { Handler operatorHandler } -// operators TODO: -// - mergeEmpty (sets only if the document is empty, do I do that now?) - var orOpType = &operationType{Type: "OR", NumArgs: 2, Precedence: 20, Handler: orOperator} var andOpType = &operationType{Type: "AND", NumArgs: 2, Precedence: 20, Handler: andOperator} var reduceOpType = &operationType{Type: "REDUCE", NumArgs: 2, Precedence: 35, Handler: reduceOperator} diff --git a/pkg/yqlib/operator_booleans.go b/pkg/yqlib/operator_booleans.go index 02c3e0d..5d4cf33 100644 --- a/pkg/yqlib/operator_booleans.go +++ b/pkg/yqlib/operator_booleans.go @@ -71,7 +71,7 @@ func findBoolean(wantBool bool, d *dataTreeNavigator, context Context, expressio if expressionNode != nil { //need to evaluate the expression against the node candidate := &CandidateNode{Node: node} - rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode) + rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode) if err != nil { return false, err } diff --git a/pkg/yqlib/operator_booleans_test.go b/pkg/yqlib/operator_booleans_test.go index c505ab8..ce6ef71 100644 --- a/pkg/yqlib/operator_booleans_test.go +++ b/pkg/yqlib/operator_booleans_test.go @@ -67,6 +67,22 @@ var booleanOperatorScenarios = []expressionScenario{ "D0, P[], (doc)::a: true\nb: false\n", }, }, + { + skipDoc: true, + document: `[{pet: cat}]`, + expression: `any_c(.name == "harry") as $c`, + expected: []string{ + "D0, P[], (doc)::[{pet: cat}]\n", + }, + }, + { + skipDoc: true, + document: `[{pet: cat}]`, + expression: `all_c(.name == "harry") as $c`, + expected: []string{ + "D0, P[], (doc)::[{pet: cat}]\n", + }, + }, { skipDoc: true, document: `[false, false]`, diff --git a/pkg/yqlib/operator_has.go b/pkg/yqlib/operator_has.go index 21d3632..474f195 100644 --- a/pkg/yqlib/operator_has.go +++ b/pkg/yqlib/operator_has.go @@ -12,14 +12,21 @@ func hasOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi log.Debugf("-- hasOperation") var results = list.New() - rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) - wanted := rhs.MatchingNodes.Front().Value.(*CandidateNode).Node - wantedKey := wanted.Value + readonlyContext := context.Clone() + readonlyContext.DontAutoCreate = true + rhs, err := d.GetMatchingNodes(readonlyContext, expressionNode.Rhs) if err != nil { return Context{}, err } + wantedKey := "null" + wanted := &yaml.Node{Tag: "!!null"} + if rhs.MatchingNodes.Len() != 0 { + wanted = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node + wantedKey = wanted.Value + } + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { candidate := el.Value.(*CandidateNode) diff --git a/pkg/yqlib/operator_has_test.go b/pkg/yqlib/operator_has_test.go index 453dc0d..ef8e79d 100644 --- a/pkg/yqlib/operator_has_test.go +++ b/pkg/yqlib/operator_has_test.go @@ -13,6 +13,22 @@ var hasOperatorScenarios = []expressionScenario{ "D0, P[], (!!bool)::true\n", }, }, + { + skipDoc: true, + document: `a: hello`, + expression: `has(.b) as $c`, + expected: []string{ + "D0, P[], (doc)::a: hello\n", + }, + }, + { + skipDoc: true, + document: `a: hello`, + expression: `has(.b)`, + expected: []string{ + "D0, P[], (!!bool)::false\n", + }, + }, { description: "Has map key", document: `- a: "yes" diff --git a/pkg/yqlib/operator_select.go b/pkg/yqlib/operator_select.go index c89cbf4..8d033b1 100644 --- a/pkg/yqlib/operator_select.go +++ b/pkg/yqlib/operator_select.go @@ -11,9 +11,7 @@ func selectOperator(d *dataTreeNavigator, context Context, expressionNode *Expre for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { candidate := el.Value.(*CandidateNode) - childContext := context.SingleChildContext(candidate) - childContext.DontAutoCreate = true - rhs, err := d.GetMatchingNodes(childContext, expressionNode.Rhs) + rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs) if err != nil { return Context{}, err diff --git a/pkg/yqlib/operator_sort_keys.go b/pkg/yqlib/operator_sort_keys.go index e5908be..f977a9f 100644 --- a/pkg/yqlib/operator_sort_keys.go +++ b/pkg/yqlib/operator_sort_keys.go @@ -10,7 +10,7 @@ func sortKeysOperator(d *dataTreeNavigator, context Context, expressionNode *Exp for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { candidate := el.Value.(*CandidateNode) - rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode.Rhs) + rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(candidate), expressionNode.Rhs) if err != nil { return Context{}, err } diff --git a/pkg/yqlib/operator_sort_keys_test.go b/pkg/yqlib/operator_sort_keys_test.go index 303b5ad..dc1a909 100644 --- a/pkg/yqlib/operator_sort_keys_test.go +++ b/pkg/yqlib/operator_sort_keys_test.go @@ -13,6 +13,14 @@ var sortKeysOperatorScenarios = []expressionScenario{ "D0, P[], (doc)::{a: blah, b: bing, c: frog}\n", }, }, + { + skipDoc: true, + document: `{c: frog}`, + expression: `sortKeys(.d)`, + expected: []string{ + "D0, P[], (doc)::{c: frog}\n", + }, + }, { description: "Sort keys recursively", subdescription: "Note the array elements are left unsorted, but maps inside arrays are sorted", diff --git a/pkg/yqlib/operator_strings.go b/pkg/yqlib/operator_strings.go index bca8e23..18ef46a 100644 --- a/pkg/yqlib/operator_strings.go +++ b/pkg/yqlib/operator_strings.go @@ -13,7 +13,9 @@ func getSubstituteParameters(d *dataTreeNavigator, block *ExpressionNode, contex regEx := "" replacementText := "" - regExNodes, err := d.GetMatchingNodes(context, block.Lhs) + readonlyContext := context.Clone() + readonlyContext.DontAutoCreate = true + regExNodes, err := d.GetMatchingNodes(readonlyContext, block.Lhs) if err != nil { return "", "", err } @@ -78,7 +80,9 @@ func joinStringOperator(d *dataTreeNavigator, context Context, expressionNode *E log.Debugf("-- joinStringOperator") joinStr := "" - rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) + readonlyContext := context.Clone() + readonlyContext.DontAutoCreate = true + rhs, err := d.GetMatchingNodes(readonlyContext, expressionNode.Rhs) if err != nil { return Context{}, err } @@ -119,7 +123,9 @@ func splitStringOperator(d *dataTreeNavigator, context Context, expressionNode * log.Debugf("-- splitStringOperator") splitStr := "" - rhs, err := d.GetMatchingNodes(context, expressionNode.Rhs) + readonlyContext := context.Clone() + readonlyContext.DontAutoCreate = true + rhs, err := d.GetMatchingNodes(readonlyContext, expressionNode.Rhs) if err != nil { return Context{}, err } diff --git a/pkg/yqlib/operator_unique.go b/pkg/yqlib/operator_unique.go index 92e1a64..785f734 100644 --- a/pkg/yqlib/operator_unique.go +++ b/pkg/yqlib/operator_unique.go @@ -31,15 +31,20 @@ func uniqueBy(d *dataTreeNavigator, context Context, expressionNode *ExpressionN var newMatches = orderedmap.NewOrderedMap() for _, node := range candidateNode.Content { child := &CandidateNode{Node: node} - rhs, err := d.GetMatchingNodes(context.SingleChildContext(child), expressionNode.Rhs) + rhs, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(child), expressionNode.Rhs) if err != nil { return Context{}, err } - first := rhs.MatchingNodes.Front() - keyCandidate := first.Value.(*CandidateNode) - keyValue := keyCandidate.Node.Value + keyValue := "null" + + if rhs.MatchingNodes.Len() > 0 { + first := rhs.MatchingNodes.Front() + keyCandidate := first.Value.(*CandidateNode) + keyValue = keyCandidate.Node.Value + } + _, exists := newMatches.Get(keyValue) if !exists { diff --git a/pkg/yqlib/operator_unique_by_test.go b/pkg/yqlib/operator_unique_test.go similarity index 73% rename from pkg/yqlib/operator_unique_by_test.go rename to pkg/yqlib/operator_unique_test.go index 0ca8e70..bc46578 100644 --- a/pkg/yqlib/operator_unique_by_test.go +++ b/pkg/yqlib/operator_unique_test.go @@ -39,6 +39,22 @@ var uniqueOperatorScenarios = []expressionScenario{ "D0, P[], (!!seq)::- {name: harry, pet: cat}\n- {name: billy, pet: dog}\n", }, }, + { + skipDoc: true, + document: `[{name: harry, pet: cat}, {pet: fish}, {name: harry, pet: dog}]`, + expression: `unique_by(.name)`, + expected: []string{ + "D0, P[], (!!seq)::- {name: harry, pet: cat}\n- {pet: fish}\n", + }, + }, + { + skipDoc: true, + document: `[{name: harry, pet: cat}, {pet: fish}, {name: harry, pet: dog}]`, + expression: `unique_by(.cat.dog)`, + expected: []string{ + "D0, P[], (!!seq)::- {name: harry, pet: cat}\n", + }, + }, } func TestUniqueOperatorScenarios(t *testing.T) {