Git Product home page Git Product logo

structsconv's Introduction

Structs Conversion (structsconv)

coverage quality GoDoc

The structsconv package makes it easy to convert between structs of different types. This development is inspired by the java mapstruct library.

Motivation

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.

Release Compatibility

  • v1.0.0: is compatible with Golang versions:
    • 1.17+

Installation

  1. Install the package:
go get -u github.com/rendis/structsconv
  1. Import the package:
import "github.com/rendis/structsconv"

Quick start

Default Mapping

Fields with the same name and same type are mapped by default.

simple struct → simple struct

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 }


nested struct → nested struct

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.}}


slice → slice

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}]}


map → map

  • 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

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.

Rules for fields with different names

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)


Rules for constants

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}


Rules for ignoring fields

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:"" }


Rules with arguments

Root Struct & Current Struct

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 not Child1Dto.

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)


More and more arguments

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
}

structsconv's People

Contributors

rendis avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

Forkers

vernonr3

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.