Skip to content

Commit

Permalink
Add allow-unmapped, ignore-case
Browse files Browse the repository at this point in the history
  • Loading branch information
yuin committed Jul 30, 2023
1 parent b37041b commit af74a53
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 31 deletions.
74 changes: 64 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,18 @@ mappings: # configurations for object-to-
b: # mapping operand B
package: ./domain
name: Todo
explicitOnly: false # sesame maps same names automatically if false(default: false)
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)
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: 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
```
Expand Down Expand Up @@ -140,18 +144,15 @@ Mapping codes look like the following:
1. Create new Mappers object as a singleton object. The Mappers object is a groutine-safe.

````go
mappers := mapper.NewMappers() # Creates new Mappers object
mapper.AddTimeToStringMapper(mappers) # Add custom mappers
mappers.AddFactory("TodoMapperHelper", func(ms MapperGetter) (any, error) {
return &todoMapperHelper{}, nil
}) # Add helpers

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.
```go
obj, err := mappers.Get("TodoMapper") # Get mapper by its name
obj, err := mappers.Get("TodoMapper") // Get mapper by its name
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -268,6 +269,59 @@ mappers.AddFactory("TodoMapperHelper", func(ms MapperGetter) (any, error) {
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:
```go
type delegatingMappers struct {
Mappers
parent Mappers
}

func (d *delegatingMappers) Get(name string) (any, error) {
v, err := d.Mappers.Get(name)
if err != nil && strings.Contains(err.Error(), "not found") {
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
Expand Down
38 changes: 26 additions & 12 deletions gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,14 @@ func (m *Mapping) PrivateName() string {
type ObjectMapping struct {
// ExplicitOnly indicates that implicit mappings should not be
// performed.
ExplicitOnly bool
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
Expand Down Expand Up @@ -710,7 +717,7 @@ func genMapFuncBody(printer Printer,

destName, ok := mapping.Fields.Pair(typ, "*")
if ok { // embedded
destField, _ := GetField(destStruct, destName)
destField, _ := GetField(destStruct, destName, mapping.IgnoreCase)
err := genFieldMapStmts(printer, sourceNameBase, destField.Type(), destNameBase+"."+destName, destField.Type(), mctx)
if err != nil {
return err
Expand All @@ -721,18 +728,18 @@ func genMapFuncBody(printer Printer,
if mapping.Ignores.Contains(typ, sourceField.Name()) {
continue
}
found := false
var destFieldType types.Type
var destFieldNameBase string
destName, ok := mapping.Fields.Pair(typ, sourceField.Name())
if ok { // map explicitly
found := false
if destName == "*" { // embedded
found = true
destFieldType = sourceField.Type()
destFieldNameBase = destNameBase
} else {
parts := strings.SplitN(destName, ".", -1)
destField, ok := GetField(destStruct, destName)
destField, ok := GetField(destStruct, destName, mapping.IgnoreCase)
if ok {
found = true
destFieldType = destField.Type()
Expand All @@ -741,7 +748,7 @@ func genMapFuncBody(printer Printer,
if len(parts) > 1 {
for i := 1; i < len(parts); i++ {
nestName := strings.Join(parts[:i], ".")
nestField, ok := GetField(destStruct, nestName)
nestField, ok := GetField(destStruct, nestName, mapping.IgnoreCase)
if ok {
p("if %s.%s == nil {", destNameBase, nestName)
p(" %s.%s = %s{}", destNameBase, nestName, strings.Replace(GetSource(nestField.Type(), mctx), "*", "&", 1))
Expand All @@ -750,17 +757,23 @@ func genMapFuncBody(printer Printer,
}
}
}
if !found {
return fmt.Errorf("Could not map a field: '%s.%s.%s' to '%s'",
source.Pkg().Name(), source.Name(), sourceField.Name(), destName)
}
} else if !mapping.ExplicitOnly { // map implicitly
destField, ok := GetField(destStruct, sourceField.Name())
destField, ok := GetField(destStruct, sourceField.Name(), mapping.IgnoreCase)
if ok {
found = true
destFieldType = destField.Type()
destFieldNameBase = destNameBase + "." + destField.Name()
} else {
if mapping.AllowUnmapped {
LogFunc(LogLevelDebug, "%s.%s.%s is ignored", source.Pkg().Name(), source.Name(), sourceField.Name())
continue
}
return fmt.Errorf("Unmapped field: '%s.%s.%s'", source.Pkg().Name(), source.Name(), sourceField.Name())
}
}

if !found {
LogFunc(LogLevelDebug, "%s.%s.%s is ignored", source.Pkg().Name(), source.Name(), sourceField.Name())
} else {
continue
}

Expand All @@ -777,14 +790,15 @@ func genMapFuncBody(printer Printer,

parts := strings.SplitN(sourceFieldName, ".", 2)
if len(parts) > 1 {
f, ok := GetField(sourceStruct, parts[0])
f, ok := GetField(sourceStruct, parts[0], mapping.IgnoreCase)
if !ok {
continue
}

nestMapping := NewObjectMapping()
nestMapping.ExplicitOnly = true
nestMapping.AddField(typ, parts[1], destFieldName)
nestMapping.IgnoreCase = mapping.IgnoreCase
err := genMapFuncBody(printer, f, sourceNameBase+"."+parts[0],
dest, destNameBase, nestMapping, typ, mctx)
if err != nil {
Expand Down
14 changes: 10 additions & 4 deletions testdata/testmod/mapper/todo_mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestTodoMapper(t *testing.T) {
todoMapper, _ := obj.(TodoMapper)

source := &model.TodoModel{
ID: 1,
Id: 1,
UserID: "AAA",
Title: "Write unit tests",
Type: 1,
Expand Down Expand Up @@ -69,14 +69,14 @@ func TestTodoMapper(t *testing.T) {
t.Errorf("Compare value is mismatch(-:expected, +:actual) :%s\n", diff)
}

// entity.ID=in64, model.ID=int(32), so ID can not be casted into a dest type
// entity.ID=in64, model.Id=int(32), so ID can not be casted into a dest type
entity.ID = 1
reversed, err := todoMapper.TodoToTodoModel(entity)
if err != nil {
t.Fatal(err)
}
source.ValidateOnly = false
source.ID = 0
source.Id = 0

if diff := cmp.Diff(source, reversed); len(diff) != 0 {
t.Errorf("Compare value is mismatch(-:expected, +:actual) :%s\n", diff)
Expand All @@ -90,12 +90,18 @@ var _ TodoMapperHelper = &todoMapperHelper{}

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

func (h *todoMapperHelper) TodoToTodoModel(source *domain.Todo, dest *model.TodoModel) error {
if source.Attributes == nil {
return nil
}
if _, ok := source.Attributes["ValidateOnly"]; ok {
dest.ValidateOnly = true
}
Expand All @@ -117,7 +123,7 @@ func TestMapperHelper(t *testing.T) {
todoMapper, _ := obj.(TodoMapper)

source := &model.TodoModel{
ID: 1,
Id: 1,
UserID: "AAA",
Title: "Write unit tests",
Type: 1,
Expand Down
2 changes: 1 addition & 1 deletion testdata/testmod/model/todo.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package model

type TodoModel struct {
ID int
Id int
UserID string
Title string
Type int
Expand Down
2 changes: 2 additions & 0 deletions testdata/testmod/sesame.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ mappings:
b:
package: ./domain
name: Todo
ignore-case: true
fields:
- a: Done
b: Finished
- a: UserID
b: User.ID
ignores:
- a: ValidateOnly
- b: User
_includes:
- ./*/**/*_sesame.yml
10 changes: 6 additions & 4 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,25 @@ import (

// GetField finds a *[types].Var by name.
// If a field not found, GetField returns false.
func GetField(st *types.Struct, name string) (*types.Var, bool) {
func GetField(st *types.Struct, name string, ignoreCase bool) (*types.Var, bool) {
parts := strings.SplitN(name, ".", 2)
if len(parts) > 1 {
for i := 0; i < st.NumFields(); i++ {
f := st.Field(i)
if f.Name() == parts[0] {
if (f.Name() == parts[0]) ||
(ignoreCase && strings.ToLower(f.Name()) == strings.ToLower(parts[0])) {
s, ok := GetStructType(f.Type())
if !ok {
return nil, false
}
return GetField(s, parts[1])
return GetField(s, parts[1], ignoreCase)
}
}
} else {
for i := 0; i < st.NumFields(); i++ {
f := st.Field(i)
if f.Name() == name {
if (f.Name() == name) ||
(ignoreCase && strings.ToLower(f.Name()) == strings.ToLower(name)) {
return f, true
}
}
Expand Down

0 comments on commit af74a53

Please sign in to comment.