Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2deepcopy #195

Merged
merged 2 commits into from
Oct 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go:
- 1.10.x
- 1.11.x
- 1.12.x
- 1.13.x
- tip

# Setting sudo access to false will let Travis CI use containers rather than
Expand Down
20 changes: 16 additions & 4 deletions dict.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package sprig

import "github.com/imdario/mergo"
import (
"github.com/imdario/mergo"
"github.com/mitchellh/copystructure"
)

func set(d map[string]interface{}, key string, value interface{}) map[string]interface{} {
d[key] = value
Expand Down Expand Up @@ -88,13 +91,13 @@ func merge(dst map[string]interface{}, srcs ...map[string]interface{}) interface
}

func mergeOverwrite(dst map[string]interface{}, srcs ...map[string]interface{}) interface{} {
for _, src := range srcs {
for _, src := range srcs {
if err := mergo.MergeWithOverwrite(&dst, src); err != nil {
// Swallow errors inside of a template.
return ""
}
}
return dst
}
return dst
}

func values(dict map[string]interface{}) []interface{} {
Expand All @@ -105,3 +108,12 @@ func values(dict map[string]interface{}) []interface{} {

return values
}

func deepCopy(i interface{}) interface{} {
c, err := copystructure.Copy(i)
if err != nil {
panic("deepCopy error: " + err.Error())
}

return c
}
24 changes: 19 additions & 5 deletions dict_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func TestMerge(t *testing.T) {
"i": "eye", // overridden twice
"j": "jay", // overridden and merged
"k": map[string]interface{}{
"l": true, // overriden
"l": true, // overriden
},
}
assert.Equal(t, expected, dict["dst"])
Expand Down Expand Up @@ -222,9 +222,9 @@ func TestMergeOverwrite(t *testing.T) {
t.Error(err)
}
expected := map[string]interface{}{
"a": 1, // key overwritten from src1
"b": 2, // merged from src1
"c": 3, // merged from dst
"a": 1, // key overwritten from src1
"b": 2, // merged from src1
"c": 3, // merged from dst
"d": map[string]interface{}{ // deep merge
"e": "four",
"f": 5,
Expand All @@ -233,7 +233,7 @@ func TestMergeOverwrite(t *testing.T) {
"h": 10, // merged from src2
"i": "i", // overwritten twice src2 wins
"j": "j", // overwritten twice src2 wins
"k": map[string]interface{} { // deep merge
"k": map[string]interface{}{ // deep merge
"l": false, // overwritten src1 wins
},
}
Expand All @@ -252,3 +252,17 @@ func TestValues(t *testing.T) {
}
}
}

func TestDeepCopy(t *testing.T) {
tests := map[string]string{
`{{- $d := dict "a" 1 "b" 2 | deepCopy }}{{ values $d | sortAlpha | join "," }}`: "1,2",
`{{- $d := dict "a" 1 "b" 2 | deepCopy }}{{ keys $d | sortAlpha | join "," }}`: "a,b",
`{{- $one := dict "foo" (dict "bar" "baz") "qux" true -}}{{ deepCopy $one }}`: "map[foo:map[bar:baz] qux:true]",
}

for tpl, expect := range tests {
if err := runt(tpl, expect); err != nil {
t.Error(err)
}
}
}
27 changes: 25 additions & 2 deletions docs/dicts.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,13 @@ Merge two or more dictionaries into one, giving precedence to the dest dictionar
$newdict := merge $dest $source1 $source2
```

This is a deep merge operation.
This is a deep merge operation but not a deep copy operation. Nested objects that
are merged are the same instance on both dicts. If you want a deep copy along
with the merge than use the `deepCopy` function along with merging. For example,

```
deepCopy $source | merge $dest
```

## mergeOverwrite

Expand Down Expand Up @@ -116,7 +122,13 @@ newdict:
$newdict := mergeOverwrite $dest $source1 $source2
```

This is a deep merge operation.
This is a deep merge operation but not a deep copy operation. Nested objects that
are merged are the same instance on both dicts. If you want a deep copy along
with the merge than use the `deepCopy` function along with merging. For example,

```
deepCopy $source | mergeOverwrite $dest
```

## keys

Expand Down Expand Up @@ -170,6 +182,17 @@ The above returns `list["value1", "value2", "value 3"]`. Note that the `values`
function gives no guarantees about the result ordering- if you care about this,
then use `sortAlpha`.

## deepCopy, mustDeepCopy

The `deepCopy` and `mustDeepCopy` functions takes a value and makes a deep copy
of the value. This includes dicts and other structures. `deepCopy` panics
when there is a problem while `mustDeepCopy` returns an error to the template
system when there is an error.

```
dict "a" 1 "b" 2 | deepCopy
```

## A Note on Dict Internals

A `dict` is implemented in Go as a `map[string]interface{}`. Go developers can
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The Sprig library provides over 70 template functions for Go's template language
- [Defaults Functions](defaults.md): `default`, `empty`, `coalesce`, `toJson`, `toPrettyJson`, `ternary`
- [Encoding Functions](encoding.md): `b64enc`, `b64dec`, etc.
- [Lists and List Functions](lists.md): `list`, `first`, `uniq`, etc.
- [Dictionaries and Dict Functions](dicts.md): `dict`, `hasKey`, `pluck`, etc.
- [Dictionaries and Dict Functions](dicts.md): `dict`, `hasKey`, `pluck`, `deepCopy`, etc.
- [Type Conversion Functions](conversion.md): `atoi`, `int64`, `toString`, etc.
- [File Path Functions](paths.md): `base`, `dir`, `ext`, `clean`, `isAbs`
- [Flow Control Functions](flow_control.md): `fail`
Expand Down
3 changes: 2 additions & 1 deletion functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ var genericMap = map[string]interface{}{
"empty": empty,
"coalesce": coalesce,
"compact": compact,
"deepCopy": deepCopy,
"toJson": toJson,
"toPrettyJson": toPrettyJson,
"ternary": ternary,
Expand Down Expand Up @@ -301,5 +302,5 @@ var genericMap = map[string]interface{}{

// URLs:
"urlParse": urlParse,
"urlJoin": urlJoin,
"urlJoin": urlJoin,
}
2 changes: 2 additions & 0 deletions glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ import:
version: ~0.3.7
- package: github.com/huandu/xstrings
version: ^1.2
- package: github.com/mitchellh/copystructure
version: ^1.0.0
33 changes: 33 additions & 0 deletions issue_188_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package sprig

import (
"testing"
)

func TestIssue188(t *testing.T) {
tests := map[string]string{

// This first test shows two merges and the merge is NOT A DEEP COPY MERGE.
// The first merge puts $one on to $target. When the second merge of $two
// on to $target the nested dict brought over from $one is changed on
// $one as well as $target.
`{{- $target := dict -}}
{{- $one := dict "foo" (dict "bar" "baz") "qux" true -}}
{{- $two := dict "foo" (dict "bar" "baz2") "qux" false -}}
{{- mergeOverwrite $target $one | toString | trunc 0 }}{{ $__ := mergeOverwrite $target $two }}{{ $one }}`: "map[foo:map[bar:baz2] qux:true]",

// This test uses deepCopy on $one to create a deep copy and then merge
// that. In this case the merge of $two on to $target does not affect
// $one because a deep copy was used for that merge.
`{{- $target := dict -}}
{{- $one := dict "foo" (dict "bar" "baz") "qux" true -}}
{{- $two := dict "foo" (dict "bar" "baz2") "qux" false -}}
{{- deepCopy $one | mergeOverwrite $target | toString | trunc 0 }}{{ $__ := mergeOverwrite $target $two }}{{ $one }}`: "map[foo:map[bar:baz] qux:true]",
}

for tpl, expect := range tests {
if err := runt(tpl, expect); err != nil {
t.Error(err)
}
}
}