goalesce

package module
v0.0.0-...-132a388 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 3, 2024 License: Apache-2.0 Imports: 3 Imported by: 6

README

GoVersion GoDoc GoReport CodeCov

Package goalesce

Package goalesce is a library for copying and merging objects in Go. It can merge and copy any type of object, including structs, maps, arrays and slices, even nested ones.

Introduction

The main entry points are the DeepCopy and DeepMerge functions:

func DeepCopy  [T any](     o T, opts ...Option) (T, error)
func DeepMerge [T any](o1, o2 T, opts ...Option) (T, error)

DeepCopy, as the name implies, copies the given object and returns the copy. The copy is "deep" in the sense that it copies all the fields and elements of the object recursively.

DeepMerge merges the two values into a single value and returns that value. Again, the merge is "deep" and will merge all the fields and elements of the object recursively.

When called with no options, DeepMerge uses the following merge algorithm:

  • If both values are untyped nils, return nil.
  • If one value is untyped nil, return the other value.
  • If both values are zero-values for the type, return the type's zero-value.
  • If one value is a zero-value for the type, return the other value.
  • Otherwise, the values are merged using the following rules:
    • If both values are interfaces of same underlying types, merge the underlying values.
    • If both values are pointers, merge the values pointed to.
    • If both values are maps, merge the maps recursively, key by key.
    • If both values are structs, merge the structs recursively, field by field.
    • For other types (including slices), return the second value ("atomic" semantics).

Note that by default, slices and arrays are merged with atomic semantics, that is, the second slice or array overwrites the first one completely if it is non-zero. It is possible to change this behavior, see examples below.

Both DeepCopy and DeepMerge can be called with a list of options to modify its default merging and copying behavior. See the documentation of each option for details.

Using DeepCopy

Using DeepCopy is extremely simple:

Copying atomic values

Immutable values are always copied with atomic semantics, that is, the returned "copy" is actually the value itself. This is OK since only immutable types that are typically passed by value (int, string, etc.) are copied with this strategy.

v = "abc"
copied, _ = goalesce.DeepCopy(v)
fmt.Printf("DeepCopy(%+v) = %+v\n", v, copied)

Output:

DeepCopy(abc) = abc
Copying structs

The copied struct is a newly-allocated object; the struct fields are deep-copied:

type User struct {
    ID   int
    Name string
}
v = User{ID: 1, Name: "Alice"}
copied, _ = goalesce.DeepCopy(v)
fmt.Printf("DeepCopy(%+v) = %+v\n", v, copied)

Output:

DeepCopy({ID:1 Name:Alice}) = {ID:1 Name:Alice}

Only exported fields can be copied. Unexported fields are ignored.

Copying pointers

The copied pointer never points to the same memory address; the pointer target is deep-copied:

v = &User{ID: 1, Name: "Alice"}
copied, _ = goalesce.DeepCopy(v)
fmt.Printf("DeepCopy(%+v) = %+v, %p != %p\n", v, copied, v, copied)

Output:

DeepCopy(&{ID:1 Name:Alice}) = &{ID:1 Name:Alice}, 0xc00000e2a0 != 0xc00000e2d0
Copying maps

The copied map never points to the same memory address; the map entries are deep-copied:

v = map[int]string{1: "a", 2: "b"}
copied, _ = goalesce.DeepCopy(v)
fmt.Printf("DeepCopy(%+v) = %+v, %p != %p\n", v, copied, v, copied)

Output:

DeepCopy(map[1:a 2:b]) = map[1:a 2:b], 0xc000101470 != 0xc0001015c0
Copying slices

The copied slice never points to the same memory address; the slice elements are deep-copied:

v = []int{1, 2}
copied, _ = goalesce.DeepCopy(v)
fmt.Printf("DeepCopy(%+v) = %+v, %p != %p\n", v, copied, v, copied)

Output:

DeepCopy([1 2]) = [1 2], 0xc000018b90 != 0xc000018ba0
Custom copiers

The option WithTypeCopier can be used to delegate the copying of a given type to a custom function:

negatingCopier := func(v reflect.Value) (reflect.Value, error) {
    result := reflect.New(v.Type()).Elem()
    result.SetInt(-v.Int())
    return result, nil
}
v := 1
copied, err := goalesce.DeepCopy(v, goalesce.WithTypeCopier(reflect.TypeOf(v), negatingCopier))
fmt.Printf("DeepCopy(%+v, WithTypeCopier) = %+v, %v\n", v, copied, err)

Output:

DeepCopy(1, WithTypeCopier) = -1, <nil>

Using DeepMerge

Merging atomic values

Immutable values are always merged with atomic semantics: the "merged" value is actually the second value if it is non-zero, and the first value otherwise. This is OK since values of types like int, string, etc. are immutable.

v1 := "abc"
v2 := "def"
merged, _ := goalesce.DeepMerge(v1, v2)
fmt.Printf("DeepMerge(%v, %v) = %v\n", v1, v2, merged)

Output:

DeepMerge(abc, def) = def
Merging pointers

Pointers are merged by merging the values they point to (which could be nil):

stringPtr := func(s string) *string { return &s }
v1 := stringPtr("abc")
v2 := stringPtr("def")
merged, _ := goalesce.DeepMerge(v1, v2)
fmt.Printf("DeepMerge(%v, %v) = %v\n", *v1, *v2, *(merged.(*string)))

Output:

DeepMerge(abc, def) = def
Merging maps

When both maps are non-zero-values, the default behavior is to merge the two maps key by key, recursively merging the values.

v1 := map[int]string{1: "a", 2: "b"}
v2 := map[int]string{2: "c", 3: "d"}
merged, _ := goalesce.DeepMerge(v1, v2)
fmt.Printf("DeepMerge(%v, %v) = %v\n", v1, v2, merged)

Output:

DeepMerge(map[1:a 2:b], map[2:c 3:d]) = map[1:a 2:c 3:d]
Merging interfaces

When both interfaces are non-zero-values, the default behavior is to merge their runtime values recursively.

type Bird interface {
    Chirp()
}
type Duck struct {
    Name string
}
func (d *Duck) Chirp() {
    println("quack")
}
v1 := Bird(&Duck{Name: "Donald"})
v2 := Bird(&Duck{Name: "Scrooge"})
merged, _ = goalesce.DeepMerge(v1, v2)
fmt.Printf("DeepMerge(%+v, %+v) = %+v\n", v1, v2, merged)

Output:

DeepMerge(&{Name:Donald}, &{Name:Scrooge}) = &{Name:Scrooge}

If the two values have different runtime types, an error is returned.

Merging slices and arrays

When both slices or arrays are non-zero-values, the default behavior is to apply atomic semantics, that is, to replace the first slice or array with the second one:

v1 := []int{1, 2}
v2 := []int{2, 3}
merged, _ := goalesce.DeepMerge(v1, v2)
fmt.Printf("DeepMerge(%v, %v) = %v\n", v1, v2, merged)

Output:

DeepMerge([1 2], [2 3]) = [2 3]

This is indeed the safest choice when merging slices and arrays, but other merging strategies can be used (see below).

Treating empty slices as zero-values

An empty slice is not a zero-value for a slice. Therefore, when the second slice is an empty slice, an empty slice is returned:

v1 := []int{1, 2}
v2 := []int{} // empty slice
merged, _ := goalesce.DeepMerge(v1, v2)
fmt.Printf("DeepMerge(%v, %v) = %v\n", v1, v2, merged)

Output:

DeepMerge([1 2], []) = []

To consider empty slices as zero-values, use the WithZeroEmptySlice option. This changes the default behavior: when merging a non-empty slice with an empty slice, normally the empty slice is returned as in the example above; but with this option, the non-empty slice is returned.

v1 = []int{1, 2}
v2 = []int{} // empty slice will be considered zero-value
merged, _ = goalesce.DeepMerge(v1, v2, goalesce.WithZeroEmptySliceMerge())
fmt.Printf("DeepMerge(%+v, %+v, ZeroEmptySlice) = %+v\n", v1, v2, merged)

Output:

DeepMerge([1 2], [], ZeroEmptySlice) = [1 2]
Using "set-union" strategy

The "set-union" strategy can be used to merge the two slices together by creating a resulting slice that contains all elements from both slices, but no duplicates:

v1 := []int{1, 2}
v2 := []int{2, 3}
merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceSetUnionMerge())
fmt.Printf("DeepMerge(%v, %v, SetUnion) = %v\n", merged)

Output:

DeepMerge([1 2], [2 3], SetUnion) = [1 2 3]

When the slice elements are pointers, this strategy dereferences the pointers and compare their targets. If the resulting value is nil, the zero-value is used instead. This means that two nil pointers are considered equal, and equal to a non-nil pointer to the zero-value:

intPtr := func(i int) *int { return &i }
v1 := []*int{new(int), intPtr(0)} // new(int) and intPtr(0) are equal and point both to the zero-value (0)
v2 := []*int{nil, intPtr(1)}      // nil will be merged as the zero-value (0)
merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceSetUnionMerge())
for i, elem := range merged.([]*int) {
    fmt.Printf("%v: %T %v\n", i, elem, *elem)
}

Output:

0: *int 0
1: *int 1

This strategy is fine for slices of simple types and pointers thereof, but it is not recommended for slices of complex types as the elements may not be fully comparable. Also, it is not suitable for slices of double pointers.

The resulting slice's element order is deterministic: each element appears in the order it was first encountered when iterating over the two slices.

This strategy is available with two options:

  • WithDefaultSliceSetUnionMerge: applies this strategy to all slices;
  • WithSliceSetUnionMerge: applies this strategy to slices of a given type.

This strategy is not available for arrays.

Using "list-append" strategy

The "list-append" strategy appends the second slice to the first one (possibly resulting in duplicates):

v1 := []int{1, 2}
v2 := []int{2, 3}
merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceListAppendMerge())
fmt.Printf("DeepMerge(%v, %v, ListAppend) = %v\n", merged)

Output

DeepMerge([1 2], [2 3], ListAppend) = [1 2 2 3]

The resulting slice's element order is deterministic.

This strategy is available with two options:

  • WithDefaultSliceListAppendMerge: applies this strategy to all slices;
  • WithSliceListAppendMerge: applies this strategy to slices of a given type.

This strategy is not available for arrays.

Using "merge-by-index" strategy

The "merge-by-index" strategy can be used to merge two slices together using their indices as the merge key:

v1 := []int{1, 2, 3}
v2 := []int{-1, -2}
merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceMergeByIndex())
fmt.Printf("DeepMerge(%v, %v, MergeByIndex) = %v\n", v1, v2, merged)

Output:

DeepMerge([1 2 3], [-1 -2], MergeByIndex) = [-1 -2 3]

This strategy is available for slices with two options:

  • WithDefaultSliceMergeByIndex: applies this strategy to all slices;
  • WithSliceMergeByIndex: applies this strategy to slices of a given type.

This strategy is available for arrays with two options:

  • WithDefaultArrayMergeByIndex: applies this strategy to all arrays;
  • WithArrayMergeByIndex: applies this strategy to arrays of a given type.
Using "merge-by-key" strategy

The "merge-by-key" strategy can be used to merge two slices together using an arbitrary merge key:

type User struct {
    ID   int
    Name string
    Age  int
}
mergeKeyFunc := func(_ int, v reflect.Value) (reflect.Value, error) {
    return v.FieldByName("ID"), nil
}
v1 := []User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}}
v2 := []User{{ID: 2, Age: 30}, {ID: 1, Age: 20}}
merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithMergeByKeyFunc(reflect.TypeOf(User{}), mergeKeyFunc))
fmt.Printf("DeepMerge(%+v, %+v) = %+v\n", v1, v2, merged)

Output:

DeepMerge([{ID:1 Name:Alice Age:0} {ID:2 Name:Bob Age:0}], [{ID:2 Name: Age:30} {ID:1 Name: Age:20}]) = [{ID:1 Name:Alice Age:20} {ID:2 Name:Bob Age:30}]

This strategy is similar to Kubernetes' strategic merge patch.

The function mergeKeyFunc must be of type SliceMergeKeyFunc. It will be invoked with the index and value of the slice element to extract a merge key from.

The most common usage for this strategy is to merge slices of structs, where the merge key is the name of a primary key field. In this case, we can use the WithMergeByID option to specify the field name to use as merge key, and simplify the example above as follows:

v1 := []User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}}
v2 := []User{{ID: 1, Age: 20}      , {ID: 2, Age: 30}}
merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithMergeByID(reflect.TypeOf(User{}), "ID"))
fmt.Printf("DeepMerge(%+v, %+v, MergeByID) = %+v\n", v1, v2, merged)

Output:

DeepMerge([{ID:1 Name:Alice} {ID:2 Name:Bob}], [{ID:1 Age:20} {ID:2 Age:30}], MergeByID) = [{ID:1 Name:Alice Age:20} {ID:2 Name:Bob Age:30}]

The option WithMergeByID also works out-of-the-box on slices of pointers to structs:

v1 := []*User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}}
v2 := []*User{{ID: 2, Age: 30}, {ID: 1, Age: 20}}
merged, _ = goalesce.DeepMerge(v1, v2, goalesce.WithMergeByID(reflect.TypeOf(User{}), "ID"))
jsn, _ := json.MarshalIndent(merged, "", "  ")
fmt.Println(string(jsn))

Output:

[
  {
    "ID": 1,
    "Name": "Alice",
    "Age": 20
  },
  {
    "ID": 2,
    "Name": "Bob",
    "Age": 30
  }
]

This strategy is not available for arrays.

Merging structs

When both structs are non-zero-values, the default behavior is to merge the two structs field by field, recursively merging their values.

type User struct {
    ID   int
    Name string
    Age  int
}
v1 := User{ID: 1, Name: "Alice"}
v2 := User{ID: 1, Age: 20}
merged, _ := goalesce.DeepMerge(v1, v2)
fmt.Printf("DeepMerge(%+v, %+v) = %+v\n", v1, v2, merged)

Output:

DeepMerge({ID:1 Name:Alice}, {ID:1 Age:20}) = {ID:1 Name:Alice Age:20}

Only exported fields can be merged. Unexported fields are ignored.

Per-field merging strategies

When the default struct merging behavior is not desired or sufficient, per-field merging strategies can be used.

The struct tag goalesce allows to specify the following per-field strategies:

Strategy Valid on Effect
atomic Any field Applies "atomic" semantics.
union Slice fields Applies "set-union" semantics.
append Slice fields Applies "list-append" semantics.
index Slice fields Applies "merge-by-index" semantics.
id Slice of struct fields Applies "merge-by-id" semantics.

With the id strategy, a merge key must also be provided, separated by a colon from the strategy name itself, e.g. goalesce:"id:ID". The merge key must be the name of an exported field in the slice's struct element type.

Example:

type Actor struct {
    ID   int
    Name string
}
type Movie struct {
    Name        string
    Description string
    Actors      []Actor           `goalesce:"id:ID"`
    Tags        []string          `goalesce:"union"`
    Labels      map[string]string `goalesce:"atomic"`
}
v1 = Movie{
    Name:        "The Matrix",
    Description: "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.",
    Actors: []Actor{
        {ID: 1, Name: "Keanu Reeves"},
        {ID: 2, Name: "Laurence Fishburne"},
        {ID: 3, Name: "Carrie-Anne Moss"},
    },
    Tags: []string{"sci-fi", "action"},
    Labels: map[string]string{
        "producer": "Wachowski Brothers",
    },
}
v2 = Movie{
    Name: "The Matrix",
    Actors: []Actor{
        {ID: 2, Name: "Laurence Fishburne"},
        {ID: 3, Name: "Carrie-Anne Moss"},
        {ID: 4, Name: "Hugo Weaving"},
    },
    Tags: []string{"action", "fantasy"},
    Labels: map[string]string{
        "director": "Wachowski Brothers",
    },
}
merged, _ = goalesce.DeepMerge(v1, v2)
jsn, _ := json.MarshalIndent(merged, "", "  ")
fmt.Println(string(jsn))

Output:

{
  "Name": "The Matrix",
  "Description": "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.",
  "Actors": [
    {
      "ID": 1,
      "Name": "Keanu Reeves"
    },
    {
      "ID": 2,
      "Name": "Laurence Fishburne"
    },
    {
      "ID": 3,
      "Name": "Carrie-Anne Moss"
    },
    {
      "ID": 4,
      "Name": "Hugo Weaving"
    }
  ],
  "Tags": [
    "sci-fi",
    "action",
    "fantasy"
  ],
  "Labels": {
    "director": "Wachowski Brothers"
  }
}

If you cannot annotate your struct with a goalesce tag, you can use the following options to specify per-field strategies programmatically:

  • WithFieldListAppendMerge
  • WithFieldListUnionMerge
  • WithFieldMergeByIndex
  • WithFieldMergeByID
  • WithFieldMergeByKeyFunc

See the online documentation for more examples.

Custom mergers

The following options allow to pass a custom merger to the DeepMerge function:

  • The WithTypeMerger option can be used to merge a given type with a custom merger.
  • The WithFieldMerger option can be used to merge a given struct field with a custom merger.

Here is an example showcasing WithTypeMerger:

summingMerger := func(v1, v2 reflect.Value) (reflect.Value, error) {
    result := reflect.New(v1.Type()).Elem()
    result.SetInt(v1.Int() + v2.Int())
    return result, nil
}
v1 := 1
v2 := 2
merged, err := goalesce.DeepMerge(v1, v2, goalesce.WithTypeMerger(reflect.TypeOf(v1), summingMerger))
fmt.Printf("DeepMerge(%+v, %+v, WithTypeMerger) = %+v, %v\n", v1, v2, merged, err)

Output:

DeepMerge(1, 2, WithTypeMerger) = 3, <nil>

It gets a bit more involved when the custom merger needs to access the global merge function, for example, to delegate the merging of child values.

For these cases, there are 2 other options:

  • The WithTypeMergerProvider option can be used to merge a given type with a custom DeepMergeFunc.
  • The WithFieldMergerProvider option can be used to merge a given struct field with a custom DeepMergeFunc.

The above options give the custom merger access to the parent merger and the parent copier.

Here is an example showcasing WithFieldMergerProvider:

userMergerProvider := func(globalMerger goalesce.DeepMergeFunc, globalCopier goalesce.DeepCopyFunc) goalesce.DeepMergeFunc {
    return func(v1, v2 reflect.Value) (reflect.Value, error) {
        if v1.Int() == 1 {
            return reflect.Value{}, errors.New("user 1 has been deleted")
        }
        return globalMerger(v1, v2) // use global merger for other values
    }
}
v1 := User{ID: 1, Name: "Alice"}
v2 := User{ID: 1, Age: 20}
merged, err := goalesce.DeepMerge(v1, v2, goalesce.WithFieldMergerProvider(reflect.TypeOf(User{}), "ID", userMergerProvider))
fmt.Printf("DeepMerge(%+v, %+v, WithFieldMergerProvider) = %+v, %v\n", v1, v2, merged, err)

Output:

DeepMerge({ID:1 Name:Alice Age:0}, {ID:1 Name: Age:20}, WithFieldMergerProvider) = {ID:0 Name: Age:0}, user 1 has been deleted

Documentation

Overview

Package goalesce is a library for copying and merging objects in Go.

Read the project's README file for more information:

On pkg.go.dev: https://pkg.go.dev/github.com/adutra/goalesce#section-readme

On GitHub: https://github.com/adutra/goalesce#readme

Example
package main

import (
	"encoding/json"
	"fmt"
	"reflect"

	"github.com/adutra/goalesce"
)

type User struct {
	ID   int
	Name string
	Age  int
}

type Actor struct {
	ID   int
	Name string
}

type Movie struct {
	Name        string
	Description string
	Actors      []Actor           `goalesce:"id:ID"`
	Tags        []string          `goalesce:"union"`
	Labels      map[string]string `goalesce:"atomic"`
}

type Bird interface {
	Chirp()
}

type Duck struct {
	Name string
}

func (d *Duck) Chirp() {
	println("quack")
}

type Goose struct {
	Name string
}

func (d *Goose) Chirp() {
	println("honk")
}

func main() {

	var v, copied interface{}

	// Copying immutable values: the "copy" operation is a no-op
	v = "abc"
	copied, _ = goalesce.DeepCopy(v)
	fmt.Printf("DeepCopy(%+v) = %+v\n", v, copied)

	// Copying structs: fields are deep-copied
	v = User{ID: 1, Name: "Alice"}
	copied, _ = goalesce.DeepCopy(v)
	fmt.Printf("DeepCopy(%+v) = %+v\n", v, copied)

	// Copying pointers: the values pointed to are deep-copied, the pointers have different addresses
	v = &User{ID: 1, Name: "Alice"}
	copied, _ = goalesce.DeepCopy(v)
	fmt.Printf("DeepCopy(%+v) = %+v\n", v, copied)

	// Copying maps: keys and values are deep-copied, the maps point to different addresses
	v = map[int]string{1: "a", 2: "b"}
	copied, _ = goalesce.DeepCopy(v)
	fmt.Printf("DeepCopy(%+v) = %+v\n", v, copied)

	// Copying slices: elements are deep-copied, the slices point to different addresses
	v = []int{1, 2}
	copied, _ = goalesce.DeepCopy(v)
	fmt.Printf("DeepCopy(%+v) = %+v\n", v, copied)

	// Copying interfaces
	v = Bird(&Duck{Name: "Donald"})
	copied, _ = goalesce.DeepCopy(v)
	fmt.Printf("DeepCopy(%+v) = %+v\n", v, copied)

	var v1, v2, merged interface{}

	// Merging immutable values: the "merge" operation returns v2 if it is non-zero, otherwise v1
	v1 = "abc"
	v2 = "def"
	merged, _ = goalesce.DeepMerge(v1, v2)
	fmt.Printf("DeepMerge(%+v, %+v) = %+v\n", v1, v2, merged)

	v1 = 1
	v2 = 0 // 0 is the zero-value for ints: v1 will be returned
	merged, _ = goalesce.DeepMerge(v1, v2)
	fmt.Printf("DeepMerge(%+v, %+v) = %+v\n", v1, v2, merged)

	// Merging structs
	v1 = User{ID: 1, Name: "Alice"}
	v2 = User{ID: 1, Age: 20}
	merged, _ = goalesce.DeepMerge(v1, v2)
	fmt.Printf("DeepMerge(%+v, %+v) = %+v\n", v1, v2, merged)

	// Merging pointers
	v1 = &User{ID: 1, Name: "Alice"}
	v2 = &User{ID: 1, Age: 20}
	merged, _ = goalesce.DeepMerge(v1, v2)
	fmt.Printf("DeepMerge(%+v, %+v) = %+v\n", v1, v2, merged)

	// Merging maps
	v1 = map[int]string{1: "a", 2: "b"}
	v2 = map[int]string{2: "c", 3: "d"}
	merged, _ = goalesce.DeepMerge(v1, v2)
	fmt.Printf("DeepMerge(%+v, %+v) = %+v\n", v1, v2, merged)

	// Merging interfaces
	v1 = Bird(&Duck{Name: "Donald"})
	v2 = Bird(&Duck{Name: "Scrooge"})
	merged, _ = goalesce.DeepMerge(v1, v2)
	fmt.Printf("DeepMerge(%+v, %+v) = %+v\n", v1, v2, merged)

	// Merging interfaces: different implementations are not supported
	v1 = Bird(&Duck{Name: "Donald"})
	v2 = Bird(&Goose{Name: "Scrooge"})
	merged, err := goalesce.DeepMerge(v1, v2)
	fmt.Printf("DeepMerge(%+v, %+v) = %+v, %v\n", v1, v2, merged, err)

	// Merging slices with default atomic semantics
	v1 = []int{1, 2}
	v2 = []int{2, 3}
	merged, _ = goalesce.DeepMerge(v1, v2)
	fmt.Printf("DeepMerge(%+v, %+v) = %+v\n", v1, v2, merged)

	v1 = []int{1, 2}
	v2 = []int{} // empty slice is NOT a zero-value!
	merged, _ = goalesce.DeepMerge(v1, v2)
	fmt.Printf("DeepMerge(%+v, %+v) = %+v\n", v1, v2, merged)

	// Merging slices with empty slices treated as zero-value slices
	v1 = []int{1, 2}
	v2 = []int{} // empty slice will be considered zero-value
	merged, _ = goalesce.DeepMerge(v1, v2, goalesce.WithZeroEmptySliceMerge())
	fmt.Printf("DeepMerge(%+v, %+v, ZeroEmptySlice) = %+v\n", v1, v2, merged)

	// Merging slices with set-union semantics
	v1 = []int{1, 2}
	v2 = []int{2, 3}
	merged, _ = goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceSetUnionMerge())
	fmt.Printf("DeepMerge(%+v, %+v, SetUnion) = %+v\n", v1, v2, merged)

	// Merging slices with list-append semantics
	v1 = []int{1, 2}
	v2 = []int{2, 3}
	merged, _ = goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceListAppendMerge())
	fmt.Printf("DeepMerge(%+v, %+v, ListAppend) = %+v\n", v1, v2, merged)

	// Merging slices with merge-by-index semantics
	v1 = []int{1, 2, 3}
	v2 = []int{-1, -2}
	merged, _ = goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceMergeByIndex())
	fmt.Printf("DeepMerge(%+v, %+v, MergeByIndex) = %+v\n", v1, v2, merged)

	// Merging slices with merge-by-id semantics, merge key = field User.ID
	v1 = []User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}}
	v2 = []User{{ID: 2, Age: 30}, {ID: 1, Age: 20}}
	merged, _ = goalesce.DeepMerge(v1, v2, goalesce.WithSliceMergeByID(reflect.TypeOf([]User{}), "ID"))
	fmt.Printf("DeepMerge(%+v, %+v, MergeByID) = %+v\n", v1, v2, merged)

	// Merging structs with custom field strategies
	v1 = Movie{
		Name:        "The Matrix",
		Description: "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.",
		Actors: []Actor{
			{ID: 1, Name: "Keanu Reeves"},
			{ID: 2, Name: "Laurence Fishburne"},
			{ID: 3, Name: "Carrie-Anne Moss"},
		},
		Tags: []string{"sci-fi", "action"},
		Labels: map[string]string{
			"producer": "Wachowski Brothers",
		},
	}
	v2 = Movie{
		Name: "The Matrix",
		Actors: []Actor{
			{ID: 2, Name: "Laurence Fishburne"},
			{ID: 3, Name: "Carrie-Anne Moss"},
			{ID: 4, Name: "Hugo Weaving"},
		},
		Tags: []string{"action", "fantasy"},
		Labels: map[string]string{
			"director": "Wachowski Brothers",
		},
	}
	merged, _ = goalesce.DeepMerge(v1, v2)
	jsn, _ := json.MarshalIndent(merged, "", "  ")
	fmt.Printf("Merged movie:\n%+v\n", string(jsn))
}
Output:

DeepCopy(abc) = abc
DeepCopy({ID:1 Name:Alice Age:0}) = {ID:1 Name:Alice Age:0}
DeepCopy(&{ID:1 Name:Alice Age:0}) = &{ID:1 Name:Alice Age:0}
DeepCopy(map[1:a 2:b]) = map[1:a 2:b]
DeepCopy([1 2]) = [1 2]
DeepCopy(&{Name:Donald}) = &{Name:Donald}
DeepMerge(abc, def) = def
DeepMerge(1, 0) = 1
DeepMerge({ID:1 Name:Alice Age:0}, {ID:1 Name: Age:20}) = {ID:1 Name:Alice Age:20}
DeepMerge(&{ID:1 Name:Alice Age:0}, &{ID:1 Name: Age:20}) = &{ID:1 Name:Alice Age:20}
DeepMerge(map[1:a 2:b], map[2:c 3:d]) = map[1:a 2:c 3:d]
DeepMerge(&{Name:Donald}, &{Name:Scrooge}) = &{Name:Scrooge}
DeepMerge(&{Name:Donald}, &{Name:Scrooge}) = <nil>, types do not match: *goalesce_test.Duck != *goalesce_test.Goose
DeepMerge([1 2], [2 3]) = [2 3]
DeepMerge([1 2], []) = []
DeepMerge([1 2], [], ZeroEmptySlice) = [1 2]
DeepMerge([1 2], [2 3], SetUnion) = [1 2 3]
DeepMerge([1 2], [2 3], ListAppend) = [1 2 2 3]
DeepMerge([1 2 3], [-1 -2], MergeByIndex) = [-1 -2 3]
DeepMerge([{ID:1 Name:Alice Age:0} {ID:2 Name:Bob Age:0}], [{ID:2 Name: Age:30} {ID:1 Name: Age:20}], MergeByID) = [{ID:1 Name:Alice Age:20} {ID:2 Name:Bob Age:30}]
Merged movie:
{
  "Name": "The Matrix",
  "Description": "A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.",
  "Actors": [
    {
      "ID": 1,
      "Name": "Keanu Reeves"
    },
    {
      "ID": 2,
      "Name": "Laurence Fishburne"
    },
    {
      "ID": 3,
      "Name": "Carrie-Anne Moss"
    },
    {
      "ID": 4,
      "Name": "Hugo Weaving"
    }
  ],
  "Tags": [
    "sci-fi",
    "action",
    "fantasy"
  ],
  "Labels": {
    "director": "Wachowski Brothers"
  }
}

Index

Examples

Constants

View Source
const (
	// MergeStrategyAtomic applies "atomic" semantics.
	MergeStrategyAtomic = "atomic"
	// MergeStrategyAppend applies "list-append" semantics.
	MergeStrategyAppend = "append"
	// MergeStrategyUnion applies "set-union" semantics.
	MergeStrategyUnion = "union"
	// MergeStrategyIndex applies "merge-by-index" semantics.
	MergeStrategyIndex = "index"
	// MergeStrategyID applies "merge-by-id" semantics.
	MergeStrategyID = "id"
)
View Source
const MergeStrategyTag = "goalesce"

MergeStrategyTag is the struct tag used to specify the merge strategy to use for a struct field.

Variables

This section is empty.

Functions

func DeepCopy

func DeepCopy[T any](o T, opts ...Option) (T, error)

DeepCopy deep-copies the value and returns the copied value.

This function never modifies its inputs. It always returns an entirely newly-allocated value that shares no references with the inputs.

func DeepMerge

func DeepMerge[T any](o1, o2 T, opts ...Option) (T, error)

DeepMerge merges the 2 values and returns the merged value.

When called with no options, the function uses the following default algorithm:

  • If both values are nil, return nil.
  • If one value is nil, return the other value.
  • If both values are zero-values for the type, return the type's zero-value.
  • If one value is a zero-value for the type, return the other value.
  • Otherwise, the values are merged using the following rules:
  • If both values are interfaces of same underlying types, merge the underlying values.
  • If both values are pointers, merge the values pointed to.
  • If both values are maps, merge the maps recursively, key by key.
  • If both values are structs, merge the structs recursively, field by field.
  • For other types (including slices), return the second value ("atomic" semantics)

This function never modifies its inputs. It always returns an entirely newly-allocated value that shares no references with the inputs.

Note that by default, slices are merged with atomic semantics, that is, the second slice overwrites the first one completely. It is possible to change this behavior and use list-append, set-union, or merge-by semantics. See Option.

This function returns an error if the values are not of the same type, or if the merge encounters an error.

func MustDeepCopy

func MustDeepCopy[T any](o T, opts ...Option) T

MustDeepCopy is like DeepCopy, but panics if the copy returns an error.

func MustDeepMerge

func MustDeepMerge[T any](o1, o2 T, opts ...Option) T

MustDeepMerge is like DeepMerge, but panics if the merge returns an error.

Types

type DeepCopyFunc

type DeepCopyFunc func(v reflect.Value) (reflect.Value, error)

DeepCopyFunc is a function for copying objects. A deep copy function is expected to abide by the general contract of DeepCopy and to copy the given value to a newly-allocated value, avoiding retaining references to passed objects. Note that the passed values can be zero-values, but will never be invalid values. The returned value must be of same type as the passed value. By convention, when the function returns an invalid value and a nil error, it is assumed that the function is delegating the copy to the main copy function. See examples for more.

type DeepCopyFuncProvider

type DeepCopyFuncProvider func(global DeepCopyFunc) DeepCopyFunc

DeepCopyFuncProvider is a factory for DeepCopyFunc instances. It takes the main DeepCopyFunc instance as argument. It allows to create type copiers that are able to delegate the copy of nested values to that instance, instead of having to handle them internally. See examples for more.

type DeepMergeFunc

type DeepMergeFunc func(v1, v2 reflect.Value) (reflect.Value, error)

DeepMergeFunc is a function for merging objects. A deep merge function is expected to abide by the general contract of DeepMerge and to merge the 2 values into a single value, favoring v2 over v1 in case of conflicts. Note that the passed values can be zero-values, but will never be invalid values. The passed values are guaranteed to be of the same type; the returned value must also be of that same type. By convention, when the function returns an invalid value and a nil error, it is assumed that the function is delegating the merge to the main merge function. See examples for more.

type DeepMergeFuncProvider

type DeepMergeFuncProvider func(globalMerger DeepMergeFunc, globalCopier DeepCopyFunc) DeepMergeFunc

DeepMergeFuncProvider is a factory for DeepMergeFunc instances. It takes the main DeepMergeFunc and DeepCopyFunc instances as arguments. It allows to create type mergers that are able to delegate the merge and copy of nested values to those instances, instead of having to handle them internally. See examples for more.

type Option

type Option func(c *coalescer)

Option is an option that can be passed to DeepCopy or DeepMerge to customize the function behavior.

func WithArrayMergeByIndex

func WithArrayMergeByIndex(arrayType reflect.Type) Option

WithArrayMergeByIndex applies merge-by-index semantics to the given slice type. The given mergeKeyFunc will be used to extract the element merge key.

func WithAtomicCopy

func WithAtomicCopy(t reflect.Type) Option

WithAtomicCopy causes the given type to be copied with atomic semantics, instead of its default copy semantics. When a non-zero value of this type is copied, the value is returned as is.

func WithAtomicFieldMerge

func WithAtomicFieldMerge(structType reflect.Type, field string) Option

WithAtomicFieldMerge causes the given field to be merged atomically, that is, with "atomic" semantics, instead of its default merge semantics. When 2 non-zero-values of this field are merged, the second value is returned as is. This is the programmatic equivalent of adding a `goalesce:atomic` struct tag to that field.

func WithAtomicMerge

func WithAtomicMerge(t reflect.Type) Option

WithAtomicMerge causes the given type to be merged with atomic semantics, instead of its default merge semantics. When 2 non-zero-values of this type are merged, the second value is returned as is. Note that this option does not modify the copy behavior for the type; if atomic semantics are also needed when copying (which is usually the case), use both this option and WithAtomicCopy.

func WithDefaultArrayMergeByIndex

func WithDefaultArrayMergeByIndex() Option

WithDefaultArrayMergeByIndex applies merge-by-index semantics to all arrays to be merged.

func WithDefaultSliceListAppendMerge

func WithDefaultSliceListAppendMerge() Option

WithDefaultSliceListAppendMerge applies list-append merge semantics to all slices to be merged.

Example
package main

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/adutra/goalesce"
)

func main() {
	{
		v1 := []int{1, 2}
		v2 := []int{2, 3}
		merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceListAppendMerge())
		fmt.Printf("DeepMerge(%+v, %+v, ListAppend) = %+v\n", v1, v2, merged)
	}
	{
		// slice of pointers
		intPtr := func(i int) *int { return &i }
		v1 := []*int{new(int), intPtr(0)}
		v2 := []*int{(*int)(nil), intPtr(1)}
		merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceListAppendMerge())
		fmt.Printf("DeepMerge(%+v, %+v, ListAppend) = %+v\n", printPtrSlice(v1), printPtrSlice(v2), printPtrSlice(merged))
	}
}

func printPtrSlice(i interface{}) string {
	v := reflect.ValueOf(i)
	if v.IsNil() {
		return fmt.Sprintf("%T(nil)", i)
	}
	s := make([]string, v.Len())
	for i := 0; i < v.Len(); i++ {
		s[i] = printPtr(v.Index(i).Interface())
	}
	return fmt.Sprintf("[%v]", strings.Join(s, " "))
}

func printPtr(i interface{}) string {
	v := reflect.ValueOf(i)
	if v.IsNil() {
		return fmt.Sprintf("%T(nil)", i)
	}
	return fmt.Sprintf("&%+v", v.Elem().Interface())
}
Output:

DeepMerge([1 2], [2 3], ListAppend) = [1 2 2 3]
DeepMerge([&0 &0], [*int(nil) &1], ListAppend) = [&0 &0 *int(nil) &1]

func WithDefaultSliceMergeByIndex

func WithDefaultSliceMergeByIndex() Option

WithDefaultSliceMergeByIndex applies merge-by-index semantics to all slices to be merged.

Example
package main

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/adutra/goalesce"
)

func main() {
	{
		v1 := []int{1, 2, 3}
		v2 := []int{-1, -2}
		merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceMergeByIndex())
		fmt.Printf("DeepMerge(%+v, %+v, MergeByIndex) = %+v\n", v1, v2, merged)
	}
	{
		// slice of pointers
		intPtr := func(i int) *int { return &i }
		v1 := []*int{intPtr(1), intPtr(2), intPtr(3)}
		v2 := []*int{nil, intPtr(-2)}
		merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceMergeByIndex())
		fmt.Printf("DeepMerge(%+v, %+v, MergeByIndex) = %+v\n", printPtrSlice(v1), printPtrSlice(v2), printPtrSlice(merged))
	}
}

func printPtrSlice(i interface{}) string {
	v := reflect.ValueOf(i)
	if v.IsNil() {
		return fmt.Sprintf("%T(nil)", i)
	}
	s := make([]string, v.Len())
	for i := 0; i < v.Len(); i++ {
		s[i] = printPtr(v.Index(i).Interface())
	}
	return fmt.Sprintf("[%v]", strings.Join(s, " "))
}

func printPtr(i interface{}) string {
	v := reflect.ValueOf(i)
	if v.IsNil() {
		return fmt.Sprintf("%T(nil)", i)
	}
	return fmt.Sprintf("&%+v", v.Elem().Interface())
}
Output:

DeepMerge([1 2 3], [-1 -2], MergeByIndex) = [-1 -2 3]
DeepMerge([&1 &2 &3], [*int(nil) &-2], MergeByIndex) = [&1 &-2 &3]

func WithDefaultSliceSetUnionMerge

func WithDefaultSliceSetUnionMerge() Option

WithDefaultSliceSetUnionMerge applies set-union merge semantics to all slices to be merged. When the slice elements are pointers, this strategy dereferences the pointers and compare their targets. This strategy is fine for slices of simple types and pointers thereof, but it is not recommended for slices of complex types as the elements may not be fully comparable.

Example
package main

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/adutra/goalesce"
)

func main() {
	{
		v1 := []int{1, 2}
		v2 := []int{2, 3}
		merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceSetUnionMerge())
		fmt.Printf("DeepMerge(%+v, %+v, SetUnion) = %+v\n", v1, v2, merged)
	}
	{
		// slice of pointers
		intPtr := func(i int) *int { return &i }
		v1 := []*int{new(int), intPtr(0)} // new(int) and intPtr(0) are equal and point both to 0
		v2 := []*int{nil, intPtr(1)}      // nil will be merged as the zero-value (0)
		merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithDefaultSliceSetUnionMerge())
		fmt.Printf("DeepMerge(%+v, %+v, SetUnion) = %+v\n", printPtrSlice(v1), printPtrSlice(v2), printPtrSlice(merged))
	}
}

func printPtrSlice(i interface{}) string {
	v := reflect.ValueOf(i)
	if v.IsNil() {
		return fmt.Sprintf("%T(nil)", i)
	}
	s := make([]string, v.Len())
	for i := 0; i < v.Len(); i++ {
		s[i] = printPtr(v.Index(i).Interface())
	}
	return fmt.Sprintf("[%v]", strings.Join(s, " "))
}

func printPtr(i interface{}) string {
	v := reflect.ValueOf(i)
	if v.IsNil() {
		return fmt.Sprintf("%T(nil)", i)
	}
	return fmt.Sprintf("&%+v", v.Elem().Interface())
}
Output:

DeepMerge([1 2], [2 3], SetUnion) = [1 2 3]
DeepMerge([&0 &0], [*int(nil) &1], SetUnion) = [&0 &1]

func WithErrorOnCycle

func WithErrorOnCycle() Option

WithErrorOnCycle instructs the operation to return an error when a cycle is detected. By default, cycles are replaced with a nil pointer.

func WithFieldListAppendMerge

func WithFieldListAppendMerge(structType reflect.Type, field string) Option

WithFieldListAppendMerge merges the given struct field with list-append semantics. The field must be of slice type. This is the programmatic equivalent of adding a `goalesce:append` struct tag to that field.

func WithFieldMergeByID

func WithFieldMergeByID(structType reflect.Type, field string, key string) Option

WithFieldMergeByID merges the given struct field with merge-by-key semantics. The field must be of slice type. The slice element type must be of some other struct type, or a pointer thereto. The passed key must be a valid field name for that struct type and will be used to extract the slice element's merge key; therefore, that field should generally be a unique identifier or primary key for objects of this type. This is the programmatic equivalent of adding a `goalesce:id:key` struct tag to the struct field.

func WithFieldMergeByIndex

func WithFieldMergeByIndex(structType reflect.Type, field string) Option

WithFieldMergeByIndex merges the given struct field with merge-by-index semantics. The field must be of slice type. This is the programmatic equivalent of adding a `goalesce:index` struct tag to that field.

func WithFieldMergeByKeyFunc

func WithFieldMergeByKeyFunc(structType reflect.Type, field string, mergeKeyFunc SliceMergeKeyFunc) Option

WithFieldMergeByKeyFunc merges the given struct field with merge-by-key semantics. The field must be of slice type. The slice element type must be of some other struct type, or a pointer thereto. The given SliceMergeKeyFunc will be used to extract the slice element's merge key; therefore, the field should generally be a unique identifier or primary key for objects of this type.

func WithFieldMerger

func WithFieldMerger(structType reflect.Type, field string, merger DeepMergeFunc) Option

WithFieldMerger merges the given struct field with the given custom merger. This option does not allow the type merger to access the parent DeepMergeFunc instance being created. For that, use WithFieldMergerProvider instead.

func WithFieldMergerProvider

func WithFieldMergerProvider(structType reflect.Type, field string, provider DeepMergeFuncProvider) Option

WithFieldMergerProvider merges the given struct field with a custom merger that will be obtained by calling the given provider function with the global DeepMergeFunc and DeepCopyFunc instances. This option allows the type merger to access those instances in order to delegate the merge and copy of nested objects. See ExampleWithFieldMergerProvider.

Example
package main

import (
	"errors"
	"fmt"
	"reflect"

	"github.com/adutra/goalesce"
)

type User struct {
	ID   int
	Name string
	Age  int
}

func main() {
	userMergerProvider := func(mainMerger goalesce.DeepMergeFunc, mainCopier goalesce.DeepCopyFunc) goalesce.DeepMergeFunc {
		return func(v1, v2 reflect.Value) (reflect.Value, error) {
			if v1.Int() == 1 {
				return reflect.Value{}, errors.New("user 1 has been deleted")
			}
			return mainMerger(v1, v2) // delegate to main merger
		}
	}
	{
		v1 := User{ID: 1, Name: "Alice"}
		v2 := User{ID: 1, Age: 20}
		merged, err := goalesce.DeepMerge(v1, v2, goalesce.WithFieldMergerProvider(reflect.TypeOf(User{}), "ID", userMergerProvider))
		fmt.Printf("DeepMerge(%+v, %+v, WithFieldMergerProvider) = %+v, %v\n", v1, v2, merged, err)
	}
	{
		v1 := User{ID: 2, Name: "Bob"}
		v2 := User{ID: 2, Age: 30}
		merged, err := goalesce.DeepMerge(v1, v2, goalesce.WithFieldMergerProvider(reflect.TypeOf(User{}), "ID", userMergerProvider))
		fmt.Printf("DeepMerge(%+v, %+v, WithFieldMergerProvider) = %+v, %v\n", v1, v2, merged, err)
	}
}
Output:

DeepMerge({ID:1 Name:Alice Age:0}, {ID:1 Name: Age:20}, WithFieldMergerProvider) = {ID:0 Name: Age:0}, user 1 has been deleted
DeepMerge({ID:2 Name:Bob Age:0}, {ID:2 Name: Age:30}, WithFieldMergerProvider) = {ID:2 Name:Bob Age:30}, <nil>

func WithFieldSetUnionMerge

func WithFieldSetUnionMerge(structType reflect.Type, field string) Option

WithFieldSetUnionMerge merges the given struct field with set-union semantics. The field must be of slice type. This is the programmatic equivalent of adding a `goalesce:union` struct tag to that field.

func WithSliceListAppendMerge

func WithSliceListAppendMerge(sliceType reflect.Type) Option

WithSliceListAppendMerge applies list-append merge semantics to the given slice type.

func WithSliceMergeByID

func WithSliceMergeByID(sliceOfStructType reflect.Type, elemField string) Option

WithSliceMergeByID applies merge-by-key semantics to slices whose elements are of some struct type, or a pointer thereto. The passed field name will be used to extract the element's merge key; therefore, the field should generally be a unique identifier or primary key for objects of this type.

Example
package main

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/adutra/goalesce"
)

type User struct {
	ID   int
	Name string
	Age  int
}

func main() {
	{
		v1 := []User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}}
		v2 := []User{{ID: 2, Age: 30}, {ID: 1, Age: 20}}
		merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithSliceMergeByID(reflect.TypeOf([]User{}), "ID"))
		fmt.Printf("DeepMerge(%+v, %+v, MergeByID) = %+v\n", v1, v2, merged)
	}
	{
		// slice of pointers
		v1 := []*User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}}
		v2 := []*User{{ID: 2, Age: 30}, {ID: 1, Age: 20}}
		merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithSliceMergeByID(reflect.TypeOf([]*User{}), "ID"))
		fmt.Printf("DeepMerge(%+v, %+v, MergeByID) = %+v\n", printPtrSlice(v1), printPtrSlice(v2), printPtrSlice(merged))
	}
}

func printPtrSlice(i interface{}) string {
	v := reflect.ValueOf(i)
	if v.IsNil() {
		return fmt.Sprintf("%T(nil)", i)
	}
	s := make([]string, v.Len())
	for i := 0; i < v.Len(); i++ {
		s[i] = printPtr(v.Index(i).Interface())
	}
	return fmt.Sprintf("[%v]", strings.Join(s, " "))
}

func printPtr(i interface{}) string {
	v := reflect.ValueOf(i)
	if v.IsNil() {
		return fmt.Sprintf("%T(nil)", i)
	}
	return fmt.Sprintf("&%+v", v.Elem().Interface())
}
Output:

DeepMerge([{ID:1 Name:Alice Age:0} {ID:2 Name:Bob Age:0}], [{ID:2 Name: Age:30} {ID:1 Name: Age:20}], MergeByID) = [{ID:1 Name:Alice Age:20} {ID:2 Name:Bob Age:30}]
DeepMerge([&{ID:1 Name:Alice Age:0} &{ID:2 Name:Bob Age:0}], [&{ID:2 Name: Age:30} &{ID:1 Name: Age:20}], MergeByID) = [&{ID:1 Name:Alice Age:20} &{ID:2 Name:Bob Age:30}]

func WithSliceMergeByIndex

func WithSliceMergeByIndex(sliceType reflect.Type) Option

WithSliceMergeByIndex applies merge-by-index semantics to the given slice type. The given mergeKeyFunc will be used to extract the element merge key.

func WithSliceMergeByKeyFunc

func WithSliceMergeByKeyFunc(sliceType reflect.Type, mergeKeyFunc SliceMergeKeyFunc) Option

WithSliceMergeByKeyFunc applies merge-by-key semantics to the given slice type. The given SliceMergeKeyFunc will be used to extract the element merge key.

Example
package main

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/adutra/goalesce"
)

type User struct {
	ID   int
	Name string
	Age  int
}

func main() {
	{
		v1 := []User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}}
		v2 := []User{{ID: 2, Age: 30}, {ID: 1, Age: 20}}
		mergeKeyFunc := func(_ int, v reflect.Value) (reflect.Value, error) {
			return v.FieldByName("ID"), nil
		}
		merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithSliceMergeByKeyFunc(reflect.TypeOf([]User{}), mergeKeyFunc))
		fmt.Printf("DeepMerge(%+v, %+v, MergeByKeyFunc) = %+v\n", v1, v2, merged)
	}
	{
		v1 := []*User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}}
		v2 := []*User{{ID: 2, Age: 30}, {ID: 1, Age: 20}}
		mergeKeyFunc := func(_ int, v reflect.Value) (reflect.Value, error) {
			return v.Elem().FieldByName("ID"), nil
		}
		merged, _ := goalesce.DeepMerge(v1, v2, goalesce.WithSliceMergeByKeyFunc(reflect.TypeOf([]*User{}), mergeKeyFunc))
		fmt.Printf("DeepMerge(%+v, %+v, MergeByKeyFunc) = %+v\n", printPtrSlice(v1), printPtrSlice(v2), printPtrSlice(merged))
	}
}

func printPtrSlice(i interface{}) string {
	v := reflect.ValueOf(i)
	if v.IsNil() {
		return fmt.Sprintf("%T(nil)", i)
	}
	s := make([]string, v.Len())
	for i := 0; i < v.Len(); i++ {
		s[i] = printPtr(v.Index(i).Interface())
	}
	return fmt.Sprintf("[%v]", strings.Join(s, " "))
}

func printPtr(i interface{}) string {
	v := reflect.ValueOf(i)
	if v.IsNil() {
		return fmt.Sprintf("%T(nil)", i)
	}
	return fmt.Sprintf("&%+v", v.Elem().Interface())
}
Output:

DeepMerge([{ID:1 Name:Alice Age:0} {ID:2 Name:Bob Age:0}], [{ID:2 Name: Age:30} {ID:1 Name: Age:20}], MergeByKeyFunc) = [{ID:1 Name:Alice Age:20} {ID:2 Name:Bob Age:30}]
DeepMerge([&{ID:1 Name:Alice Age:0} &{ID:2 Name:Bob Age:0}], [&{ID:2 Name: Age:30} &{ID:1 Name: Age:20}], MergeByKeyFunc) = [&{ID:1 Name:Alice Age:20} &{ID:2 Name:Bob Age:30}]

func WithSliceSetUnionMerge

func WithSliceSetUnionMerge(sliceType reflect.Type) Option

WithSliceSetUnionMerge applies set-union merge semantics to the given slice type. When the slice elements are of a pointer type, this strategy dereferences the pointers and compare their targets. This strategy is fine for slices of simple types and pointers thereof, but it is not recommended for slices of complex types as the elements may not be fully comparable.

func WithTrileanMerge

func WithTrileanMerge() Option

WithTrileanMerge causes all boolean pointers to be merged using a three-valued logic, instead of their default merge semantics. When this is enabled, boolean pointers will behave as if they were "trileans", that is, a type with 3 possible values: nil (its zero-value), false and true (contrary to booleans, with trileans false is NOT a zero-value). The merge of trileans obeys the following rules:

v1    v2    merged
nil   nil   nil
nil   false false
nil   true  true
false nil   false
false false false
false true  true
true  nil   true
true  false false
true  true  true

The biggest difference with regular boolean pointers is that DeepMerge(&true, &false) will return &true for boolean pointers, while with trileans, it will return &false.

func WithTypeCopier

func WithTypeCopier(t reflect.Type, copier DeepCopyFunc) Option

WithTypeCopier will defer the copy of the given type to the given custom copier. This option does not allow the type copier to access the global DeepCopyFunc instance. For that, use WithTypeCopierProvider instead.

Example
package main

import (
	"fmt"
	"reflect"

	"github.com/adutra/goalesce"
)

func main() {
	negatingCopier := func(v reflect.Value) (reflect.Value, error) {
		if v.Int() < 0 {
			return reflect.Value{}, nil // delegate to main copier
		}
		result := reflect.New(v.Type()).Elem()
		result.SetInt(-v.Int())
		return result, nil
	}
	{
		v := 1
		copied, err := goalesce.DeepCopy(v, goalesce.WithTypeCopier(reflect.TypeOf(v), negatingCopier))
		fmt.Printf("DeepCopy(%+v, WithTypeCopier) = %+v, %v\n", v, copied, err)
	}
	{
		v := -1
		copied, err := goalesce.DeepCopy(v, goalesce.WithTypeCopier(reflect.TypeOf(v), negatingCopier))
		fmt.Printf("DeepCopy(%+v, WithTypeCopier) = %+v, %v\n", v, copied, err)
	}
}
Output:

DeepCopy(1, WithTypeCopier) = -1, <nil>
DeepCopy(-1, WithTypeCopier) = -1, <nil>

func WithTypeCopierProvider

func WithTypeCopierProvider(t reflect.Type, provider DeepCopyFuncProvider) Option

WithTypeCopierProvider will defer the copy of the given type to a custom copier that will be obtained by calling the given provider function with the global DeepCopyFunc instance. This option allows the type copier to access this instance in order to delegate the copy of nested objects. See ExampleWithTypeCopierProvider.

Example
package main

import (
	"errors"
	"fmt"
	"reflect"

	"github.com/adutra/goalesce"
)

type User struct {
	ID   int
	Name string
	Age  int
}

func main() {
	userCopierProvider := func(mainCopier goalesce.DeepCopyFunc) goalesce.DeepCopyFunc {
		return func(v reflect.Value) (reflect.Value, error) {
			if v.FieldByName("ID").Int() == 1 {
				return reflect.Value{}, errors.New("user 1 has been deleted")
			}
			result := reflect.New(v.Type()).Elem()
			id, err := mainCopier(v.FieldByName("ID"))
			if err != nil {
				return reflect.Value{}, err
			}
			result.FieldByName("ID").Set(id)
			name, err := mainCopier(v.FieldByName("Name"))
			if err != nil {
				return reflect.Value{}, err
			}
			result.FieldByName("Name").Set(name)
			return result, nil
		}
	}
	{
		v := User{ID: 1, Name: "Alice"}
		copied, err := goalesce.DeepCopy(v, goalesce.WithTypeCopierProvider(reflect.TypeOf(User{}), userCopierProvider))
		fmt.Printf("DeepCopy(%+v, WithTypeCopier) = %+v, %v\n", v, copied, err)
	}
	{
		v := User{ID: 2, Name: "Bob"}
		copied, err := goalesce.DeepCopy(v, goalesce.WithTypeCopierProvider(reflect.TypeOf(User{}), userCopierProvider))
		fmt.Printf("DeepCopy(%+v, WithTypeCopier) = %+v, %v\n", v, copied, err)
	}
}
Output:

DeepCopy({ID:1 Name:Alice Age:0}, WithTypeCopier) = {ID:0 Name: Age:0}, user 1 has been deleted
DeepCopy({ID:2 Name:Bob Age:0}, WithTypeCopier) = {ID:2 Name:Bob Age:0}, <nil>

func WithTypeMerger

func WithTypeMerger(t reflect.Type, merger DeepMergeFunc) Option

WithTypeMerger will defer the merge of the given type to the given custom merger. This option does not allow the type merger to access the global DeepMergeFunc instance. For that, use WithTypeMergerProvider instead.

Example
package main

import (
	"fmt"
	"reflect"

	"github.com/adutra/goalesce"
)

func main() {
	dividingMerger := func(v1, v2 reflect.Value) (reflect.Value, error) {
		if v2.Int() == 0 {
			return reflect.Value{}, nil // delegate to main merger
		}
		result := reflect.New(v1.Type()).Elem()
		result.SetInt(v1.Int() / v2.Int())
		return result, nil
	}
	{
		v1 := 6
		v2 := 2
		merged, err := goalesce.DeepMerge(v1, v2, goalesce.WithTypeMerger(reflect.TypeOf(v1), dividingMerger))
		fmt.Printf("DeepMerge(%+v, %+v, WithTypeMerger) = %+v, %v\n", v1, v2, merged, err)
	}
	{
		v1 := 1
		v2 := 0
		merged, err := goalesce.DeepMerge(v1, v2, goalesce.WithTypeMerger(reflect.TypeOf(v1), dividingMerger))
		fmt.Printf("DeepMerge(%+v, %+v, WithTypeMerger) = %+v, %v\n", v1, v2, merged, err)
	}
}
Output:

DeepMerge(6, 2, WithTypeMerger) = 3, <nil>
DeepMerge(1, 0, WithTypeMerger) = 1, <nil>

func WithTypeMergerProvider

func WithTypeMergerProvider(t reflect.Type, provider DeepMergeFuncProvider) Option

WithTypeMergerProvider will defer the merge of the given type to a custom merger that will be obtained by calling the given provider function with the global DeepMergeFunc and DeepCopyFunc instances. This option allows the type merger to access those instances in order to delegate the merge and copy of nested objects. See ExampleWithTypeMergerProvider.

Example
package main

import (
	"errors"
	"fmt"
	"reflect"

	"github.com/adutra/goalesce"
)

type User struct {
	ID   int
	Name string
	Age  int
}

func main() {
	userMergerProvider := func(mainMerger goalesce.DeepMergeFunc, mainCopier goalesce.DeepCopyFunc) goalesce.DeepMergeFunc {
		return func(v1, v2 reflect.Value) (reflect.Value, error) {
			if v1.FieldByName("ID").Int() == 1 {
				return reflect.Value{}, errors.New("user 1 has been deleted")
			}
			result := reflect.New(v1.Type()).Elem()
			id, err := mainCopier(v1.FieldByName("ID"))
			if err != nil {
				return reflect.Value{}, err
			}
			result.FieldByName("ID").Set(id)
			name, err := mainMerger(v1.FieldByName("Name"), v2.FieldByName("Name"))
			if err != nil {
				return reflect.Value{}, err
			}
			result.FieldByName("Name").Set(name)
			age, err := mainMerger(v1.FieldByName("Age"), v2.FieldByName("Age"))
			if err != nil {
				return reflect.Value{}, err
			}
			result.FieldByName("Age").Set(age)
			return result, nil
		}
	}
	{
		v1 := User{ID: 1, Name: "Alice"}
		v2 := User{ID: 1, Age: 20}
		merged, err := goalesce.DeepMerge(v1, v2, goalesce.WithTypeMergerProvider(reflect.TypeOf(User{}), userMergerProvider))
		fmt.Printf("DeepMerge(%+v, %+v, WithTypeMerger) = %+v, %v\n", v1, v2, merged, err)
	}
	{
		v1 := User{ID: 2, Name: "Bob"}
		v2 := User{ID: 2, Age: 30}
		merged, err := goalesce.DeepMerge(v1, v2, goalesce.WithTypeMergerProvider(reflect.TypeOf(User{}), userMergerProvider))
		fmt.Printf("DeepMerge(%+v, %+v, WithTypeMerger) = %+v, %v\n", v1, v2, merged, err)
	}
}
Output:

DeepMerge({ID:1 Name:Alice Age:0}, {ID:1 Name: Age:20}, WithTypeMerger) = {ID:0 Name: Age:0}, user 1 has been deleted
DeepMerge({ID:2 Name:Bob Age:0}, {ID:2 Name: Age:30}, WithTypeMerger) = {ID:2 Name:Bob Age:30}, <nil>

func WithZeroEmptySliceMerge

func WithZeroEmptySliceMerge() Option

WithZeroEmptySliceMerge instructs the merger to consider empty slices as zero (nil) slices. This changes the default behavior: when merging a non-empty slice with an empty slice, normally the empty slice is returned, but with this option, the non-empty slice is returned.

type SliceMergeKeyFunc

type SliceMergeKeyFunc func(index int, element reflect.Value) (key reflect.Value, err error)

SliceMergeKeyFunc is a function that extracts a merge key from a slice element's index and value. The passed element may be the zero-value for the slice element type, but it will never be an invalid value. The returned merge key can be a zero-value, but cannot be invalid; moreover, it must be comparable as it will be stored internally in a temporary map during the merge.

var SliceIndex SliceMergeKeyFunc = func(index int, element reflect.Value) (key reflect.Value, err error) {
	return reflect.ValueOf(index), nil
}

SliceIndex is a merge key func that returns the element indices as keys, thus achieving merge-by-index semantics. When using this func to do slice merges, the resulting slices will have their elements coalesced index by index.

var SliceUnion SliceMergeKeyFunc = func(index int, element reflect.Value) (key reflect.Value, err error) {
	return safeIndirect(element), nil
}

SliceUnion is a merge key func that returns the elements themselves as keys, thus achieving set-union semantics. If the elements are pointers, they are dereferenced, which means that the set-union semantics will apply to the pointer targets, not to the pointers themselves. When using this func to do slice merges, the resulting slices will have no duplicate items (that is, items having the same merge key).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL