Skip to content
This repository has been archived by the owner on Dec 23, 2024. It is now read-only.

Commit

Permalink
Merge pull request #9 from InVisionApp/unexported
Browse files Browse the repository at this point in the history
Handle Unexported Struct Fields
  • Loading branch information
talpert authored Jun 30, 2017
2 parents 54f1632 + 6d8bf28 commit cf08399
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 11 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ results in:
}
```

### Options
**Overwrite** `bool`
If true, overwrite a target value with source value even if it already exists

**ErrorOnUnexported** `bool`
Unexported fields on a struct can not be set. When a struct contains an unexported
field, the default behavior is to treat the entire struct as a single entity and
replace according to Overwrite settings.
If this is enabled, an error will be thrown instead.

### Custom Merge Functions
#### Define a custom merge function for a type:
```go
Expand Down
11 changes: 10 additions & 1 deletion merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ type Options struct {
// Overwrite a target value with source value even if it already exists
Overwrite bool

// Unexported fields on a struct can not be set. When a struct contains an unexported
// field, the default behavior is to treat the entire struct as a single entity and
// replace according to Overwrite settings. If this is enabled, an error will be thrown instead.
//
// Note: this is used by the default mergeStruct function, and may not apply if that is
// overwritten with a custom function. Custom struct merge functions should consider
// using this value as well.
ErrorOnUnexported bool

// A set of default and customizable functions that define how values are merged
MergeFuncs *funcSelector

Expand All @@ -35,7 +44,7 @@ func Merge(target, source interface{}, opt *Options) error {
}

if !reflect.Indirect(vT).IsValid() {
return errors.New("can not assign to zero value target. Use MergeCopy")
return errors.New("target can not be zero value")
}

// use defaults if none are provided
Expand Down
7 changes: 6 additions & 1 deletion merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"reflect"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
Expand Down Expand Up @@ -276,6 +277,10 @@ var _ = Describe("Merge", func() {
true,
false,
),
Entry("time",
time.Now(),
time.Now().Add(time.Hour),
),
)

Context("merge map", func() {
Expand Down Expand Up @@ -354,7 +359,7 @@ var _ = Describe("Merge", func() {

err := Merge(target, &source, NewOptions())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("can not assign to zero value target. Use MergeCopy"))
Expect(err.Error()).To(Equal("target can not be zero value"))
})
})
})
Expand Down
14 changes: 10 additions & 4 deletions mfunc.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,18 @@ func mergeStruct(t, s reflect.Value, o *Options) (reflect.Value, error) {
fieldT := newT.Field(i)
logrus.Debug("merging struct field %s", fieldT)

//should never happen because its created above. Maybe remove?
if !fieldT.IsValid() || !fieldT.CanSet() {
return reflect.Value{}, fmt.Errorf("problem with field(%s) valid: %v; can set: %v",
newT.Type().Field(i).Name, fieldT.IsValid(), fieldT.CanSet())
// field is addressable because it's created above. So this means it is unexported.
if !fieldT.CanSet() {
if o.ErrorOnUnexported {
return reflect.Value{}, fmt.Errorf("struct of type %v has unexported field: %s",
t.Type().Name(), newT.Type().Field(i).Name)
}

// revert to using the default func instead to treat the struct as single entity
return defaultMergeFunc(t, s, o)
}

//fieldT should always be valid because it's created above
merged, err := merge(valT.Field(i), valS.Field(i), o)
if err != nil {
return reflect.Value{}, fmt.Errorf("failed to merge field `%s.%s`: %v",
Expand Down
41 changes: 36 additions & 5 deletions mfunc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -946,11 +946,42 @@ var _ = Describe("mergeStruct", func() {
sourceBazVal = reflect.ValueOf(sourceBaz)
})

It("errors", func() {
merged, err := mergeStruct(targetBazVal, sourceBazVal, NewOptions())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("problem with field(private) valid: true; can set: false"))
Expect(merged.IsValid()).ToNot(BeTrue())
Context("ErrOnUnexported is true", func() {
It("errors", func() {
opt := NewOptions()
opt.ErrorOnUnexported = true
merged, err := mergeStruct(targetBazVal, sourceBazVal, opt)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("struct of type Baz has unexported field: private"))
Expect(merged.IsValid()).ToNot(BeTrue())
})
})

Context("ErrOnUnexported is false", func() {
var opt *Options

BeforeEach(func() {
opt = NewOptions()
opt.ErrorOnUnexported = false
})

Context("overwrite is true", func() {
It("replaces the whole struct", func() {
opt.Overwrite = true
merged, err := mergeStruct(targetBazVal, sourceBazVal, opt)
Expect(err).ToNot(HaveOccurred())
Expect(merged).To(Equal(sourceBazVal))
})
})

Context("overwrite is true", func() {
It("replaces the whole struct", func() {
opt.Overwrite = false
merged, err := mergeStruct(targetBazVal, sourceBazVal, opt)
Expect(err).ToNot(HaveOccurred())
Expect(merged).To(Equal(targetBazVal))
})
})
})
})

Expand Down

0 comments on commit cf08399

Please sign in to comment.