Skip to content

Commit

Permalink
Add goverter:matchIgnoreCase
Browse files Browse the repository at this point in the history
  • Loading branch information
nissim-natanov-outreach committed Mar 27, 2022
1 parent 58ba62d commit edc349e
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 21 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,37 @@ type Output struct {
}
```

### Case-insensitive field matching

With `goverter:matchIgnoreCase` tag you can instruct goverter to perform case-insensitive mapping
between source and target fields. If this tag is present on a conversion method, goverter matches
the fields using strings.EqualFold method.

Use this tag only when it is extremely unlikely for the source or the target to have two fields
that differ only in casing. E.g.: converting go-jet generated model to protoc generated struct.
If `matchIgnoreCase` is present and goverter detects an ambiquous match, it either prefers an exact
match (if found) or reports an error. Use goverter:map to fix an ambiquous match error.

`goverter:matchIgnoreCase` takes no parameters.

```go
// goverter:converter
type Converter interface {
// goverter:matchIgnoreCase
// goverter:map FullId FullID
Convert(source Input) Output
}

type Input struct {
Uuid string
FullId int
fullId int
}
type Output struct {
UUID string // auto-matched with Uuid due to goverter:matchIgnoreCase
FullID string // mapped to FullId, to resolve ambiguity
}
```

#### Struct identity mapping

Expand Down
1 change: 1 addition & 0 deletions builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ type MethodContext struct {
IdentityMapping map[string]struct{}
Signature xtype.Signature
PointerChange bool
MatchIgnoreCase bool
}
26 changes: 15 additions & 11 deletions builder/struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,20 @@ func mapField(gen Generator, ctx *MethodContext, targetField *types.Var, sourceI

mappedName, hasOverride := ctx.Mapping[targetField.Name()]
if ctx.Signature.Target != target.T.String() || !hasOverride {
if fieldSource, ok := source.StructField(targetField.Name()); ok {
nextID := sourceID.Code.Clone().Dot(targetField.Name())
sourceMatch, err := source.StructField(targetField.Name(), ctx.MatchIgnoreCase, ctx.IgnoredFields)
if err == nil {
nextID := sourceID.Code.Clone().Dot(sourceMatch.Name)
lift = append(lift, &Path{
Prefix: ".",
SourceID: targetField.Name(),
SourceType: fieldSource.T.String(),
SourceID: sourceMatch.Name,
SourceType: sourceMatch.Type.T.String(),
TargetID: targetField.Name(),
TargetType: targetField.Type().String(),
})
return nextID, fieldSource, []jen.Code{}, lift, nil
return nextID, sourceMatch.Type, []jen.Code{}, lift, nil
}
cause := fmt.Sprintf("Cannot set value for field %s because it does not exist on the source entry.", targetField.Name())
// field lookup either did not find anything or failed due to ambiquous match with case ignored
cause := fmt.Sprintf("Cannot match the target field with the source entry: %s.", err.Error())
return nil, nil, nil, nil, NewError(cause).Lift(&Path{
Prefix: ".",
SourceID: "???",
Expand Down Expand Up @@ -122,12 +124,14 @@ func mapField(gen Generator, ctx *MethodContext, targetField *types.Var, sourceI
SourceType: "???",
}).Lift(lift...)
}
var ok bool
if nextSource, ok = nextSource.StructField(path[i]); ok {
nextID = nextID.Clone().Dot(path[i])
// since we are searching for a mapped name, search for exact match, explicit field map does not ignore case
sourceMatch, err := nextSource.StructField(path[i], false, ctx.IgnoredFields)
if err == nil {
nextSource = sourceMatch.Type
nextID = nextID.Clone().Dot(sourceMatch.Name)
liftPath := &Path{
Prefix: ".",
SourceID: path[i],
SourceID: sourceMatch.Name,
SourceType: nextSource.T.String(),
}

Expand All @@ -142,7 +146,7 @@ func mapField(gen Generator, ctx *MethodContext, targetField *types.Var, sourceI
continue
}

cause := fmt.Sprintf("Mapped source field '%s' doesn't exist.", path[i])
cause := fmt.Sprintf("Cannot find the mapped field on the source entry: %s.", err.Error())
return nil, nil, []jen.Code{}, nil, NewError(cause).Lift(&Path{
Prefix: ".",
SourceID: path[i],
Expand Down
11 changes: 9 additions & 2 deletions comments/parse_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ type ConverterConfig struct {

// Method contains settings that can be set via comments.
type Method struct {
IgnoredFields map[string]struct{}
NameMapping map[string]string
IgnoredFields map[string]struct{}
NameMapping map[string]string
MatchIgnoreCase bool
// target to source
IdentityMapping map[string]struct{}
}
Expand Down Expand Up @@ -226,6 +227,12 @@ func parseMethodComment(comment string) (Method, error) {
m.IgnoredFields[f] = struct{}{}
}
continue
case "matchIgnoreCase":
if len(fields) != 1 {
return m, fmt.Errorf("invalid %s:matchIgnoreCase, parameters not supported", prefix)
}
m.MatchIgnoreCase = true
continue
}
return m, fmt.Errorf("unknown %s comment: %s", prefix, line)
}
Expand Down
4 changes: 4 additions & 0 deletions generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type methodDefinition struct {
Mapping map[string]string
IgnoredFields map[string]struct{}
IdentityMapping map[string]struct{}
MatchIgnoreCase bool

Jen jen.Code

Expand Down Expand Up @@ -77,6 +78,7 @@ func (g *generator) registerMethod(methodType *types.Func, methodComments commen
Source: xtype.TypeOf(source),
Target: xtype.TypeOf(target),
Mapping: methodComments.NameMapping,
MatchIgnoreCase: methodComments.MatchIgnoreCase,
IgnoredFields: methodComments.IgnoredFields,
IdentityMapping: methodComments.IdentityMapping,
ReturnError: returnError,
Expand Down Expand Up @@ -152,6 +154,7 @@ func (g *generator) buildMethod(method *methodDefinition) *builder.Error {
Mapping: method.Mapping,
IgnoredFields: method.IgnoredFields,
IdentityMapping: method.IdentityMapping,
MatchIgnoreCase: method.MatchIgnoreCase,
Signature: xtype.Signature{Source: method.Source.T.String(), Target: method.Target.T.String()},
}
stmt, newID, err := g.buildNoLookup(ctx, xtype.VariableID(sourceID.Clone()), source, target)
Expand Down Expand Up @@ -232,6 +235,7 @@ func (g *generator) Build(ctx *builder.MethodContext, sourceID *xtype.JenID, sou
if ctx.PointerChange {
ctx.PointerChange = false
method.Mapping = ctx.Mapping
method.MatchIgnoreCase = ctx.MatchIgnoreCase
method.IgnoredFields = ctx.IgnoredFields
}

Expand Down
2 changes: 1 addition & 1 deletion runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestScenario(t *testing.T) {

if scenario.Error != "" {
require.Error(t, err)
require.Equal(t, replaceAbsolutePath(fmt.Sprint(err)), scenario.Error)
require.Equal(t, scenario.Error, replaceAbsolutePath(fmt.Sprint(err)))
return
}

Expand Down
36 changes: 36 additions & 0 deletions scenario/3_struct_match_ignore_case.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
input:
input.go: |
package structs
// goverter:converter
type Converter interface {
// goverter:matchIgnoreCase
Convert(source Input) Output
}
type Input struct {
Name string
UUID string
OtherID int
}
type Output struct {
Name string
Uuid string
OtherId int
}
success: |
// Code generated by github.com/jmattheis/goverter, DO NOT EDIT.
package generated
import execution "github.com/jmattheis/goverter/execution"
type ConverterImpl struct{}
func (c *ConverterImpl) Convert(source execution.Input) execution.Output {
var structsOutput execution.Output
structsOutput.Name = source.Name
structsOutput.Uuid = source.UUID
structsOutput.OtherId = source.OtherID
return structsOutput
}
46 changes: 46 additions & 0 deletions scenario/3_struct_match_ignore_case_ambiguous_ok.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
input:
input.go: |
package structs
// goverter:converter
type Converter interface {
// goverter:matchIgnoreCase
// goverter:map AmbID AmbId
// goverter:ignore Ambid2
Convert(source Input) Output
}
type Input struct {
OtherID string
OtherId int // should prefer exact match
Uuid string
AmbID float64 // explicit choice
Ambid float64
AmbID2 string
Ambid2 string
}
type Output struct {
OtherId int
Uuid string // should prefer exact match
UUID string
AmbId float64
AmbId2 string
}
success: |
// Code generated by github.com/jmattheis/goverter, DO NOT EDIT.
package generated
import execution "github.com/jmattheis/goverter/execution"
type ConverterImpl struct{}
func (c *ConverterImpl) Convert(source execution.Input) execution.Output {
var structsOutput execution.Output
structsOutput.OtherId = source.OtherId
structsOutput.Uuid = source.Uuid
structsOutput.UUID = source.Uuid
structsOutput.AmbId = source.AmbID
structsOutput.AmbId2 = source.AmbID2
return structsOutput
}
10 changes: 10 additions & 0 deletions scenario/7_error_invalid_match_ignore_case_with_param.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
input:
input.go: |
package structs
// goverter:converter
type Converter interface {
// goverter:matchIgnoreCase a
ConvertString(source string) string
}
error: '/ABSOLUTE/execution/input.go:4:1: type Converter: parsing method ConvertString: invalid goverter:matchIgnoreCase, parameters not supported'
42 changes: 42 additions & 0 deletions scenario/7_error_match_ignore_case_err_path copy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
input:
input.go: |
package structs
// goverter:converter
type Converter interface {
// goverter:matchIgnoreCase
Convert(source Input) Output
}
type Input struct {
NESTED InputNested
}
type InputNested struct {
Name string
}
type Output struct {
Nested OutputNested
}
type OutputNested struct {
Name int
}
error: |-
Error while creating converter method:
func (github.com/jmattheis/goverter/execution.Converter).Convert(source github.com/jmattheis/goverter/execution.Input) github.com/jmattheis/goverter/execution.Output
| github.com/jmattheis/goverter/execution.Input
|
| | github.com/jmattheis/goverter/execution.InputNested
| |
| | | string
| | |
source.NESTED.Name
target.Nested.Name
| | |
| | | int
| |
| | github.com/jmattheis/goverter/execution.OutputNested
|
| github.com/jmattheis/goverter/execution.Output
TypeMismatch: Cannot convert string to int
39 changes: 39 additions & 0 deletions scenario/7_error_match_ignore_case_unresolved_ambiguity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
input:
input.go: |
package structs
// goverter:converter
type Converter interface {
// goverter:matchIgnoreCase
Convert(source Input) Output
}
type Input struct {
MyID string
MyId string
}
type Output struct {
Myid string
}
error: |-
Error while creating converter method:
func (github.com/jmattheis/goverter/execution.Converter).Convert(source github.com/jmattheis/goverter/execution.Input) github.com/jmattheis/goverter/execution.Output
| github.com/jmattheis/goverter/execution.Input
|
|
|
source.???
target.Myid
| |
| | string
|
| github.com/jmattheis/goverter/execution.Output
Cannot match the target field with the source entry: multiple matches found for "Myid". Possible matches: MyID, MyId.
Explicitly define the mapping via goverter:map. Example:
goverter:map MyID Myid
See https://github.com/jmattheis/goverter#struct-field-mapping.
2 changes: 1 addition & 1 deletion scenario/7_error_struct_missing_field.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ error: |-
|
| github.com/jmattheis/goverter/execution.Output
Cannot set value for field Age because it does not exist on the source entry.
Cannot match the target field with the source entry: "Age" does not exist.
2 changes: 1 addition & 1 deletion scenario/7_error_struct_missing_mapped_field.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ error: |-
|
| github.com/jmattheis/goverter/execution.Output
Mapped source field 'Name3' doesn't exist.
Cannot find the mapped field on the source entry: "Name3" does not exist.
2 changes: 1 addition & 1 deletion scenario/7_error_struct_missing_mapped_nested_field.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ error: |-
|
| github.com/jmattheis/goverter/execution.Output
Mapped source field 'Name3' doesn't exist.
Cannot find the mapped field on the source entry: "Name3" does not exist.
Loading

0 comments on commit edc349e

Please sign in to comment.