From 14f8f92b762c960cebb704f5095a4dca6a69c27d Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Wed, 1 Dec 2021 10:32:36 +1100 Subject: [PATCH] Added map, map_values --- pkg/yqlib/doc/headers/map.md | 3 ++ pkg/yqlib/doc/map.md | 40 +++++++++++++++++++ pkg/yqlib/expression_tokeniser.go | 3 ++ pkg/yqlib/lib.go | 2 + pkg/yqlib/operator_map.go | 64 +++++++++++++++++++++++++++++++ pkg/yqlib/operator_map_test.go | 51 ++++++++++++++++++++++++ 6 files changed, 163 insertions(+) create mode 100644 pkg/yqlib/doc/headers/map.md create mode 100644 pkg/yqlib/doc/map.md create mode 100644 pkg/yqlib/operator_map.go create mode 100644 pkg/yqlib/operator_map_test.go diff --git a/pkg/yqlib/doc/headers/map.md b/pkg/yqlib/doc/headers/map.md new file mode 100644 index 00000000..e5fef62d --- /dev/null +++ b/pkg/yqlib/doc/headers/map.md @@ -0,0 +1,3 @@ +# Map + +Maps values of an array. Use `map_values` to map values of an object. diff --git a/pkg/yqlib/doc/map.md b/pkg/yqlib/doc/map.md new file mode 100644 index 00000000..1d64292c --- /dev/null +++ b/pkg/yqlib/doc/map.md @@ -0,0 +1,40 @@ +# Map + +Maps values of an array. Use `map_values` to map values of an object. + +## Map array +Given a sample.yml file of: +```yaml +- 1 +- 2 +- 3 +``` +then +```bash +yq eval 'map(. + 1)' sample.yml +``` +will output +```yaml +- 2 +- 3 +- 4 +``` + +## Map object values +Given a sample.yml file of: +```yaml +a: 1 +b: 2 +c: 3 +``` +then +```bash +yq eval 'map_values(. + 1)' sample.yml +``` +will output +```yaml +a: 2 +b: 3 +c: 4 +``` + diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index 5951ca20..7ff5dca8 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -312,6 +312,9 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`:\s*`), opToken(createMapOpType)) lexer.Add([]byte(`length`), opToken(lengthOpType)) + lexer.Add([]byte(`map`), opToken(mapOpType)) + lexer.Add([]byte(`map_values`), opToken(mapValuesOpType)) + lexer.Add([]byte(`flatten\([0-9]+\)`), flattenWithDepth()) lexer.Add([]byte(`flatten`), opTokenWithPrefs(flattenOpType, nil, flattenPreferences{depth: -1})) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index d19d03bd..410c07ac 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -64,6 +64,8 @@ var shortPipeOpType = &operationType{Type: "SHORT_PIPE", NumArgs: 2, Precedence: var lengthOpType = &operationType{Type: "LENGTH", NumArgs: 0, Precedence: 50, Handler: lengthOperator} var collectOpType = &operationType{Type: "COLLECT", NumArgs: 1, Precedence: 50, Handler: collectOperator} +var mapOpType = &operationType{Type: "MAP", NumArgs: 1, Precedence: 50, Handler: mapOperator} +var mapValuesOpType = &operationType{Type: "MAP_VALUES", NumArgs: 1, Precedence: 50, Handler: mapValuesOperator} var encodeOpType = &operationType{Type: "ENCODE", NumArgs: 0, Precedence: 50, Handler: encodeOperator} var decodeOpType = &operationType{Type: "DECODE", NumArgs: 0, Precedence: 50, Handler: decodeOperator} diff --git a/pkg/yqlib/operator_map.go b/pkg/yqlib/operator_map.go new file mode 100644 index 00000000..a28b652a --- /dev/null +++ b/pkg/yqlib/operator_map.go @@ -0,0 +1,64 @@ +package yqlib + +import ( + "container/list" +) + +func mapValuesOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + //run expression against entries + // splat toEntries and pipe it into Rhs + splatted, err := splat(d, context.SingleChildContext(candidate), traversePreferences{}) + if err != nil { + return Context{}, err + } + + assignUpdateExp := &ExpressionNode{ + Operation: &Operation{OperationType: assignOpType, UpdateAssign: true}, + Rhs: expressionNode.Rhs, + } + _, err = assignUpdateOperator(d, splatted, assignUpdateExp) + if err != nil { + return Context{}, err + } + + } + + return context, nil +} + +func mapOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + + var results = list.New() + + for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { + candidate := el.Value.(*CandidateNode) + //run expression against entries + // splat toEntries and pipe it into Rhs + splatted, err := splat(d, context.SingleChildContext(candidate), traversePreferences{}) + if err != nil { + return Context{}, err + } + + result, err := d.GetMatchingNodes(splatted, expressionNode.Rhs) + log.Debug("expressionNode.Rhs %v", expressionNode.Rhs.Operation.OperationType) + log.Debug("result %v", result) + if err != nil { + return Context{}, err + } + + selfExpression := &ExpressionNode{Operation: &Operation{OperationType: selfReferenceOpType}} + collected, err := collectTogether(d, result, selfExpression) + if err != nil { + return Context{}, err + } + collected.Node.Style = unwrapDoc(candidate.Node).Style + + results.PushBack(collected) + + } + + return context.ChildContext(results), nil +} diff --git a/pkg/yqlib/operator_map_test.go b/pkg/yqlib/operator_map_test.go new file mode 100644 index 00000000..1dd5db57 --- /dev/null +++ b/pkg/yqlib/operator_map_test.go @@ -0,0 +1,51 @@ +package yqlib + +import ( + "testing" +) + +var mapOperatorScenarios = []expressionScenario{ + { + skipDoc: true, + document: `[1,2,3]`, + document2: `[5,6,7]`, + expression: `map(. + 1)`, + expected: []string{ + "D0, P[], (!!seq)::[2, 3, 4]\n", + "D0, P[], (!!seq)::[6, 7, 8]\n", + }, + }, + { + description: "Map array", + document: `[1,2,3]`, + expression: `map(. + 1)`, + expected: []string{ + "D0, P[], (!!seq)::[2, 3, 4]\n", + }, + }, + { + skipDoc: true, + document: `{a: 1, b: 2, c: 3}`, + document2: `{x: 10, y: 20, z: 30}`, + expression: `map_values(. + 1)`, + expected: []string{ + "D0, P[], (doc)::{a: 2, b: 3, c: 4}\n", + "D0, P[], (doc)::{x: 11, y: 21, z: 31}\n", + }, + }, + { + description: "Map object values", + document: `{a: 1, b: 2, c: 3}`, + expression: `map_values(. + 1)`, + expected: []string{ + "D0, P[], (doc)::{a: 2, b: 3, c: 4}\n", + }, + }, +} + +func TestMapOperatorScenarios(t *testing.T) { + for _, tt := range mapOperatorScenarios { + testScenario(t, &tt) + } + documentScenarios(t, "map", mapOperatorScenarios) +}