Skip to content

Commit

Permalink
Merge pull request #195 from mattfarina/v2deepcopy
Browse files Browse the repository at this point in the history
V2deepcopy
  • Loading branch information
mattfarina authored Oct 2, 2019
2 parents 04ad0b0 + 9a07700 commit 673efda
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 13 deletions.
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)
}
}
}

0 comments on commit 673efda

Please sign in to comment.