Datamapper is a library written in Golang. It allows you to convert models to each other using tags. If you are tired of writing a lot of converters for structures, you will definitely like Datamapper :)
Let's start by installing Datamapper:
go install github.com/underbek/datamapper@latest
Datamapper provides the ability to customize model conversion using flags:
Usage:
datamapper [OPTIONS]
Application Options:
-c, --config= Yaml config path
-v, --version Current version
-d, --destination= Destination file path
--cf= User conversion functions sources/packages. Can add package alias like {package_path}:{alias)
--from= Model from name
--from-tag= Model from tag (default: map)
--from-source= From model source/package. Can add package alias like {package_path}:{alias) (default: .)
--to= Model to name
--to-tag= Model to tag (default: map)
--to-source= To model source/package. Can add package alias like {package_path}:{alias) (default: .)
-i, --inverse Create direct and inverse conversions
-s, --with-slice Create convertors with slice
-r, --recursive Parse recursive fields and create conversion if it not exists
-p, --with-pointers If field is pointer and recursive flag enabled then create convertors with pointers
Help Options:
-h, --help Show this help message
Datamapper can read configuration file by config
flag:
Config example:
# array of conversion functions
conversion-functions:
## source path or full package name
- source: github.com/underbek/datamapper/_test_data/mapper/convertors
## optional package alias
alias: cf
- source: github.com/underbek/datamapper/_test_data/mapper/other_convertors
# array of conversion mapping
options:
## From model
- from:
## name of model (can use with pointer)
name: "*User"
## mapping tag (optional|default = map)
tag : map
## source path or full package name
source: github.com/underbek/datamapper/_test_data/mapper/domain
## optional package alias
alias: domain
## From model like a from model
to:
name: "User"
source: github.com/underbek/datamapper/_test_data/mapper/transport
## Destination file path
destination: _test_data/local_test/domain_to_dto_user_converter.go
## If you need to crate inverse conversions
inverse: true
## Parse recursive fields and create conversion if it not exists (default = false)
recursive: false
## If field is pointer and recursive flag enabled then create convertors with pointers (default = false)
with-pointers: false
## Create convertors for slices (default = false)
with-slice: true
- from:
name: "User"
source: github.com/underbek/datamapper/_test_data/mapper/broken
alias: bk
tag: map
to:
name: "*User"
source: github.com/underbek/datamapper/_test_data/mapper/domain
alias: dm
tag: map
destination: _test_data/local_test/broken_to_domain_user_converter.go
inverse: true
with-slice: true
Datamapper already has converters for basic types. You can look into them here. In addition, you can use your own converters:
- by types
package conversion
import "fmt"
func ConvertIntToString(from int) string {
return fmt.Sprint(from)
}
- by generics
package conversion
import "fmt"
func ConvertAnyToString[T int | uint | float32](from T) string {
return fmt.Sprint(from)
}
func ConvertStringToMany[T int | uint | float32](from int) T {
return T(from)
}
func ConvertAnyToMany[T, V int | uint | float32](from T) V {
return V(from)
}
- with error
package converts
import "github.com/shopspring/decimal"
func ConvertStringToDecimal(from string) (decimal.Decimal, error) {
return decimal.NewFromString(from)
}
- Parse and filter tag
- Generate empty convertor
- Map similar types
- Simple convertor test
- Create conversion functions
- Use conversion functions in convertor
- Parse conversion functions from sources
- Parse generic types from other package (constrains.Float)
- Parse conversion functions with generic from
- Parse conversion functions with generic to
- Parse conversion functions with generic from and to
- Parse conversion functions with generic struct
- Parse conversion functions with struct
- Create base conversation source
- Generate convertors by other package models
- Generate convertors by other package fields in models
- Generate convertors by same package conversion functions
- Fix tests
- Delete package flag
- First mapper tests
- Use other conversion functions in convertor
- Use conversion functions with error -> convertor with error
- Convert with pointer field
- Convert with pointer field with error
- No nil err if from and to fields are pointers
- Add CI with tests and linters
- Parse other package
- First console generate
- Set default options
- Add generation info
- Fill readme
- First release
- Get first value by tag
- Move main to root
- Alias package name for (from/to) models and custom cf
- Use conversion functions with pointers
- Use some conversion functions sources
- Parse type alias
- Parse array, slice, map
- Generate convertors by slice fields
- New alias options (alias to each convertors' path)
- Converts both ways in one source
- Generate convertor with from/to pointer
- Fix error by parsing function as member
- Wrap errors
- Parse packages with broken sources
- Recursive convert by option if not found conversions
- Parse user struct in struct
- Use generated convertors in convertor like conversion function
- Generator must be return function model
- Use conversion functions from datamapper package without parsing
- Update readme
- Use one destination for models convertors by recursive flag
- Map field without tag
- Generate convertors with map fields
- Generate convertors with array fields
- Option for default field value if from field is nil
- Parse comments
- Parse embed struct
- Parse func aliases
- Warning or error politics if tags is not equals
- Fill some conversion functions
- Copy using conversion functions from datamapper to target service if flag set
- Parse custom error by conversion functions
- Fix cyclop linter
// Code generated by datamapper.
// https://github.com/underbek/datamapper
// Package local_test is a generated datamapper package.
package local_test
import (
"errors"
"fmt"
cf "github.com/underbek/datamapper/_test_data/mapper/convertors"
domain "github.com/underbek/datamapper/_test_data/mapper/domain"
"github.com/underbek/datamapper/_test_data/mapper/other_convertors"
"github.com/underbek/datamapper/_test_data/mapper/transport"
"github.com/underbek/datamapper/converts"
)
// ConvertDomainUserToTransportUser convert *domain.User by tag map to transport.User by tag map
func ConvertDomainUserToTransportUser(from *domain.User) (transport.User, error) {
if from == nil {
return transport.User{}, errors.New("User is nil")
}
var fromChildCount *string
if from.ChildCount != nil {
res := converts.ConvertNumericToString(*from.ChildCount)
fromChildCount = &res
}
return transport.User{
UUID: other_convertors.CustomIntegerToUUID(from.ID),
Name: from.Name,
Age: converts.ConvertDecimalToString(from.Age),
ChildCount: fromChildCount,
}, nil
}
// ConvertDomainUserSliceToTransportUserSlice convert []*domain.User to []transport.User
func ConvertDomainUserSliceToTransportUserSlice(fromSlice []*domain.User) ([]transport.User, error) {
if fromSlice == nil {
return nil, nil
}
toSlice := make([]transport.User, 0, len(fromSlice))
for _, from := range fromSlice {
to, err := ConvertDomainUserToTransportUser(from)
if err != nil {
return nil, fmt.Errorf("convert []*domain.User to []transport.User failed: %w", err)
}
toSlice = append(toSlice, to)
}
return toSlice, nil
}
// ConvertTransportUserToDomainUser convert transport.User by tag map to *domain.User by tag map
func ConvertTransportUserToDomainUser(from transport.User) (*domain.User, error) {
fromAge, err := converts.ConvertStringToDecimal(from.Age)
if err != nil {
return nil, fmt.Errorf("convert User.Age -> User.Age failed: %w", err)
}
var fromChildCount *int
if from.ChildCount != nil {
res, err := converts.ConvertStringToSigned[int](*from.ChildCount)
if err != nil {
return nil, fmt.Errorf("convert User.ChildCount -> User.ChildCount failed: %w", err)
}
fromChildCount = &res
}
return &domain.User{
ID: cf.CustomUUIDToInteger[int](from.UUID),
Name: from.Name,
Age: fromAge,
ChildCount: fromChildCount,
}, nil
}
// ConvertTransportUserSliceToDomainUserSlice convert []transport.User to []*domain.User
func ConvertTransportUserSliceToDomainUserSlice(fromSlice []transport.User) ([]*domain.User, error) {
if fromSlice == nil {
return nil, nil
}
toSlice := make([]*domain.User, 0, len(fromSlice))
for _, from := range fromSlice {
to, err := ConvertTransportUserToDomainUser(from)
if err != nil {
return nil, fmt.Errorf("convert []transport.User to []*domain.User failed: %w", err)
}
toSlice = append(toSlice, to)
}
return toSlice, nil
}