anymapper

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 23, 2023 License: MIT Imports: 11 Imported by: 4

README

go-anymapper

The go-anymapper package is a fast and convenient tool for mapping data between different types, including basic Go types like strings and integers, as well as more complex data structures. It allows you to create custom mapping rules to fit the unique requirements of your application. This means you can use go-anymapper to easily convert data in the most useful way for your specific needs.

Installation

go get -u github.com/defiweb/go-anymapper

Usage

The simplest way to use the go-anymapper package is to use the Map function. It takes two arguments: the source and the destination. The function will try to map the source to the destination using the following rules:

  • If the dst value is an empty interface, the src value is assigned to it.
  • boolintX, uintX, floatXtrue1, false0 (if source is number, then ≠0true).
  • intX, uintX, floatXintX, uintX, floatX ⇒ cast numbers to the destination type.
  • intX, uintX, floatX[]byte ⇒ converts using binary.Read and binary.Write.
  • intX, uintX, floatX[X]byte ⇒ converts using binary.Read and binary.Write.
  • stringintX, uintX ⇒ converts using big.Int.SetString and big.Int.String.
  • stringfloatX ⇒ converts string to or from number using big.Float.SetString and big.Float.String.
  • string[]byte ⇒ converts using []byte(s) and string(b).
  • sliceslice ⇒ recursively map each slice element.
  • slicearray ⇒ recursively map each slice element if lengths are the same.
  • arrayarray ⇒ recursively map each array element if lengths are the same.
  • mapmap ⇒ recursively map every key and value pair.
  • structstruct ⇒ recursively map every struct field.
  • structmap[string]X ⇒ map struct fields to map elements using field names as keys and vice versa.

The above types refer to the type kind, not the actual type, hence type MyInt int is also considered as int.

In addition to the above rules, the default configuration of the mapper supports the following conversions:

  • time.Timestring ⇒ converts string to or from time using RFC3339 format.
  • time.Timeuint, uint32, uint64, int, int32, int64 ⇒ convert using Unix timestamp.
  • time.Timeuint8, uint16, int8, int16 ⇒ not allowed.
  • time.TimefloatX ⇒ convert to or from unix timestamp, preserving the fractional part of a second.
  • time.Timebig.Int ⇒ convert using Unix timestamp.
  • time.Timebig.Float ⇒ convert using Unix timestamp, preserving the fractional part of a second.
  • time.Timeother ⇒ try to convert using int64 as intermediate value.
  • big.IntintX, uintX, floatX ⇒ convert using big.Int.Int64 and big.Int.SetUint64.
  • big.Intstring ⇒ converts using big.Int.String and big.Int.SetString.
  • big.Int[]byte ⇒ converts using big.Int.Bytes and big.Int.SetBytes.
  • big.Intbig.Float ⇒ coverts using big.Float.Int and big.Float.SetInt.
  • big.FloatintX, uintX ⇒ convert using big.Float.Int64 and big.Float.SetUint64.
  • big.FloatfloatX ⇒ convert using big.Float.Float64 and big.Float.SetFloat64.
  • big.Floatstring ⇒ converts to or from string using big.Float.String and big.Float.SetString.
  • big.Ratstring ⇒ converts to or from string using big.Rat.String and big.Rat.SetString.
  • big.Ratbig.Float ⇒ converts using big.Float.SetRat and big.Float.Rat.
  • big.Ratslice, [2]array ⇒ convert first element to/from numerator and second to/form denominator.
  • big.Ratother ⇒ try to convert using big.Float as intermediate value.

Mapping will fail if the target type is not large enough to hold the source value. For example, mapping int64 to int8 may fail because int64 can store values larger than int8.

When mapping numbers from a byte slice or array, the length of the slice/array must be the same as the size of the variable in bytes. The size of int, uint is always considered as 64 bits.

The mapper will not overwrite the values in the destination if they do not have corresponding values in the source. For slices, if the destination slice is longer than the source slice, the extra elements will remain unchanged.

When using the mapper to convert values to interface types, it will attempt to use existing elements in the destination if possible. For example, mapping []int{1, 2} to []any{"", 0} will result in []any{"1", 2}, allowing to easily assign values to a specific implementation of an interface.

Mapping structures

Structures are treated by mapper as key-value maps. The mapper will try to map recursively every field of the source structure to the corresponding field of the destination structure or map.

Field names can be overridden with a tag (whose name is defined in Mapper.Tag, default is map).

As a special case, if the field tag is "-", the field is always omitted.

If the tag is not set, struct field names will be mapped using the Mapper.FieldNameMapper function.

Tags can be defined for both source and target structures. In this case, the names used in the tags must be the same for both structures.

If destination structure has fields that are not present in the source structure, the mapper will set zero values for those fields.

Strict types

If Context.StrictTypes is set to true, strict type checking will be enforced for the mapping process. This means that the source and destination types must be exactly the same for the mapping to be successful. However, mapping between different data structures, such as structstruct, structmap and mapmap is always allowed. If the destination type is an empty interface, the source value will be assigned to it regardless of the strict type check setting.

Additionally, the strict type check applies to custom types as well. For example, a custom type type MyInt int will not be treated as int anymore.

Custom mapping functions

If it is not possible to implement the above interfaces, custom mapping functions can be registered with the Mapper.Mapper map. The keys of this map are the types of the destination or source values, and the values are functions that return a MapFunc function that can map the source value to the destination value.

If the function returns a nil value, it means that the mapping is not possible. If both the source and destination types are registered, the source type will be used first. If it returns a nil value, the destination type will be used. If neither of them returns a nil value, the mapping will fail.

MapTo and MapFrom interfaces:

This feature is disabled by default. To enable it, set Mapper.Hooks to Mapper.MappingInterfaceHooks.

The go-anymapper package provides two interfaces that can be implemented by the source and destination types to customize the mapping process.

If the source value implements MapTo interface, the MapTo method will be used to map the source value to the destination value.

If the destination value implements MapFrom interface, the MapFrom method will be used to map the source value to the destination value.

If both source and destination values implement the MapTo and MapFrom interfaces then only MapTo will be used.

Default mapper instance

The package defines the default mapper instance Default that is used by Map and MapRefl functions. It is possible to change configuration of the default mapper, but it may affect other packages that use the default mapper. To avoid this, it is recommended to create a new instance of the mapper using the New method.

Examples

Mapping between simple types
package main

import (
	"fmt"

	"github.com/defiweb/go-anymapper"
)

func main() {
	var a int = 42
	var b string

	err := anymapper.Map(a, &b)
	if err != nil {
		panic(err)
	}

	fmt.Println(b) // "42"
}
Mapping between structure and map
package main

import (
	"fmt"

	"github.com/defiweb/go-anymapper"
)

type Data struct {
	Foo int `map:"bar"`
	Bar int `map:"foo"`
}

func main() {
	a := Data{Foo: 42, Bar: 1337}
	b := make(map[string]uint64)

	err := anymapper.Map(a, &b)
	if err != nil {
		panic(err)
	}

	fmt.Println(b) // map[bar:42 foo:1337]
}
MapFrom and MapTo interfaces
package main

import (
	"fmt"
	"math/big"

	"github.com/defiweb/go-anymapper"
)

type Val struct {
	X *big.Int
}

func (v *Val) MapFrom(m *anymapper.Mapper, x reflect.Value) error {
	return m.Map(x.Interface(), &v.X)
}

func (v *Val) MapTo(m *anymapper.Mapper, x reflect.Value) error {
	if v.X == nil {
		return m.Map(0, x.Addr().Interface())
	}
	return m.Map(v.X, x.Addr().Interface())
}

func main() {
	var a int = 42
	var b Val
	
	// Enable MapTo and MapFrom interfaces:
	anymapper.Default.Hooks = anymapper.MappingInterfaceHooks

	err := anymapper.Map(a, &b)
	if err != nil {
		panic(err)
	}

	fmt.Println(b.X.String()) // "42"
}
Custom mapping function
package main

import (
	"fmt"
	"reflect"
	"math/big"

	"github.com/defiweb/go-anymapper"
)

type Val struct {
	X *big.Int
}

func main() {
	var a int = 42
	var b Val

	typ := reflect.TypeOf(Val{})
	anymapper.Default.Mappers[typ] = func(m *anymapper.Mapper, src, dst reflect.Type) anymapper.MapFunc {
		if src == typ {
			return func(m *anymapper.Mapper, _ *anymapper.Context, src, dst reflect.Value) error {
				return m.MapRefl(src.FieldByName("X"), dst)
			}
		}
		if dst == typ {
			return func(m *anymapper.Mapper, _ *anymapper.Context, src, dst reflect.Value) error {
				return m.MapRefl(src, reflect.ValueOf(&dst.Addr().Interface().(*Val).X))
			}
		}
		return nil
	}

	err := anymapper.Map(a, &b)
	if err != nil {
		panic(err)
	}

	fmt.Println(b.X.String()) // "42"
}
Benchmark

Following benchmarks compare the performance of the go-anymapper package with the mapstructure package.

package main

import (
	"testing"

	"github.com/defiweb/go-anymapper"
	"github.com/mitchellh/mapstructure"
)

func Benchmark(b *testing.B) {
	type Object struct {
		A string
		B int
		C []string
		D []any
		E map[string]string
	}
	b.Run("anymapper/map-struct", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			input := map[string]interface{}{
				"A": "a",
				"B": 1,
				"C": []string{"a", "b", "c"},
				"D": []any{1, "2", 3.0},
				"E": map[string]string{"a": "a", "b": "b", "c": "c"},
			}
			var result Object
			err := anymapper.Map(input, &result)
			if err != nil {
				b.Fatal(err)
			}
		}
	})
	b.Run("anymapper/struct-map", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			input := Object{
				A: "a",
				B: 1,
				C: []string{"a", "b", "c"},
				D: []any{1, "2", 3.0},
				E: map[string]string{"a": "a", "b": "b", "c": "c"},
			}
			var result map[string]any
			err := anymapper.Map(input, &result)
			if err != nil {
				b.Fatal(err)
			}
		}
	})
	b.Run("mapstructure/map-struct", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			input := map[string]interface{}{
				"A": "a",
				"B": 1,
				"C": []string{"a", "b", "c"},
				"D": []any{1, "2", 3.0},
				"E": map[string]string{"a": "a", "b": "b", "c": "c"},
			}
			var result Object
			err := mapstructure.Decode(input, &result)
			if err != nil {
				b.Fatal(err)
			}
		}
	})
	b.Run("mapstructure/struct-map", func(b *testing.B) {
		for i := 0; i < b.N; i++ {
			input := Object{
				A: "a",
				B: 1,
				C: []string{"a", "b", "c"},
				D: []any{1, "2", 3.0},
				E: map[string]string{"a": "a", "b": "b", "c": "c"},
			}
			var result map[string]any
			err := mapstructure.Decode(input, &result)
			if err != nil {
				b.Fatal(err)
			}
		}
	})
}

Results:

BenchmarK/anymapper/map-struct         	  972992	      1174 ns/op
Benchmark/anymapper/struct-map         	  903348	      1311 ns/op
BenchmarK/mapstructure/map-struct      	  339668	      3501 ns/op
Benchmark/mapstructure/struct-map      	 1354458	      889.5 ns/op

Documentation

https://pkg.go.dev/github.com/defiweb/go-anymapper

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Default = New()

Default is the default Mapper used by the Map and MapRefl functions. It also provides additional mapping rules for time.Time, big.Int, big.Float and big.Rat. It can be modified to change the default behavior, but if the mapper is used by other packages, it is recommended to create a copy of the default mapper and modify the copy.

View Source
var InvalidDstErr = errors.New("mapper: invalid destination value")

InvalidDstErr is returned when reflect.IsValid returns false for the destination value. It may happen when the destination value was not passed as a pointer.

View Source
var InvalidSrcErr = errors.New("mapper: invalid source value")

InvalidSrcErr is returned when reflect.IsValid returns false for the source value.

View Source
var MappingInterfaceHooks = Hooks{
	MapFuncHook: func(m *Mapper, src, dst reflect.Type) MapFunc {
		if isSimpleType(src) && isSimpleType(dst) {
			return nil
		}
		if implMapTo(src) {
			return mapToInterface
		}
		if implMapFrom(dst) {
			return mapFromInterface
		}
		return nil
	},
	SourceValueHook: func(v reflect.Value) reflect.Value {
		for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr {
			if _, ok := v.Interface().(MapTo); ok {
				return v
			}
			v = v.Elem()
		}
		return reflect.Value{}
	},
	DestinationValueHook: func(v reflect.Value) reflect.Value {
		for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr {
			if v.Kind() == reflect.Ptr && v.IsNil() {
				if !v.CanSet() {
					return reflect.Value{}
				}
				v.Set(reflect.New(v.Type().Elem()))
			}
			if _, ok := v.Interface().(MapFrom); ok {
				return v
			}
			v = v.Elem()
		}
		return reflect.Value{}
	},
}

MappingInterfaceHooks is a set of hooks that checks if the source or destination type implements the MapTo or MapFrom interface. If so, it will use one of those interfaces to map the value. If both interfaces are implemented, MapTo will be used.

Functions

func Map

func Map(src, dst any) error

Map maps the source value to the destination value.

It is shorthand for Default.mapRefl(src, dst).

func MapContext

func MapContext(ctx *Context, src, dst any) error

MapContext maps the source value to the destination value.

It is shorthand for Default.MapContext(ctx, src, dst).

func MapRefl

func MapRefl(src, dst reflect.Value) error

MapRefl maps the source value to the destination value.

It is shorthand for Default.MapRefl(src, dst).

func MapReflContext

func MapReflContext(ctx *Context, src, dst reflect.Value) error

MapReflContext maps the source value to the destination value.

It is shorthand for Default.MapReflContext(ctx, src, dst).

Types

type Context

type Context struct {
	// StrictTypes enables strict type checking. If enabled, the source and
	// destination types must be exactly the same for the mapping to be
	// successful. However, mapping between different data structures, such as
	// `struct` ⇔ `struct`, `struct` ⇔ `map` and `map` ⇔ `map` is always
	// allowed. If the destination type is an empty interface, the source value
	// will be assigned to it regardless of the strict type check setting.
	StrictTypes bool

	// Tag is the name of the struct tag that is used by the mapper to
	// determine the name of the field to map to.
	Tag string

	// ByteOrder is the byte order used to map numbers to and from byte slices.
	ByteOrder binary.ByteOrder

	// DisableCache disables the cache of the type mappers.
	DisableCache bool

	// FieldMapper is a function that maps a struct field name to another name,
	// it is used only when the tag is not present.
	FieldMapper func(string) string

	// Custom is a custom value that can be used to pass additional information
	// to the mapping functions.
	Custom any
}

Context is a context that is passed to the mapping functions. It can be used to pass additional information to the mapping functions or to change the behavior of the mapper without modifying the global state or creating a copy of the mapper.

func (*Context) WithByteOrder

func (c *Context) WithByteOrder(byteOrder binary.ByteOrder) *Context

WithByteOrder returns a copy of the context with the ByteOrder field set to the given value.

func (*Context) WithCustom

func (c *Context) WithCustom(custom any) *Context

WithCustom returns a copy of the context with the Custom field set to the given value.

func (*Context) WithDisabledCache added in v0.3.0

func (c *Context) WithDisabledCache(disableCache bool) *Context

WithDisabledCache returns a copy of the context with the DisableCache field set to the given value.

func (*Context) WithFieldMapper

func (c *Context) WithFieldMapper(fieldMapper func(string) string) *Context

WithFieldMapper returns a copy of the context with the FieldMapper field set to the given value.

func (*Context) WithStrictTypes

func (c *Context) WithStrictTypes(strictTypes bool) *Context

WithStrictTypes returns a copy of the context with the StrictTypes field set to the given value.

func (*Context) WithTag

func (c *Context) WithTag(tag string) *Context

WithTag returns a copy of the context with the Tag field set to the given value.

type Hooks

type Hooks struct {
	// MapFuncHook allows to bypass the default mapping rules and use a custom
	// mapping function. If the hook returns nil, then the default mapping
	// rules are used.
	//
	// Returned MapFunc is cached.
	MapFuncHook MapFuncProvider

	// SourceValueHook returns a value that should be used as the source
	// value. It is called before the source value is used in the mapping.
	//
	// If the hook returns an invalid value, then the default function is used.
	//
	// By default, mapper unpacks pointers and dereferences interfaces. This
	// hook can be used to change this behavior.
	SourceValueHook func(reflect.Value) reflect.Value

	// DestinationValueHook returns a value that should be used as the destination
	// value. It is called before the destination value is used in the mapping.
	//
	// If the hook returns an invalid value, then the default function is used.
	//
	// By default, mapper unpacks pointers and dereferences interfaces. This
	// hook can be used to change this behavior.
	DestinationValueHook func(reflect.Value) reflect.Value
}

Hooks are functions that are called during the mapping process. They can modify the behavior of the mapper.

type InvalidMappingErr

type InvalidMappingErr struct {
	From, To reflect.Type
	Reason   string
}

func NewInvalidMappingError

func NewInvalidMappingError(from, to reflect.Type, reason string) *InvalidMappingErr

func NewStrictMappingError

func NewStrictMappingError(from, to reflect.Type) *InvalidMappingErr

func (*InvalidMappingErr) Error

func (e *InvalidMappingErr) Error() string

type MapFrom

type MapFrom interface {
	// MapFrom sets the receiver value from the source value.
	MapFrom(m *Mapper, src reflect.Value) error
}

MapFrom interface is implemented by types that can set their value from another type.

type MapFunc

type MapFunc func(m *Mapper, ctx *Context, src, dst reflect.Value) error

MapFunc is a function that maps a src value to a dst value. It returns an error if the mapping is not possible. The src and dst values are never pointers.

type MapFuncProvider

type MapFuncProvider func(m *Mapper, src, dst reflect.Type) MapFunc

MapFuncProvider is a function that returns a MapFunc for given src and dst types. If mapping is not supported, it returns nil.

type MapTo

type MapTo interface {
	// MapTo maps the receiver value to the destination value.
	MapTo(m *Mapper, dst reflect.Value) error
}

MapTo interface is implemented by types that can map themselves to another type.

type Mapper

type Mapper struct {
	// Context is the default context used by the mapper.
	Context *Context

	// Mappers is a map of custom mapper providers. The key is the type that
	// the mapper can map to and from. The value is a function that returns
	// a MapFunc that maps the source type to the destination type. Provider
	// can return nil if the mapping is not possible.
	//
	// If both source and destination types have defined providers, then
	// the provider for source value is used first, and if it returns nil,
	// then the provider for destination value is used.
	Mappers map[reflect.Type]MapFuncProvider

	// Hooks are functions that are called during the mapping process. They
	// can modify the behavior of the mapper. See Hooks for more information.
	Hooks Hooks
	// contains filtered or unexported fields
}

Mapper hold the mapper configuration.

func New

func New() *Mapper

New returns a new Mapper with default configuration.

func (*Mapper) Copy

func (m *Mapper) Copy() *Mapper

Copy creates a copy of the current Mapper with the same configuration.

func (*Mapper) Map

func (m *Mapper) Map(src, dst any) error

Map maps the source value to the destination value.

func (*Mapper) MapContext

func (m *Mapper) MapContext(ctx *Context, src, dst any) error

MapContext maps the source value to the destination value.

func (*Mapper) MapRefl

func (m *Mapper) MapRefl(src, dst reflect.Value) error

MapRefl maps the source value to the destination value.

func (*Mapper) MapReflContext

func (m *Mapper) MapReflContext(ctx *Context, src, dst reflect.Value) error

MapReflContext maps the source value to the destination value.

Jump to

Keyboard shortcuts

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