Skip to content

Commit

Permalink
Merge pull request #15 from thallgren/deepcopy
Browse files Browse the repository at this point in the history
Ensure that unfrozen copy is deep
  • Loading branch information
thallgren committed Feb 18, 2020
2 parents 45acaa6 + 27b9eab commit acb5a01
Show file tree
Hide file tree
Showing 14 changed files with 141 additions and 7 deletions.
3 changes: 3 additions & 0 deletions dgo/array.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ type (
// FrozenCopy checks if the receiver is frozen. If it is, it returned. If not, a frozen copy
// of the receiver is returned.
FrozenCopy() Value

// ThawedCopy returns a thawed copy of the receiver.
ThawedCopy() Value
}

// Iterable enables the implementor to express how iteration is performed over contained elements
Expand Down
4 changes: 4 additions & 0 deletions internal/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ func (a *alias) FrozenCopy() dgo.Value {
panic(a.freezeAttempt())
}

func (a *alias) ThawedCopy() dgo.Value {
return a
}

func (a *alias) freezeAttempt() error {
return fmt.Errorf(`attempt to freeze unresolved alias '%s'`, a.Reference())
}
Expand Down
10 changes: 10 additions & 0 deletions internal/array.go
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,12 @@ func (v *array) Copy(frozen bool) dgo.Array {
cp[i] = f.FrozenCopy()
}
}
} else {
for i := range cp {
if f, ok := cp[i].(dgo.Freezable); ok {
cp[i] = f.ThawedCopy()
}
}
}
return &array{slice: cp, frozen: frozen}
}
Expand Down Expand Up @@ -1070,6 +1076,10 @@ func (v *array) FrozenCopy() dgo.Value {
return v.Copy(true)
}

func (v *array) ThawedCopy() dgo.Value {
return v.Copy(false)
}

func (v *array) GoSlice() []dgo.Value {
if v.frozen {
return util.SliceCopy(v.slice)
Expand Down
11 changes: 5 additions & 6 deletions internal/binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,12 +355,11 @@ func (v *binary) Frozen() bool {
}

func (v *binary) FrozenCopy() dgo.Value {
if !v.frozen {
cs := make([]byte, len(v.bytes))
copy(cs, v.bytes)
return &binary{bytes: cs, frozen: true}
}
return v
return v.Copy(true)
}

func (v *binary) ThawedCopy() dgo.Value {
return v.Copy(false)
}

func (v *binary) GoBytes() []byte {
Expand Down
4 changes: 4 additions & 0 deletions internal/binary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ func TestBinary_Copy(t *testing.T) {
c = c.Copy(true)
require.True(t, c.Frozen())
require.Same(t, c, c.Copy(true))

c = a.ThawedCopy().(dgo.Binary)
require.False(t, c.Frozen())
require.NotSame(t, c, c.ThawedCopy())
}

func TestBinary_Equal(t *testing.T) {
Expand Down
30 changes: 30 additions & 0 deletions internal/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ func (v *mapEntry) FrozenCopy() dgo.Value {
return c
}

func (v *mapEntry) ThawedCopy() dgo.Value {
c := &mapEntry{key: v.key, value: v.value}
if f, ok := v.value.(dgo.Freezable); ok {
v.value = f.ThawedCopy()
}
return c
}

func (v *hashNode) FrozenCopy() dgo.Value {
if v.Frozen() {
return v
Expand All @@ -140,6 +148,14 @@ func (v *hashNode) FrozenCopy() dgo.Value {
return c
}

func (v *hashNode) ThawedCopy() dgo.Value {
c := &hashNode{mapEntry: mapEntry{key: v.key, value: v.value}}
if f, ok := v.value.(dgo.Freezable); ok {
v.value = f.ThawedCopy()
}
return c
}

func (v *mapEntry) Key() dgo.Value {
return v.key
}
Expand All @@ -164,6 +180,12 @@ func (v *mapEntry) copyFreeze() {
}
}

func (v *mapEntry) copyThaw() {
if f, ok := v.value.(dgo.Freezable); ok {
v.value = f.ThawedCopy()
}
}

var emptyMap = &hashMap{frozen: true}

// Map creates an immutable dgo.Map from the given slice which must have 0, 1, or an
Expand Down Expand Up @@ -406,6 +428,10 @@ func (g *hashMap) Copy(frozen bool) dgo.Map {
for e := c.first; e != nil; e = e.next {
e.copyFreeze()
}
} else {
for e := c.first; e != nil; e = e.next {
e.copyThaw()
}
}
return c
}
Expand Down Expand Up @@ -475,6 +501,10 @@ func (g *hashMap) FrozenCopy() dgo.Value {
return g.Copy(true)
}

func (g *hashMap) ThawedCopy() dgo.Value {
return g.Copy(false)
}

func (g *hashMap) Get(key interface{}) dgo.Value {
tbl := g.table
tl := len(tbl) - 1
Expand Down
24 changes: 24 additions & 0 deletions internal/map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,16 @@ func TestMap_EntryType(t *testing.T) {
vt := v.Type()
require.Equal(t, `"a":{1,2}`, vt.String())
})
m = m.FrozenCopy().(dgo.Map)
m.EachEntry(func(v dgo.MapEntry) {
require.True(t, v.Frozen())
require.NotSame(t, v, v.ThawedCopy())
})
m = m.ThawedCopy().(dgo.Map)
m.EachEntry(func(v dgo.MapEntry) {
require.False(t, v.Frozen())
require.NotSame(t, v, v.ThawedCopy())
})
}

func TestNewMapType_max_min(t *testing.T) {
Expand Down Expand Up @@ -981,6 +991,20 @@ func TestMapEntry_Frozen(t *testing.T) {
require.NotSame(t, e, e.FrozenCopy())
}

func TestMapEntry_Thawed(t *testing.T) {
e := internal.NewMapEntry(`a`, 1)
require.NotSame(t, e, e.ThawedCopy())

e = e.FrozenCopy().(dgo.MapEntry)
require.NotSame(t, e, e.ThawedCopy())

e = internal.NewMapEntry(`a`, vf.MutableValues(`a`))
c := e.ThawedCopy().(dgo.MapEntry)
c.Value().(dgo.Array).Set(0, `b`)
require.Equal(t, vf.Values(`a`), e.Value())
require.Equal(t, vf.Values(`b`), c.Value())
}

func TestMapEntry_String(t *testing.T) {
vf.Map(`a`, 1).EachEntry(func(e dgo.MapEntry) {
require.Equal(t, `"a":1`, e.String())
Expand Down
4 changes: 4 additions & 0 deletions internal/native.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ func (v *native) FrozenCopy() dgo.Value {
panic(fmt.Errorf(`native value cannot be frozen`))
}

func (v *native) ThawedCopy() dgo.Value {
panic(fmt.Errorf(`native value cannot be copied`))
}

func (v *native) HashCode() int {
rv := (*reflect.Value)(v)
switch rv.Kind() {
Expand Down
1 change: 1 addition & 0 deletions internal/native_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func TestNative(t *testing.T) {
require.False(t, c.Frozen())
require.Panic(t, func() { c.Freeze() }, `cannot be frozen`)
require.Panic(t, func() { c.FrozenCopy() }, `cannot be frozen`)
require.Panic(t, func() { c.ThawedCopy() }, `cannot be copied`)

require.NotEqual(t, 0, s.HashCode())
require.Equal(t, s.HashCode(), s.HashCode())
Expand Down
7 changes: 7 additions & 0 deletions internal/sensitive.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ func (v *sensitive) FrozenCopy() dgo.Value {
return v
}

func (v *sensitive) ThawedCopy() dgo.Value {
if f, ok := v.value.(dgo.Freezable); ok {
return &sensitive{f.ThawedCopy()}
}
return v
}

func (v *sensitive) HashCode() int {
return deepHashCode(nil, v)
}
Expand Down
10 changes: 9 additions & 1 deletion internal/sensitive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,18 @@ func TestSensitive(t *testing.T) {
require.Equal(t, s.Unwrap(), c.Unwrap())
require.NotSame(t, s.Unwrap(), c.Unwrap())

s = vf.Sensitive(vf.String(`a`))
s = vf.Sensitive(vf.MutableValues(`a`))
require.False(t, s.Frozen())
c = s.FrozenCopy().(dgo.Sensitive)
require.NotSame(t, s, c)
require.True(t, c.Frozen())
s = c.FrozenCopy().(dgo.Sensitive)
require.Same(t, s, c)

c = s.ThawedCopy().(dgo.Sensitive)
require.NotSame(t, s, c)
require.False(t, c.Frozen()) // string is frozen regardless

require.Equal(t, `sensitive [value redacted]`, s.String())

require.NotEqual(t, typ.Sensitive.HashCode(), s.HashCode())
Expand Down
15 changes: 15 additions & 0 deletions internal/struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,21 @@ func (v *structVal) FrozenCopy() dgo.Value {
return &structVal{rs: rs, frozen: true}
}

func (v *structVal) ThawedCopy() dgo.Value {
// Perform a by-value copy of the struct
rs := reflect.New(v.rs.Type()).Elem() // create and dereference pointer to a zero value
rs.Set(v.rs) // copy v.rs to the zero value

for i, n := 0, rs.NumField(); i < n; i++ {
ef := rs.Field(i)
ev := ValueFromReflected(ef)
if f, ok := ev.(dgo.Freezable); ok {
ReflectTo(f.ThawedCopy(), ef)
}
}
return &structVal{rs: rs, frozen: false}
}

func (v *structVal) Find(predicate dgo.EntryPredicate) dgo.MapEntry {
rv := v.rs
rt := rv.Type()
Expand Down
22 changes: 22 additions & 0 deletions internal/struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,28 @@ func Test_structMap_FrozenCopy(t *testing.T) {
require.Panic(t, func() { c.Put(`A`, `Adam`) }, `frozen`)
}

func Test_structMap_ThawedCopy(t *testing.T) {
type structA struct {
A string
E dgo.Array
}
s := structA{A: `Alpha`}
m := vf.Map(&s)
m.Put(`E`, vf.MutableValues(`Echo`, `Foxtrot`))
m.Freeze()

c := m.ThawedCopy().(dgo.Map)
require.True(t, m.Frozen())
require.True(t, m.Get(`E`).(dgo.Freezable).Frozen())
require.False(t, c.Frozen())
require.False(t, c.Get(`E`).(dgo.Freezable).Frozen())

c.Put(`A`, `Adam`)
require.Equal(t, `Adam`, c.Get(`A`))
require.Equal(t, `Alpha`, m.Get(`A`))
require.NotSame(t, c, c.FrozenCopy())
}

func Test_structMap_HashCode(t *testing.T) {
type structA struct {
A string
Expand Down
3 changes: 3 additions & 0 deletions internal/type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ func TestFromReflected(t *testing.T) {
v = tf.FromReflected(reflect.ValueOf(map[int]string{1: `a`}).Type())
require.Assignable(t, tf.Map(typ.Integer, typ.String), v)

v = tf.FromReflected(reflect.ValueOf(map[int]interface{}{1: `a`}).Type())
require.Assignable(t, tf.Map(typ.Integer, typ.Any), v)

v = tf.FromReflected(reflect.ValueOf(&map[int]string{1: `a`}).Type())
require.Assignable(t, v, tf.Map(typ.Integer, typ.String))
require.Assignable(t, v, typ.Nil)
Expand Down

0 comments on commit acb5a01

Please sign in to comment.