diff --git a/README.md b/README.md index d4673ba..843f253 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/merge.go b/merge.go index 18d3b75..3fae512 100644 --- a/merge.go +++ b/merge.go @@ -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 @@ -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 diff --git a/merge_test.go b/merge_test.go index 57eca5c..fb68b4a 100644 --- a/merge_test.go +++ b/merge_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "reflect" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" @@ -276,6 +277,10 @@ var _ = Describe("Merge", func() { true, false, ), + Entry("time", + time.Now(), + time.Now().Add(time.Hour), + ), ) Context("merge map", func() { @@ -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")) }) }) }) diff --git a/mfunc.go b/mfunc.go index bf20dda..3fd92da 100644 --- a/mfunc.go +++ b/mfunc.go @@ -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", diff --git a/mfunc_test.go b/mfunc_test.go index e5cc5dc..35974bb 100644 --- a/mfunc_test.go +++ b/mfunc_test.go @@ -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)) + }) + }) }) })