darccio / mergo

Mergo: merging Go structs and maps since 2013
BSD 3-Clause "New" or "Revised" License
2.89k stars 271 forks source link

Merge Slices #233

Open sunsingerus opened 1 year ago

sunsingerus commented 1 year ago

Right now mergo provides two options to handle slices:

  1. WithAppendSlice - which appends all items from src to dst
  2. WithSliceDeepCopy - which merges with override (override is mandatory and not configurable) min(len(src), len(dst)) items

There is also WithOverrideEmptySlice option, but it is little irrelevant to the discussion.

In case one needs to deep merge (not copy part of the src with override) elements of slices and add to dst items that are in src on places that are beyond len(dst), looks like there is no such an option available at the moment. Better to show piece of code with src, dst and expected values. Example:

type SimpleStruct struct {
    Field1 string
    Field2 string
    Field3 string
}

type StructWithSliceOfSimpleStructs struct {
    SliceOfSimpleStructs []SimpleStruct
}

src = StructWithSliceOfSimpleStructs{
    SliceOfSimpleStructs: []SimpleStruct{
        {
            Field1: "src:Slice[0].Field1",
            Field2: "src:Slice[0].Field2",
            Field3: "",
        },
        {
            Field1: "src:Slice[1].Field1",
            Field2: "src:Slice[1].Field2",
            Field3: "",
        },
    },
}
dst = StructWithSliceOfSimpleStructs{
    SliceOfSimpleStructs: []SimpleStruct{
        {
            Field1: "dst:Slice[0].Field1",
            Field2: "",
            Field3: "dst:Slice[0].Field3",
        },
    },
}
expected := StructWithSliceOfSimpleStructs{
    SliceOfSimpleStructs: []SimpleStruct{
        {
            // Original dst field is expected not to be overwritten by value
            Field1: "dst:Slice[0].Field1",
            // Empty dst field is expected to be filled with src value
            Field2: "src:Slice[0].Field2",
            // Original dst field is expected not to be overwritten by empty value
            Field3: "dst:Slice[0].Field3",
        },
        // Expected dst being appended
        {
            Field1: "src:Slice[1].Field1",
            Field2: "src:Slice[1].Field2",
            Field3: "",
        },
    },
}

Let's add new option, say mergo.WithSliceDeepMerge, (ispired by mergo.WithSliceDeepCopy) but with the functionality described. Also it would be nice to have existing options such as mergo.WithOverride and mergo.WithOverwriteWithEmptyValue being able to tune merge process. Appending extra elements from dst to src may be done configurable as well.

I am ready to make PR with described functionality as well.

Upvote & Fund

Fund with Polar

sunsingerus commented 1 year ago

PR #234

darccio commented 1 year ago

@sunsingerus I'm marking this as a feature for v2.

sunsingerus commented 1 year ago

@darccio any chances of merging my PR #234 into v2 ? What steps should be performed in order to get PR #234 accepted?

darccio commented 1 year ago

@sunsingerus v2 must be created, it's work in progress.

lucasoares commented 8 months ago

@sunsingerus v2 must be created, it's work in progress.

Any change the #234 being merged? Anything we can do to help making this feature possible?

lucasoares commented 8 months ago

@sunsingerus in your PR having the following structs:

type Index struct {
    Key      string   `json:"key,omitempty"`
    Values   []string `json:"values,omitempty"`
    Value    string   `json:"value,omitempty"`
    Required string   `json:"required,omitempty"`
}

type Config struct {
    Index []*Index    `json:"index,omitempty"`
}

Should this code work? Because it is panicking when trying to merge two maps with the desired configs...

This only represents my use case, when I need to merge two maps containing arrays...

dst := &Config{
    Index: []*Index{
        {
            Key:      "type",
            Values:   []string{"value1", "value2"},
            Required: "true",
        },
        {
            Key:      "channel",
            Values:   []string{"value3", "value4"},
            Required: "true",
        },
    },
}

src := &Config{
    Index: []*Index{
        {
            Key: "*",
        },
        {
            Key: "*",
        },
    },
}

expected := &Config{
    Index: []*Index{
        {
            Key:      "*",
            Values:   []string{"value1", "value2"},
            Required: "true",
        },
        {
            Key:      "*",
            Values:   []string{"value3", "value4"},
            Required: "true",
        },
    },
}

// convert to map[string]any using json marshal + unmarshal
mapDst, _ := map.ToMap(dst)

// convert to map[string]any using json marshal + unmarshal
mapSrc, _ := map.ToMap(src)

err := mergo.Merge(mapDst, mapSrc, mergo.WithSliceDeepMerge, mergo.WithOverride)
if err != nil {
    t.Errorf("Error while merging %s", err)
}

// convert back to config using json marshal + unmarshal
resultDst, _ := map.FromMap(mapDst)

if !reflect.DeepEqual(resultDst, expected) {
    t.Errorf("expected: %#v\ngot: %#v", expected, dst)
}