sesame

package module
v0.0.3-devel Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 11, 2024 License: MIT Imports: 21 Imported by: 0

README

sesame

An object-to-object mapper generator for Go that can 'scale'

Why the name 'sesame'?

sesame is a go mapper. Japanese often abbreviate this kind of terms as 'goma'. Goma means the sesame in Japanese.

Motivation

Multitier architectures like good-old 3tier architecture, Clean architecture, Hexagonal architecture etc have similar objects in each layers.

It is a hard work that you must write so many bolierplate code to map these objects. There are some kind of libraries(an object-to-object mapper) that simplify this job using reflections.

Object-to-object mappers that use reflections are very easy to use, but these are difficult to 'scale' .

  • Hard to debug: Objects in the real world, rather than examples, are often very large. In reflection-based libraries, it can be quite hard to debug if some fields are not mapped correctly.
  • Performance: Go's reflection is fast enough for most usecases. But, yes, large applications that selects multitier architectures often have very large objects. Many a little makes a mickle.

sesame generates object-to-object mappers source codes that DO NOT use reflections.

Status

This project is in very early stage. Any kind of feedbacks are wellcome.

Features

  • Fast : sesame generates object-to-object mappers source codes that DO NOT use reflections.
  • Easy to debug : If some fields are not mapped correctly, you just look a generated mapper source codes.
  • Flexible : sesame provides various way to map objects.
    • By name
      • Simple field to field mapping
      • Field to nesting field mapping like TodoModel.UserID -> TodoEntity.User.ID .
      • Embedded struct mapping
    • By type
    • By helper function that is written in Go
  • Zero 3rd-party dependencies at runtime : sesame generates codes that depend only standard libraries.
  • Scalable :
    • Fast, Easy to debug and flexible.
    • Mapping configurations can be separated into multiple files.
      • You do not have to edit over 10000 lines of a single YAML file that has 1000 mapping definitions.

Current limitations

  • Your project must be a Go module.

Installation

Binary installation

Get a binary from releases .

go get

sesame requires Go 1.20+.

$ go install github.com/yuin/sesame/cmd/sesame@latest

Usage

Outline
  1. Create a configuration file(s).
  2. Run the sesame command.
  3. Create a Mappers object in your code.
  4. (Optional) Add helpers and custom mappers.
  5. Get a mapper from Mappers.
  6. Map objects by the mapper.

See tests for examples.

Mapping configuration file

sesame uses mapping configuration files written in YAML.

${YOUR_GO_MODULE_ROOT}/sesame.yml:

mappers:                                         # configurations for a mapper collection
  package: mapper                                # package name for generated mapper collection
  destination: ./mapper/mappers_gen.go           # destination for generation
  nil-map: nil                                   # how are nil collections mapped
  nil-slice: nil                                 #   a value should be one of 'nil', 'empty' (default: nil)
mappings:                                        # configurations for object-to-object mappings
  - name: TodoMapper                             # name of the mapper. This must be unique within all mappers
    package: mapper                              # package name for generated mapper
    destination: ./mapper/todo_mapper_gen.go     # definition for generation
    bidirectional: true                          # generates a-to-b and b-to-a mapping if true(default: false)
    a-to-b: ModelToEntity                        # mapping function name(default: `{AName}To{BName}`)
    b-to-a: EntityToModel                        # mapping function name(default: `{BName}To{AName}`)
    a:                                           # mapping operand A
      package: ./model                           # package path for this operand
      name: TodoModel                            # struct name of this operand
    b:                                           # mapping operand B
      package: ./domain
      name: Todo
    explicit-only: false                         # sesame maps same names automatically if false(default: false)
    allow-unmapped:false                         # sesame fails with unmapped fields if false(default: false)
                                                 #   This value is ignored if `explicit-only' is set true.
    ignore-case:   false                         # sesame ignores field name cases if true(default: false)
    nil-map: nil                                 # how nil collections are mapped
    nil-slice: nil                               #   a default value is inherited from mappers
    fields:                                      # relationships between A fields and B fields
      - a: Done                                  #   you can define nested mappings like UserID
        b: Finished                              #   you can define mappings for embedded structs by '*'
      - a: UserID                                # 
        b: User.ID                               #
    ignores:                                     # ignores fields in operand X
      - a: ValidateOnly
      - b: User
_includes:                                       # includes separated configuration files
  - ./*/**/*_sesame.yml

And now, you can generate source codes just run sesame command in ${YOUR_GO_MODULE_ROOT}.

This configuration will generate the codes like the following:

./mapper/todo_mapper_gen.go :

package mapper

import (
    pkg00002 "time"

    pkg00001 "example.com/testmod/domain"
    pkg00000 "example.com/testmod/model"
)

type TodoMapperHelper interface {
    ModelToEntity(*pkg00000.TodoModel, *pkg00001.Todo) error
    EntityToModel(*pkg00001.Todo, *pkg00000.TodoModel) error
}

type TodoMapper interface {
    ModelToEntity(*pkg00000.TodoModel) (*pkg00001.Todo, error)
    EntityToModel(*pkg00001.Todo) (*pkg00000.TodoModel, error)
}

// ... (TodoMapper default implementation)
Mapping in your code

sesame generates a mapper collection into the mappers.destination . Mapping codes look like the following:

  1. Create new Mappers object as a singleton object. The Mappers object is a groutine-safe.

    mappers := mapper.NewMappers()           // Creates new Mappers object
    mapper.AddTimeToStringMapper(mappers)    // Add custom mappers
    mappers.Add("TodoMapperHelper", &todoMapperHelper{}) // Add helpers
    ```
    
    
  2. Get a mapper and call it for mapping.

    obj, err := mappers.Get("TodoMapper")    // Get mapper by its name
    if err != nil {
        t.Fatal(err)
    }
    todoMapper, _ := obj.(TodoMapper)
    entity, err := todoMapper.ModelToEntity(model) 
    
Custom mappers

By default, sesame can map following types:

  • Same types
  • Castable types(i.e. int -> int64, type MyType int <-> int)
  • map, slice and array

For others, you can write and register custom mappers.

Example: string <-> time.Time mapper

type TimeStringMapper struct {
}

func (m *TimeStringMapper) StringToTime(source string) (*time.Time, error) {
    t, err := time.Parse(time.RFC3339, source)
    if err != nil {
        return nil, err
    }
    return &t, nil
}

func (m *TimeStringMapper) TimeToString(source *time.Time) (string, error) {
    return source.Format(time.RFC3339), nil
}

type Mappers interface {
    AddFactory(string, func(MapperGetter) (any, error))
    AddMapperFuncFactory(string, string, func(MapperGetter) (any, error))
}

func AddTimeToStringMapper(mappers Mappers) {
    mappers.AddFactory("TimeStringMapper", func(m MapperGetter) (any, error) {
        return &TimeStringMapper{}, nil
    })
    mappers.AddMapperFuncFactory("string", "time#Time", func(m MapperGetter) (any, error) {
        obj, _ := m.Get("TimeStringMapper")
        stringTime := obj.(*TimeStringMapper)
        return stringTime.StringToTime, nil
    })
    mappers.AddMapperFuncFactory("time#Time", "string", func(m MapperGetter) (any, error) {
        obj, _ := m.Get("TimeStringMapper")
        stringTime := obj.(*TimeStringMapper)
        return stringTime.TimeToString, nil
    })
}

or if a mapper does not require other mappers, you can do it just

func AddTimeToStringMapper(mappers Mappers) {
    mappers.Add("TimeStringMapper", &TimeStringMapper{})
}

Mappers.AddMapperFuncFactory takes qualified type names as arguments. A qualified type name is FULL_PACKAGE_PATH#TYPENAME(i.e. time#Time, example.com/testmod/domain#Todo).

Argument types and return types in custom mapping functions must be a

  • Raw value: primitive types(i.e. string, int, slice ...)
  • Pointer: others

So func (m *TimeStringMapper) TimeToString(source *time.Time) (string, error) defines source type as a pointer(*time.Time) and return type as a raw value(string) .

Mappers.Add finds given mapper methods name like 'XxxToYyy' and calls AddMapperFuncFactory.

Helpers

You can define helper functions for more complex mappings.

type todoMapperHelper struct {
}

var _ TodoMapperHelper = &todoMapperHelper{} // TodoMapperHelper interface is generated by sesame

func (h *todoMapperHelper) ModelToEntity(source *model.TodoModel, dest *domain.Todo) error {
    if source.ValidateOnly {
        dest.Attributes["ValidateOnly"] = []string{"true"}
    }
    return nil
}

func (h *todoMapperHelper) EntityToModel(source *domain.Todo, dest *model.TodoModel) error {
    if _, ok := source.Attributes["ValidateOnly"]; ok {
        dest.ValidateOnly = true
    }
    return nil
}

and register it as {MAPPER_NAME}Helper:

mappers.Add("TodoMapperHelper", &todoMapperHelper{})

or

mappers.AddFactory("TodoMapperHelper", func(ms MapperGetter) (any, error) {
    // you can get other mappers or helpers from MapperGetter here
    return &todoMapperHelper{}, nil
})

Helpers will be called at the end of the generated mapping implementations.

Hierarchized mappers

Large applications often consist of multiple go modules.

/
|
+--- domain        : core business logics
|      |
|      +--- go.mod
|
+--- grpc          : gRPC service
|      |
|      +--- go.mod
|      +--- sesame.yml
|
+--- lib           : libraries
       |
       +--- go.mod
       +--- sesame.yml
  • lib defines common mappers like 'StringTimeMapper' .
  • gRPC defines gRPC spcific mappers that maps protoc generated models to domain entities

You can hierarchize mappers by a delegation like the following:

type delegatingMappers struct {
	Mappers
	parent Mappers
}

func (d *delegatingMappers) Get(name string) (any, error) {
	v, err := d.Mappers.Get(name)
	var merr interface {
		NotFound() bool
	}
	if errors.As(err, &merr) && merr.NotFound() {
		return d.parent.Get(name)
	}
	return v, err
}

func NewDefaultMappers(parent Mappers) Mappers {
	m := NewMappers()
	dm := &delegatingMappers{
		Mappers: m,
		parent:  parent,
	}
    // Add gRPC specific mappers and helpers
    return dm
}

// mappers := grpc_mappers.NewDefaultMappers(lib_mappers.NewMappers())

Donation

BTC: 1NEDSyUmo4SMTDP83JJQSWi1MvQUGGNMZB

License

MIT

Author

Yusuke Inuzuka

Documentation

Overview

Package sesame is an object to object mapper generator.

Index

Constants

View Source
const (
	// NilCollectionUnknown is a default value of NilCollection.
	NilCollectionUnknown = iota

	// NilCollectionAsNil maps nil collections as a nil.
	NilCollectionAsNil

	// NilCollectionAsEmpty map nil maps as empty collections.
	NilCollectionAsEmpty

	// NilCollectionMax is a maximum value of NilCollection.
	NilCollectionMax
)

Variables

View Source
var LogEnabledFor = LogLevelInfo

LogEnabledFor is a threshold for the logging.

Functions

func CanCast

func CanCast(sourceType, destType types.Type) bool

CanCast returns true if sourceType can be (safely) casted into destType.

func GetField

func GetField(st *types.Struct, name string, ignoreCase bool) (*types.Var, bool)

GetField finds a *types.Var by name. If a field not found, GetField returns false.

func GetMethod

func GetMethod(nm *types.Named, name string, ignoreCase bool) (*types.Func, bool)

GetMethod finds a *types.Func by name. If a method not found, GetField returns false.

func GetNamedType

func GetNamedType(typ types.Type) (*types.Named, bool)

GetNamedType returns a named type if an underlying type is a struct type.

func GetPreferableTypeSource

func GetPreferableTypeSource(typ types.Type, mctx *MappingContext) string

GetPreferableTypeSource returns a string representation with an alias package name. GetPreferableTypeSource returns

  • If type is defined in the universe, a type without pointer
  • Otherwise, a type with pointer

func GetQualifiedTypeName

func GetQualifiedTypeName(typ types.Type) string

GetQualifiedTypeName returns a qualified name of given type. Qualified name is a string joinning package and name with #.

func GetSource

func GetSource(typ types.Type, mctx *MappingContext) string

GetSource returns a string representation with an alias package name.

func GetStructType

func GetStructType(typ types.Type) (*types.Struct, bool)

GetStructType returns a struct type if an underlying type is a struct type.

func IsBuiltinType

func IsBuiltinType(typ types.Type) bool

IsBuiltinType returns true if given type is defined in the universe.

func IsPointerPreferableType

func IsPointerPreferableType(typ types.Type) bool

IsPointerPreferableType returns true if given type seems to be better for using as a pointer.

func LoadConfig

func LoadConfig(target any, path string) error

LoadConfig read a config file from `path` relative to the current directory.

func LoadConfigFS

func LoadConfigFS(target any, path string, fs fs.FS) error

LoadConfigFS read a config file from `path` in `fs`.

func ParseFile

func ParseFile(pkgPath string, mctx *MappingContext) (*types.Package, error)

ParseFile parses a given Go source code file.

func ParseStruct

func ParseStruct(path string, name string, mctx *MappingContext) (types.Object, error)

ParseStruct parses a given Go source code file to find a struct.

func SortedKeys

func SortedKeys[K cmp.Ordered, V any](in map[K]V) []K

SortedKeys returns the keys of the map m.

func StdLog

func StdLog(level LogLevel, format string, args ...any)

StdLog is a Log that writes to stdout and stderr.

Types

type FieldMapping

type FieldMapping struct {
	// A is a name of the field defined in [Mapping].A.
	A string

	// B is a name of the field defined in [Mapping].B.
	B string

	// SourceFile is a source file path that contains this configuration.
	SourceFile string
}

FieldMapping is definitions of how fields will be mapped.

func (*FieldMapping) Value

func (m *FieldMapping) Value(typ OperandType) string

Value returns a value by OperandType .

type FieldMappings

type FieldMappings []*FieldMapping

FieldMappings is a collection of FieldMapping s.

func (FieldMappings) ConfigLoaded

func (f FieldMappings) ConfigLoaded(path string) []error

ConfigLoaded is an event handler will be executed when config is loaded.

func (FieldMappings) Pair

func (f FieldMappings) Pair(typ OperandType, value string) (string, bool)

Pair returns a paired value.

type Generation

type Generation struct {
	// Mappers are a definition of the collection of mappers.
	Mappers *Mappers

	// Mappings is definitions of the mappings.
	Mappings []*Mapping

	// SourceFile is a source file path that contains this configuration.
	SourceFile string
}

Generation is a definition of the mappings.

func (*Generation) ConfigLoaded

func (g *Generation) ConfigLoaded(_ string) []error

ConfigLoaded is an event handler will be executed when config is loaded.

type Generator

type Generator interface {
	Generate() error
}

Generator is an interface that generates mappers.

func NewGenerator

func NewGenerator(config *Generation) Generator

NewGenerator creates a new Generator .

type Ignores

type Ignores []*FieldMapping

Ignores is a collection of fields should be ignored.

func (Ignores) ConfigLoaded

func (f Ignores) ConfigLoaded(path string) []error

ConfigLoaded is an event handler will be executed when config is loaded.

func (Ignores) Contains

func (f Ignores) Contains(typ OperandType, value string) bool

Contains returns true if this collection contains a value.

type Log

type Log func(level LogLevel, format string, args ...any)

Log is a function for the logging.

var LogFunc Log = StdLog

LogFunc is a Log used in this package.

type LogLevel

type LogLevel int

LogLevel is a level of the logging.

const (
	// LogLevelDebug is a debug level log.
	LogLevelDebug LogLevel = -4

	// LogLevelInfo is an info level log.
	LogLevelInfo LogLevel = 0

	// LogLevelWarn is a warning level log.
	LogLevelWarn LogLevel = 4

	// LogLevelError is an error level log.
	LogLevelError LogLevel = 8
)

type MapperFuncField

type MapperFuncField struct {
	// FieldName is a name of the field.
	FieldName string

	// MapperFuncName is a name of the mapper function.
	MapperFuncName string

	// Source is a source type of the function.
	Source types.Type

	// Dest is a source type of the function.
	Dest types.Type
}

MapperFuncField is a mapper function field.

func (*MapperFuncField) Signature

func (m *MapperFuncField) Signature(mctx *MappingContext) string

Signature returns a function signature.

type Mappers

type Mappers struct {
	// Package is a package of a mappers.
	Package string

	// Destination is a file path that this mappers will be written.
	Destination string

	// NilMap defines how are nil maps are mapped.
	NilMap NilCollection `mapstructure:"nil-map"`

	// NilSlice defines how are nil maps are mapped.
	NilSlice NilCollection `mapstructure:"nil-slice"`

	// SourceFile is a source file path that contains this configuration.
	SourceFile string
}

Mappers is a definition of the mappers.

func (*Mappers) ConfigLoaded

func (m *Mappers) ConfigLoaded(path string) []error

ConfigLoaded is an event handler will be executed when config is loaded.

type Mapping

type Mapping struct {
	// Name is a name of a mapper.
	Name string

	// Package is a package of a mapper.
	Package string

	// Destination is a file path that this mapper will be written.
	Destination string

	// AtoB is a name of a function.
	AtoB string `mapstructure:"a-to-b"`

	// AtoB is a name of a function.
	BtoA string `mapstructure:"b-to-a"`

	// Bidirectional means this mapping is a bi-directional mapping.
	Bidirectional bool

	// A is a mapping operand.
	A *MappingOperand

	// B is a mapping operand.
	B *MappingOperand

	// SourceFile is a source file path that contains this configuration.
	SourceFile string

	// ObjectMapping is a mapping definition for objects.
	ObjectMapping `mapstructure:",squash"`
}

Mapping is a definition of the mapping.

func (*Mapping) ConfigLoaded

func (m *Mapping) ConfigLoaded(path string) []error

ConfigLoaded is an event handler will be executed when config is loaded.

func (*Mapping) MethodName

func (m *Mapping) MethodName(typ OperandType) string

MethodName returns a name of a function that maps objects.

func (*Mapping) PrivateName

func (m *Mapping) PrivateName() string

PrivateName return a private-d name.

type MappingContext

type MappingContext struct {
	// contains filtered or unexported fields
}

MappingContext is an interface that contains contextual data for the generation.

func NewMappingContext

func NewMappingContext(absPkgPath string) *MappingContext

NewMappingContext returns new MappingContext .

func (*MappingContext) AbsolutePackagePath

func (c *MappingContext) AbsolutePackagePath() string

AbsolutePackagePath returns na absolute package path of a file will be generated this mapping.

func (*MappingContext) AddImport

func (c *MappingContext) AddImport(importpath string)

AddImport adds import path and generate new alias name for it.

func (*MappingContext) AddMapperFuncField

func (c *MappingContext) AddMapperFuncField(sourceType types.Type, destType types.Type)

AddMapperFuncField adds a mapper function and generates a field name for it.

func (*MappingContext) GetImportAlias

func (c *MappingContext) GetImportAlias(path string) string

GetImportAlias returns an alias for the given import path.

func (*MappingContext) GetImportPath

func (c *MappingContext) GetImportPath(alias string) string

GetImportPath returns a fully qualified path for the given import alias. If alias is not found, GetImportPath returns given alias.

func (*MappingContext) GetMapperFuncFieldName

func (c *MappingContext) GetMapperFuncFieldName(sourceType types.Type, destType types.Type) *MapperFuncField

GetMapperFuncFieldName returns a mapper function field name.

func (*MappingContext) Imports

func (c *MappingContext) Imports() map[string]string

Imports returns a map of the all imports. Result map key is an import path and value is an alias.

func (*MappingContext) MapperFuncFields

func (c *MappingContext) MapperFuncFields() []*MapperFuncField

MapperFuncFields returns a list of MapperFuncField .

func (*MappingContext) NextVarCount

func (c *MappingContext) NextVarCount() int

NextVarCount returns a var count and increments it.

type MappingOperand

type MappingOperand struct {
	// Package is a package path
	Package string

	// Name is a type name of the target.
	// This type must be defined in the File.
	Name string

	// SourceFile is a source file path that contains this configuration.
	SourceFile string
}

MappingOperand is a mapping target.

func (*MappingOperand) ConfigLoaded

func (m *MappingOperand) ConfigLoaded(path string) []error

ConfigLoaded is an event handler will be executed when config is loaded.

type MappingValue

type MappingValue interface {
	// GetGetterSource returns a source code of the getter.
	GetGetterSource() string

	// CanGet returns true if this value is readable.
	CanGet() bool

	// GetSetterSource returns a source code of the setter.
	GetSetterSource(valueSource string) string

	// DisplayName returns a name for humans.
	DisplayName() string

	// CanSet returns true if this value is writable.
	CanSet() bool

	// Type is a type of the value
	Type() types.Type
}

MappingValue is a value that will be a source of the mapping or a destination of the mapping.

func NewLocalMappingValue

func NewLocalMappingValue(name string, typ types.Type) MappingValue

NewLocalMappingValue is a MappingValue that related to local variables.

func NewObjectPropertyMappingValue

func NewObjectPropertyMappingValue(base string, named *types.Named, name string, ignoreCase bool) (MappingValue, bool)

NewObjectPropertyMappingValue creates a new MappingValue related to the given object.

type NilCollection

type NilCollection int

NilCollection is an enum that defines how are nil maps and nil slices mapped.

type ObjectMapping

type ObjectMapping struct {
	// ExplicitOnly indicates that implicit mappings should not be
	// performed.
	ExplicitOnly bool `mapstructure:"explicit-only"`

	// IgnoreCase means this mapping ignores field name casing.
	IgnoreCase bool `mapstructure:"ignore-case"`

	// AllowUnmapped is set true, sesame does not fail if unmapped
	// field exists.
	AllowUnmapped bool `mapstructure:"allow-unmapped"`

	// Fields is definitions of how fields will be mapped.
	Fields FieldMappings

	// Ignores is definitions of the fileds should be ignored.
	Ignores Ignores

	// NilMap defines how are nil maps are mapped.
	NilMap NilCollection `mapstructure:"nil-map"`

	// NilSlice defines how are nil maps are mapped.
	NilSlice NilCollection `mapstructure:"nil-slice"`
}

ObjectMapping is a mapping definition for objects.

func NewObjectMapping

func NewObjectMapping() *ObjectMapping

NewObjectMapping creates new ObjectMapping .

func (*ObjectMapping) AddField

func (m *ObjectMapping) AddField(typ OperandType, v1, v2 string)

AddField adds new FieldMapping to this definition.

type OperandType

type OperandType int

OperandType indicates a target for functions.

const (
	// OperandA means that an operand is 'A'.
	OperandA OperandType = 0

	// OperandB means that an operand is 'B'.
	OperandB OperandType = 1
)

func (OperandType) Inverted

func (v OperandType) Inverted() OperandType

Inverted returns an inverted OperandType .

func (OperandType) String

func (v OperandType) String() string

String implements fmt.Stringer.

type Printer

type Printer interface {
	io.Closer

	// P writes formatted-string and a newline.
	P(string, ...any)

	// WriteDoNotEdit writes a "DO NOT EDIT" header.
	WriteDoNotEdit()

	// AddVar adds a template variable name.
	AddVar(string)

	// ResolveVar resolves a variable value.
	ResolveVar(string, string)
}

Printer writes generated source codes. If dest already exists, Printer appends a new data to the end of it.

func NewPrinter

func NewPrinter(dest string) (Printer, error)

NewPrinter creates a new Printer that writes a data to dest.

Directories

Path Synopsis
cmd
sesame
package main is an executable command for sasame(https://github.com/yuin/sesame), an object to object mapper for Go.
package main is an executable command for sasame(https://github.com/yuin/sesame), an object to object mapper for Go.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL