The structsconv package makes it easy to convert between structs of different types. This development is inspired by the java mapstruct library.
The main purpose of this library is that, within a hexagonal architecture
, the conversion can be done without
the need to dirty the domain layer with tags
.
For more information on how to use, see the examples.
- v1.0.0: is compatible with Golang versions:
- 1.17+
- Install the package:
go get -u github.com/rendis/structsconv
- Import the package:
import "github.com/rendis/structsconv"
Fields with the same name
and same type
are mapped by default.
Structs
// Source struct
type SiteSource struct {
Name string
URL string
}
// Target struct
type SiteTarget struct {
Name string
URL string
}
Mapping
// Mapping struct
source := SiteSource{
Name: "Golang",
URL: "https://golang.org",
}
target := SiteTarget{}
structsconv.Map(&source, &target)
fmt.Printf("%+v\n", target) // Output: {Name:Golang URL:https://golang.org }
Structs
// Source struct
type SiteSource struct {
Name string
URL string
Meta MetaInfoSource
}
type MetaInfoSource struct {
Description string
}
// Target struct
type SiteTarget struct {
Name string
URL string
Meta MetaInfoTarget
}
type MetaInfoTarget struct {
Description string
}
Mapping
// Mapping struct
source := SiteSource{
Name: "Golang",
URL: "https://golang.org",
Meta: MetaInfoSource{
Description: "Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.",
},
}
target := SiteTarget{}
structsconv.Map(&source, &target)
fmt.Printf("%+v\n", target) // Output: {Name:Golang URL:https://golang.org Meta:{Description:Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.}}
Structs
// Source struct
type ProductDto struct {
Name string
Price int
Keywords []string
Locals []LocalDto
}
type LocalDto struct {
Id int
Stock int
}
// Target struct
type ProductDomain struct {
Name string
Price int
Keywords []string
Locals []LocalDomain
}
type LocalDomain struct {
Id int
Stock int
}
Mapping
// Mapping struct
source := ProductDto{
Name: "go shoes",
Price: 100,
Keywords: []string{"shoes", "sneakers"},
Locals: []LocalDto{
{Id: 1, Stock: 10},
{Id: 2, Stock: 20},
},
}
target := ProductDomain{}
structsconv.Map(&source, &target)
fmt.Printf("%+v\n", target) // Output: {Name:go shoes Price:100 Keywords:[shoes sneakers] Locals:[{Id:1 Stock:10} {Id:2 Stock:20}]}
- Source map key type must be equal to target map key type
- Source map value type must be equal to target map value type
Everything directly associated with the map type must be exported.
Structs
// Source struct
type SchoolDto struct {
Students map[string]StudentDto
}
type StudentDto struct {
Id string
Name string
Age int
}
// Target struct
type SchoolDomain struct {
Students map[string]StudentDomain
}
type StudentDomain struct {
Id string
Name string
Age int
}
Mapping
// Mapping struct
source := SchoolDto{
Students: map[string]StudentDto{
"stu124": {
Id: "stu124",
Name: "John",
Age: 20,
},
"stu125": {
Id: "stu125",
Name: "Jane",
Age: 21,
},
},
}
target := SchoolDomain{}
structsconv.Map(&source, &target)
fmt.Printf("%+v\n", target) // Output: {Students:{stu124:{Id:stu124 Name:John Age:20} stu125:{Id:stu125 Name:Jane Age:21}}}
Rules are used to customize the assignment. Use when desired:
- Map fields with different names or different types or both.
- Define a constant.
- Ignore a field.
- A custom mapping.
Fields with the different name
and same type
are mapped using rules
.
Structs
// Source struct
type SiteSource struct {
Name string
URL string
}
// Target struct
type SiteTarget struct {
SiteName string
URL string
}
Create the rules
var rules = structsconv.RulesSet{} // Create a new rules set
rules["SiteName"] = "Name" // Rule to map 'Name' to 'SiteName'
siteRulesDefinition := structsconv.RulesDefinition{
Rules: rules,
Source: SiteSource{}, // To get the source struct type
Target: SiteTarget{}, // To get the target struct type
}
The rules are created based on the names on the target struct.
Register the rules
structsconv.RegisterRulesDefinitions(siteRulesDefinition)
Mapping
source := SiteSource{
Name: "Golang",
URL: "https://golang.org",
}
target := SiteTarget{}
structsconv.Map(&source, &target)
fmt.Printf("%+v\n", target)
Structs
//Source struct
type ItemDto struct {
Id int
Name string
}
// Target struct
type ItemDomain struct {
Id int
ItemName string
ConstantValue string // Field does not exist in the source struct
}
Create the rules
var rules = structsconv.RulesSet{}
rules["ItemName"] = "Name"
rules["ConstantValue"] = func() string {
return "Constant value"
}
rulesDefinition := structsconv.RulesDefinition{
Rules: rules,
Source: ItemDto{},
Target: ItemDomain{},
}
Register the rules
structsconv.RegisterRulesDefinitions(rulesDefinition)
Mapping
source := ItemDto{
Id: 1,
Name: "Toy",
}
target := ItemDomain{}
structsconv.Map(&source, &target)
fmt.Printf("%+v\n", target) // Output: {Id:1 ItemName:Toy ConstantValue:Constant value}
To ignore a field, just set the rules
to nil
.
Structs
//Source struct
type ItemDto struct {
Id int
Name string
}
// Target struct
type ItemDomain struct {
Id int
ItemName string
IgnorableValue string // Field does not exist in the source struct
}
Create the rules
var rules = structsconv.RulesSet{}
rules["ItemName"] = "Name"
rules["IgnorableValue"] = nil
rulesDefinition := structsconv.RulesDefinition{
Rules: rules,
Source: ItemDto{},
Target: ItemDomain{},
}
Register the rules
structsconv.RegisterRulesDefinitions(rulesDefinition)
Mapping
source := ItemDto{
Id: 1,
Name: "Toy",
}
target := ItemDomain{}
structsconv.Map(&source, &target)
fmt.Printf("%+v\n", target) // Output: { Id:1 ItemName:Toy IgnorableValue:"" }
You will always be able to request the root origin struct
and the current origin struct
, to use them in custom mapping, you just need to request them.
Let see an example with 2 levels of nested structs:
// Source struct
type ParentRootDto struct {
ParentField string
Child1 Child1Dto
}
type Child1Dto struct {
Level1Field string
Child Child2Dto
}
type Child2Dto struct {
Level2Field string
}
// Target struct
type ParentRootDomain struct {
ParentField string // Mapped directly
Child1 Child1Domain // Mapped directly
}
type Child1Domain struct {
Level1Field string // Mapped directly
Child Child2Domain // Mapped directly
ParentField string // Custom mapping
Description string // Custom mapping
}
type Child2Domain struct {
Level2Field string // Mapped directly
ParentField string // Custom mapping
Level1Field string // Custom mapping
Description string // Custom mapping
}
Rules for Child1Domain
var child1Rules = structsconv.RulesSet{}
child1Rules["ParentField"] = func(parent ParentRootDto) string {
return parent.ParentField
}
child1Rules["Description"] = func(current Child1Dto, parent ParentRootDto) string {
return fmt.Sprintf("root parent = %s, current = %s", parent.ParentField, current.Level1Field)
}
child1Definition := structsconv.RulesDefinition{
Rules: child1Rules,
Source: Child1Dto{},
Target: Child1Domain{},
}
Rules for Child2Domain
var child2Rules = structsconv.RulesSet{}
// Note that parent is ParentRootDto and not Child1Dto
child2Rules["ParentField"] = func(parent ParentRootDto) string {
return parent.ParentField
}
// Note that parent is ParentRootDto and not Child1Dto
child2Rules["Level1Field"] = func(parent ParentRootDto) string {
return parent.Child1.Level1Field
}
// Note that parent is ParentRootDto and not Child1Dto
child2Rules["Description"] = func(parent ParentRootDto, current Child2Dto) string {
return fmt.Sprintf(
"root parent = %s, parent = %s, current = %s",
parent.ParentField, parent.Child1.Level1Field, current.Level2Field,
)
}
child2Definition := structsconv.RulesDefinition{
Rules: child2Rules,
Source: Child2Dto{},
Target: Child2Domain{},
}
Note that parent is
ParentRootDto
and notChild1Dto
.
Register the rules
structsconv.RegisterRulesDefinitions(child1Definition)
structsconv.RegisterRulesDefinitions(child2Definition)
Mapping
source := ParentRootDto{
ParentField: "ParentField (root)",
Child1: Child1Dto{
Level1Field: "level1 (Child of rut, parent of child2)",
Child: Child2Dto{
Level2Field: "level2 (last level, Child of Child1)",
},
},
}
target := ParentRootDomain{}
structsconv.Map(&source, &target) // To long to print but works :)
fmt.Printf("%+v\n", target)
You can also get more than just the root struct and the current struct as an argument.
By passing arguments to the map()
function, they will be available for request in all rules.
...
str1 = "str1"
str2 = "str2"
int1 = 1
int2 = 2
struct1 = struct{}{}
struct2 = struct{}{}
structsconv.Map(&source, &target, str1, str2, int1, int2, struct1, struct2)
...
To use them in the rules, you only need to request the ones you need.
- Request all the arguments (Ordered)
rules["targetField1"] = func(str1, str2 string, int1, int2 int, struct1 struct{}, struct2 struct{}) string {
return fmt.Sprintf("%s %s %d %d %+v %+v", str1, str2, int1, int2, struct1, struct2)
}
- Request all argument "Unordered" (1)
rules["targetField1"] = func(struct1 struct{}, struct2 struct{}, int1, int2 int, str1, str2 string) string {
return fmt.Sprintf("%s %s %d %d %+v %+v", str1, str2, int1, int2, struct1, struct2)
}
- Request all argument "Unordered" (2)
rules["targetField1"] = func(struct1 struct{}, int1 int, str1 string, struct2 struct{}, str2 string, int2 int) string {
return fmt.Sprintf("%s %s %d %d %+v %+v", str1, str2, int1, int2, struct1, struct2)
}
Notice the double quotes around the word "Unordered", this is because arguments can be requested out of order relative to their type, arguments of the same type must be requested in the same order as they were passed.
Let's see some examples:
str1 = "str1"
str2 = "str2"
structsconv.Map(&source, &target, str1, str2)
rules["targetField1"] = func(struct1 struct{}, arg1 string, arg2 string) string {
return fmt.Sprintf("%s %s", str1, str2)
}
rules["targetField1"] = func(struct1 struct{}, arg2 string, arg1 string) string {
return fmt.Sprintf("%s %s", str1, str2)
}
In both cases, the first argument is str1
and the second is str2
.
In summary, the rules are:
- Order does not matter on different types.
- Order matters on the same types.
Another way to use argument passing is through slices
.
str1 = "hello"
str2 = "world"
int1 = 1
int2 = 2
args := []interface{}{str1, str2, int1, int2}
structsconv.Map(&source, &target, args)
rules["targetField1"] = func(args []interfaces{}) string {
return fmt.Sprintf("first args = %s, last args = %d", args[0], str2[2]) // first args = hello, last args = 2
}