Skip to content

reedom/convergen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Convergen

Go Reference Go Report Card Coverage

Convergen is a code generator that creates functions for type-to-type copy. It generates functions that copy field to field between two types.

Notation Table

notation location summary
:match <name | none> interface, method Sets the field matcher algorithm (default: name).
:style <return | arg> interface, method Sets the style of the assignee variable input/output (default: return).
:recv <var> method Specifies the source value as a receiver of the generated function.
:reverse method Reverses the copy direction. Might be useful with receiver form.
:case interface, method Sets case-sensitive for name match (default).
:case:off interface, method Sets case-insensitive for name match.
:getter interface, method Includes getters for name match.
:getter:off interface, method Excludes getters for name match (default).
:stringer interface, method Calls String() if appropriate in name match.
:stringer:off interface, method Calls String() if appropriate in name match (default).
:typecast interface, method Allows type casting if appropriate in name match.
:typecast:off interface, method Suppresses type casting if appropriate in name match (default).
:skip <dst field pattern> method Marks the destination field to skip copying. Regex is allowed in /…/ syntax.
:map <src> <dst field> method the pair as assign source and destination.
:conv <func> <src> [to field] method Converts the source value by the converter and assigns its result to the destination.
:literal <dst> <literal> method Assigns the literal expression to the destination.
:preprocess <func> method Calls the function at the beginning of the convergen func.
:postprocess <func> method Calls the function at the end of the convergen function.

Sample

To use Convergen, write a generator code in the following convention:

//go:build convergen

package sample

import (
    "time"

    "github.com/sample/myapp/domain"
    "github.com/sample/myapp/storage"
)

//go:generate go run github.com/reedom/[email protected]
type Convergen interface {
    // :typecast
    // :stringer
    // :map Created.UnixMilli() Created
    DomainToStorage(*domain.User) *storage.User
}

Convergen generates code similar to the following:

// Code generated by github.com/reedom/convergen
// DO NOT EDIT.

package sample

import (
    "time"

    "github.com/sample/myapp/domain"
    "github.com/sample/myapp/storage"
)

func DomainToStorage(src *domain.User) (dst *storage.User) {
    dst = &storage.User{}
    dst.ID = int64(src.ID)
    dst.Name = src.Name
    dst.Status = src.Status.String()
    dst.Created = src.Created.UnixMilli()

    return
}

for these struct types:

package domain

import (
    "time"
)

type User struct {
    ID      int
    Name    string
    Status  Status
    Created time.Time
}

type Status string

func (s Status) String() string {
    return string(s)
}

outputs:

package storage

type User struct {
    ID      int64
    Name    string
    Status  string
    Created int64
}

Installation and Introduction

Use as a Go generator

To use Convergen as a Go generator, install the module in your Go project directory via go get:

$ go get -u github.com/reedom/convergen@latest

Then, write a generator as follows:

//go:generate go run github.com/reedom/[email protected]
type Convergen interface {
    …
}

Use as a CLI command

To use Convergen as a CLI command, install the command via go install:

$ go install github.com/reedom/convergen@latest

You can then generate code by calling:

$ convergen any-codegen-defined-code.go

The CLI help shows:

Usage: convergen [flags] <input path>

By default, the generated code is written to <input path>.gen.go

Flags:
  -dry
        Perform a dry run without writing files.
  -log
        Write log messages to <output path>.log.
  -out string
        Set the output file path.
  -print
        Print the resulting code to STDOUT as well.

Notations

:convergen

Use the :convergen notation to mark an interface as a converter definition.

By default, Convergen only looks for an interface named "Convergen" as a converter definition block. You can use the :convergen notation to enable Convergen to recognize other interface names as well. This is especially useful if you want to define methods with the same name but different receivers.

Available locations

interface

Format

":convergen"

Examples

// :convergen
type TransportConvergen interface {
    // :recv t
    ToDomain(*trans.Model) *domain.Model 
}

// :convergen
type PersistentConvergen interface {
    // :recv t
    ToDomain(*persistent.Model) *domain.Model 
}

:match <algorithm>

Use the :match notation to set the field matcher algorithm.

Default

:match name

Available locations

interface, method

Format

":match" <algorithm>

algorithm = "name" | none"

Examples

With name match, the generator matches fields or getter names (and their types) to generate the conversion code.

package model

type User struct {
    ID   int
    Name string
}
package web

type User struct {
    id   int
    name string
}

func (u *User) ID() int {
  return u.id
}
// :match name 
type Convergen interface {
    ToStorage(*User) *storage.User
}

Convergen generates:

func ToStorage(src *User) (dst *storage.User) {
    dst := &storage.User{}
    dst.ID = src.ID()
    dst.Name = src.name

    return
}

With none match, Convergen only processes fields or getters that have been explicitly specified using :map and :conv.

:style <style>

Use the :style notation to set the style of the assignee variable input/output.

Default

:style return

Available locations

interface, method

Format

":style" style

style = "arg" | "return"

Examples

Examples of return style:

Basic:

func ToStorage(src *domain.Pet) (dst *storage.Pet) {

With error:

func ToStorage(src *domain.Pet) (dst *storage.Pet, err error) {

With receiver:

func (src *domain.Pet) ToStorage() (dst *storage.Pet) {

Examples of arg style:

Basic:

func ToStorage(dst *storage.Pet, src *domain.Pet) {

With error:

func ToStorage(dst *storage.Pet, src *domain.Pet) (err error) {

With receiver:

func (src *domain.Pet) ToStorage(dst *storage.Pet) {

:recv <var>

Use the :recv notation to specify the source value as a receiver of the generated function.

According to the Go language specification, the receiver type must be defined in the same package as the generated code.

By convention, the <var> should be the same identifier as the methods of the type defines.

Default

No receiver is used.

Available locations

method

Format

":recv" var

var = variable-identifier 

Examples

In the following example, assume that domain.User is defined in another file under the same directory (package). It also assumes that other methods use u as their receiver variable name.

package domain

import (
    "github.com/sample/myapp/storage"
)

type Convergen interface {
    // :recv u
    ToStorage(*User) *storage.User  
}

The generated code will be:

package domain

import (
    "github.com/sample/myapp/storage"
)

type User struct {
    ID   int
    Name string
}

func (u *User) ToStorage() (dst *storage.User) {
    dst = &storage.User{}
    dst.ID = int64(u.ID)  
    dst.Name = u.Name

    return
}

:reverse

Reverse copy direction. Might be useful with receiver form.
To use :reverse, :style arg is required. (Otherwise it can't have any data source to copy from.)

Default

Copy in normal direction. In receiver form, receiver to a variable in argument.

Available locations

method

Format

":reverse"

Examples

package domain

import (
    "github.com/sample/myapp/storage"
)

type Convergen interface {
    // :style arg
    // :recv u
    // :reverse
    FromStorage(*User) *storage.User  
}

Will have:

package domain

import (
    "github.com/sample/myapp/storage"
)

type User struct {
    ID   int
    Name string
}

func (u *User) FromStorage(src *storage.User) {
    u.ID = int(src.User)  
    u.Name = src.Name
}

:case / :case:off

This notation controls case-sensitive or case-insensitive matches in field and method names.

It is applicable to :match name, :getter, and :skip notations.
Other notations like :map and :conv retain case-sensitive matches.

Default

":case"

Available locations

interface, method

Format

":case"
":case:off"

Examples

// interface level notation makes ":case:off" as default.
// :case:off
type Convergen interface {
    // Turn on case-sensitive match for names.
    // :case
    ToUserModel(*domain.User) storage.User

    // Adopt the default, case-insensitive match in this case.
    ToCategoryModel(*domain.Category) storage.Category
}

:getter / :getter:off

Include getters for name match.

Default

:getter:off

Available locations

interface, method

Format

":getter"
":getter:off"

Examples

With those models:

package domain

type User struct {
    name string
}

func (u *User) Name() string {
    return u.name
}
package storage

type User struct {
    Name string
}

The default Convergen behaviour can't find the private name and won't notice the getter.
So, with the following we'll get…

type Convergen interface {
    ToStorageUser(*domain.User) *storage.User
}
func ToStorageUser(src *domain.User) (dst *storage.User)
    dst = &storage.User{}
    // no match: dst.Name

    return
}

And with :getter we'll have…

type Convergen interface {
    // :getter
    ToStorageUser(*domain.User) *storage.User
}
func ToStorageUser(src *domain.User) (dst *storage.User)
    dst = &storage.User{}
    dst.Name = src.Name()

    return
}

Alternatively, you can get the same result with :map.
This is worth learning since :getter affects the entire method - :map allows you to get the result selectively.

type Convergen interface {
    // :map Name() Name
    ToStorageUser(*domain.User) *storage.User
}

:stringer / :stringer:off

When matching field names, call the String() method of a custom type if it exists.

By default, Convergen has no way of knowing how to assign a custom type to a string.
Using the :stringer notation will tell Convergen to look for a String() method on any custom types and use it when appropriate.

Default

:stringer:off

Available locations

interface, method

Format

":stringer"
":stringer:off"

Examples

Consider the following code:

package domain

type User struct {
    Status Status
}

type Status struct {
    status string
}

func (s Status) String() string {
    return string(s)
}

var (
    NotVerified = Status{"notVerified"}
    Verified    = Status{"verified"}
    Invalidated = Status{"invalidated"}
)
package storage

type User struct {
    String string
}

Without any additional notations, Convergen has no idea how to assign the Status type to a string. By adding :stringer notation to the Convergen interface, we're telling Convergen to look for a String() method on any custom types and use it when appropriate:

type Convergen interface {
    // :stringer
    ToStorageUser(*domain.User) *storage.User
}

Convergen will generate the following code:

func ToStorageUser(src *domain.User) (dst *storage.User)
    dst = &storage.User{}
    dst.Status = src.Status.String()

    return
}

Alternatively, you can achieve the same result with :map. However, :stringer affects the entire method, while :map allows you to specify the fields to map selectively:

type Convergen interface {
    // :map Status.String() Name
    ToStorageUser(*domain.User) *storage.User
}

:typecast

Allow type casting if appropriate in name match.

Default

:typecast:off

Available locations

interface, method

Format

":typecast"
":typecast:off"

Examples

With those models:

package domain

type User struct {
    ID     int
    Name   string
    Status Status
}

type Status string
package storage

type User struct {
    ID     int64  
    Name   string
    Status string
}

Convergen respects types strictly. It will give up copying fields if their types do not match. Note that Convergen relies on the types.AssignableTo(V, T Type) bool method from the standard packages. This means that the judgment is done by the type system of Go itself, not by a dumb string type name match.

Without :typecast turned on:

type Convergen interface {
    ToDomainUser(*storage.User) *domain.User
}

We'll get:

func ToDomainUser(src *storage.User) (dst *domain.User)
    dst = &domain.User{}
    // no match: dst.ID
    dst.Name = src.Name
    // no match: dst.Status

    return
}

With :typecast it turned on:

type Convergen interface {
	  // :typecast
    ToDomainUser(*storage.User) *domain.User
}
func ToDomainUser(src *storage.User) (dst *domain.User)
    dst = &domain.User{}
    dst.ID = int(src.ID)
    dst.Name = src.Name
    dst.Status = domain.Status(src.Status)

    return
}

:skip <dst field pattern>

Mark the destination field to skip copying.

A method can have multiple :skip lines that enable skipping multiple fields.
Other than field-path match, it accepts regular expression match. To specify, wrap the expression with /.
:case / :case:off affects :skip.

Available locations

method

Format

":skip" dst-field-pattern

dst-field-pattern  = field-path | regexp
field-path         = { identifier "." } identifier
regexp             = "/" regular-expression "/" 

Examples

Suppose we have the following domain and storage structs:

package domain

type User struct {
    ID      int
    Name    string
    Email   string
    Address Address
}

type Address struct {
    Street  string
    City    string
    ZipCode string
}

If we want to skip copying the Name field of the storage.User struct, we can use the :skip notation as follows:

type Convergen interface {
    // :skip Name
    ToStorage(*domain.User) *storage.User
}

If we want to skip copying multiple fields, we can use multiple :skip notations:

type Convergen interface {
    // :skip Name
    // :skip Email
    ToStorage(*domain.User) *storage.User
}

We can also use regular expressions to match multiple fields:

type Convergen interface {
    // :skip /^Name|Email$/
    ToStorage(*domain.User) *storage.User
}

This will result in the same generated code as the previous example.

:map <src> <dst field>

Specify a field mapping rule.

When to use:

  • copying a value between fields having different names.
  • assigning a method's result value to a destination field.

A method can have multiple :map lines that enable mapping multiple fields.

:case:off does not affect :map; <src> and <dst field> are compared in a case-sensitive manner.

Available locations

method

Format

":map" src dst-field

src                   = field-or-method-chain
dst-field             = field-path
field-path            = { identifier "." } identifier
field-or-getter-chain = { (identifier | getter) "." } (identifier | getter)
getter                = identifier "()"  

Examples

In the following example, two fields have the same meaning but different names.

package domain

type User struct {
    ID   int
    Name string
}
package storage

type User struct {
    UserID int
    Name   string
}

We can use :map to connect them:

type Convergen interface {
    // Map the "ID" field in domain.User to the "UserID" field in storage.User.
    // :map ID UserID
    ToStorage(*domain.User) *storage.User
}
func ToStorage(src *domain.User) (dst *storage.User) {
    dst = storage.User{}
    dst.UserID = src.ID
    dst.Name = src.Name
    
    return
}

In the following example, Status is a custom type with a method to retrieve its raw value.

package domain

type User struct {
    ID     int
    Name   string
    Status Status
}

type Status int

func (s Status) Int() int {
    return int(s)
}

var (
    NotVerified = Status(1)
    Verified    = Status(2)
    Invalidated = Status(3)
)
package storage

type User struct {
    UserID int
    Name   string
    Status int
}

We can use :map to apply the method's return value to assign:

type Convergen interface {
    // Map the "ID" field in domain.User to the "UserID" field in storage.User.
    // Map the result of the "Status.Int()" method in domain.User to the "Status" field in storage.User.
    // :map ID UserID
    // :map Status.Int() Status
    ToStorage(*domain.User) *storage.User
}
func ToStorage(src *domain.User) (dst *storage.User) {
    dst = storage.User{}
    dst.UserID = src.ID
    dst.Name = src.Name
    dst.Status = src.Status.Int()

    return
}

Note that the method's return value should be compatible with the destination field.
If they are not compatible, you can use :typecast or :stringer to help Convergen with the conversion.
Alternatively, you can use :conv notation to define a custom conversion function.

:conv <func> <src> [dst field]

Convert the source value by the converter and assign its result to the destination.

func must accept src value as the sole argument and return either
a) a single value that is compatible with the dst, or
a) a pair of variables as (dst, error).
For the latter case, the method definition should have error in return value(s).

You can omit dst field if the source and destination field paths are exactly the same.

:case:off does not take effect on :conv as <src> and <dst field> are compared in a case-sensitive manner.

Available locations

method

Format

":conv" func src [dst-field]

func                  = identifier
src                   = field-or-method-chain
dst-field             = field-path
field-path            = { identifier "." } identifier
field-or-getter-chain = { (identifier | getter) "." } (identifier | getter)
getter                = identifier "()"  

Examples

package domain

type User struct {
    ID    int
    Email string
}
package storage

type User struct {
    ID    int
    Email string
}

To store an encrypted Email field, we can use a converter function:

import (
    // The referenced library should have been imported anyhow.
    _ "github.com/sample/myapp/crypto"
)

type Convergen interface {
    // :conv crypto.Encrypt Email
    ToStorage(*domain.User) *storage.User
}

This results in:

import (
    "github.com/sample/myapp/crypto"
    _ "github.com/sample/myapp/crypto"
)

func ToStorage(src *domain.User) (dst *storage.User) {
    dst = storage.User{}
    dst.ID = src.ID
    dst.Email = crypto.Encrypt(src.Email)

    return
}

If you want to use a converter function that returns an error, you should add error to the return values of the converter method as well:

import (
    // The referenced library should have been imported anyhow.
    _ "github.com/sample/myapp/crypto"
)

type Convergen interface {
    // :conv crypto.Decrypt Email
    FromStorage(*storage.User) (*domain.User, error)
}

This results in:

import (
    "github.com/sample/myapp/crypto"
    _ "github.com/sample/myapp/crypto"
)

func ToStorage(src *storage.User) (dst *domain.User, err error) {
    dst = domain.User{}
    dst.ID = src.ID
    dst.Email, err = crypto.Decrypt(src.Email)
    if err != nil {
        return
    }

    return
}

:literal <dst> <literal>

Assign a literal expression to the destination field.

Available locations

method

Format

":literal"  dst literal

Examples

type Convergen interface {
    // :literal Created time.Now()
    FromStorage(*storage.User) *domain.User()
}

:preprocess <func> / :postprocess <func>

Call the function at the beginning(preprocess) or at the end(postprocess) of the convergen function.

Available locations

method

Format

":preprocess"  func
":postprocess" func

func  = identifier

Examples

type Convergen interface {
    // :preprocess prepareInput
    // :postprocess cleanUpOutput
    FromStorage(*storage.User) *domain.User
}

func prepareInput(src *storage.User) *storage.User {
    // modify the input source before conversion
    return src
}

func cleanUpOutput(dst *domain.User) *domain.User {
    // modify the output destination after conversion
    return dst
}

`` When FromStorage is called, the prepareInput function will be called with the input argument before the conversion takes place. Then the FromStorage method will be executed. Finally, the cleanUpOutput function will be called with the output result after the conversion has taken place.

type Convergen interface {
    // :preprocess prepareInput
    // :postprocess cleanUpOutput
    FromStorage(*storage.User) (*domain.User, error)
}

func prepareInput(src *storage.User) (*storage.User, error) {
    // modify the input source before conversion
    return src, nil
}

func cleanUpOutput(dst *domain.User) (*domain.User, error) {
    // modify the output destination after conversion
    return dst, nil
}

Contributing

For those who want to contribute, there are several ways to do it, including:

  • Reporting bugs or issues that you encounter while using Convergen.
  • Suggesting new features or improvements to the existing ones.
  • Implementing new features or fixing bugs by making a pull request to the project.
  • Improving the documentation or examples to make it easier for others to use Convergen.
  • Creating a project's logo to help with its branding.
  • Showing your support by giving the project a star.

By contributing to the project, you can help make it better and more useful for everyone. So, if you're interested, feel free to get involved!