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 | 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. |
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
}
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 {
…
}
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.
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
}
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
.
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) {
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 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
}
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
}
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
}
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
}
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
}
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.
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.
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
}
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()
}
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
}
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!