Skip to content

Commit

Permalink
New merge() function
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Schneider authored Mar 15, 2021
2 parents 797a03a + cecdf0a commit 10dfc8f
Show file tree
Hide file tree
Showing 6 changed files with 643 additions and 0 deletions.
12 changes: 12 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ context.
| `json_decode` | Parses the given JSON string and, if it is valid, returns the value it represents. |
| `json_encode` | Returns a JSON serialization of the given value. |
| `jwt_sign` | jwt_sign creates and signs a JSON Web Token (JWT) from information from a referenced [`jwt_signing_profile` block](#jwt-signing-profile-block) and additional claims provided as a function parameter. |
| `merge` | Deep-merges two or more of either objects or tuples. `null` arguments are ignored. A `null` attribute value in an object removes the previous attribute value. An attribute value with a different type than the current value is set as the new value. `merge()` with no parameters returns `null`. |
| `to_lower` | Converts a given string to lowercase. |
| `to_upper` | Converts a given string to uppercase. |
| `unixtime` | Retrieves the current UNIX timestamp in seconds. |
Expand All @@ -307,6 +308,17 @@ my_json = json_encode({
value-b: ["item1", "item2"]
})
merge({"k1": 1}, null, {"k2": 2}) // -> {"k1": 1, "k2": 2} merge object attributes
merge({"k": [1]}, {"k": [2]}) // -> {"k": [1, 2]} merge tuple values
merge({"k": {"k1": 1]}}, {"k": {"k2": 2}}) // -> {"k": {"k1": 1, "k2": 2}} merge object attributes
merge({"k": [1]}, {"k": null}, {"k": [2]}) // -> {"k": [2]} remove value and set new value
merge({"k": [1]}, {"k": 2}) // -> {"k": 2} set new value
merge([1], null, [2, "3"], [true, false]) // -> [1, 2, "3", true, false] merge tuple values
merge({"k1": 1}, 2) // -> error: cannot mix object with primitive value
merge({"k1": 1}, [2]) // -> error: cannot mix object with tuple
merge([1], 2) // -> error: cannot mix tuple with primitive value
token = jwt_sign("MyJwt", {"sub": "abc12345"})
definitions {
Expand Down
1 change: 1 addition & 0 deletions eval/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ func newFunctionsMap() map[string]function.Function {
"coalesce": stdlib.CoalesceFunc,
"json_decode": stdlib.JSONDecodeFunc,
"json_encode": stdlib.JSONEncodeFunc,
"merge": lib.MergeFunc,
"to_lower": stdlib.LowerFunc,
"to_upper": stdlib.UpperFunc,
"unixtime": lib.UnixtimeFunc,
Expand Down
110 changes: 110 additions & 0 deletions eval/lib/merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package lib

import (
"errors"

"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)

var (
MergeFunc = newMergeFunction()
)

func newMergeFunction() function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "maps",
Type: cty.DynamicPseudoType,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (cty.Type, error) {
// empty args is accepted, so assume an empty object since we have no
// key-value types.
if len(args) == 0 {
return cty.Bool, nil
}
return cty.DynamicPseudoType, nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return merge(args)
},
})
}

func merge(args []cty.Value) (cty.Value, error) {
var t string
for _, arg := range args {
if arg.IsNull() {
continue
}
at := arg.Type()
if at.IsPrimitiveType() {
return cty.StringVal(""), errors.New("cannot merge primitive value")
}
if at.IsObjectType() || at.IsMapType() {
if t == "" {
t = "o"
} else if t != "o" {
return cty.StringVal(""), errors.New("type mismatch")
}
} else if at.IsTupleType() || at.IsListType() {
if t == "" {
t = "l"
} else if t != "l" {
return cty.StringVal(""), errors.New("type mismatch")
}
}
}
if t == "o" {
return mergeObjects(args), nil
}
if t == "l" {
return mergeTuples(args), nil
}
return cty.NullVal(cty.Bool), nil
}

func mergeObjects(args []cty.Value) cty.Value {
outputMap := make(map[string]cty.Value)
for _, arg := range args {
if arg.IsNull() {
continue
}
for it := arg.ElementIterator(); it.Next(); {
k, v := it.Element()
if v.IsNull() {
delete(outputMap, k.AsString())
} else if existingVal, ok := outputMap[k.AsString()]; !ok {
// key not set
outputMap[k.AsString()] = v
} else if vType := v.Type(); vType.IsPrimitiveType() {
// primitive type
outputMap[k.AsString()] = v
} else if existingValType := existingVal.Type(); existingValType.IsObjectType() && (vType.IsObjectType() || vType.IsMapType()) {
outputMap[k.AsString()] = mergeObjects([]cty.Value{existingVal, v})
} else if existingValType.IsTupleType() && (vType.IsTupleType() || vType.IsListType()) {
outputMap[k.AsString()] = mergeTuples([]cty.Value{existingVal, v})
} else {
outputMap[k.AsString()] = v
}
}
}
return cty.ObjectVal(outputMap)
}

func mergeTuples(args []cty.Value) cty.Value {
outputList := []cty.Value{}
for _, arg := range args {
if arg.IsNull() {
continue
}
for it := arg.ElementIterator(); it.Next(); {
_, v := it.Element()
outputList = append(outputList, v)
}
}
return cty.TupleVal(outputList)
}
Loading

0 comments on commit 10dfc8f

Please sign in to comment.