Skip to content

Commit

Permalink
Merge pull request #95 from jmattheis/try-underlying-named-types
Browse files Browse the repository at this point in the history
feat: `useUnderlyingTypeMethods`
  • Loading branch information
jmattheis authored Nov 3, 2023
2 parents cb44e9a + ba19d0b commit 13d3233
Show file tree
Hide file tree
Showing 13 changed files with 349 additions and 8 deletions.
3 changes: 3 additions & 0 deletions builder/builder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package builder

import (
"go/types"

"github.com/dave/jennifer/jen"
"github.com/jmattheis/goverter/config"
"github.com/jmattheis/goverter/method"
Expand Down Expand Up @@ -40,6 +42,7 @@ type MethodContext struct {
FieldsTarget string
Signature xtype.Signature
TargetType *xtype.Type
HasMethod func(types.Type, types.Type) bool
SeenNamed map[string]struct{}

TargetVar *jen.Statement
Expand Down
70 changes: 70 additions & 0 deletions builder/underlying.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package builder

import (
"github.com/dave/jennifer/jen"
"github.com/jmattheis/goverter/xtype"
)

// UseUnderlyingTypeMethods handles UseUnderlyingTypeMethods.
type UseUnderlyingTypeMethods struct{}

// Matches returns true, if the builder can create handle the given types.
func (*UseUnderlyingTypeMethods) Matches(ctx *MethodContext, source, target *xtype.Type) bool {
if !ctx.Conf.UseUnderlyingTypeMethods {
return false
}

sourceUnderlying, targetUnderlying := findUnderlyingExtendMapping(ctx, source, target)
return sourceUnderlying || targetUnderlying
}

// Build creates conversion source code for the given source and target type.
func (*UseUnderlyingTypeMethods) Build(gen Generator, ctx *MethodContext, sourceID *xtype.JenID, source, target *xtype.Type) ([]jen.Code, *xtype.JenID, *Error) {
sourceUnderlying, targetUnderlying := findUnderlyingExtendMapping(ctx, source, target)

innerSource := source
innerTarget := target

if sourceUnderlying {
innerSource = xtype.TypeOf(source.NamedType.Underlying())
sourceID = xtype.OtherID(innerSource.TypeAsJen().Call(sourceID.Code))
}

if targetUnderlying {
innerTarget = xtype.TypeOf(target.NamedType.Underlying())
}

stmt, id, err := gen.Build(ctx, sourceID, innerSource, innerTarget, NoWrap)
if err != nil {
return nil, nil, err.Lift(&Path{
SourceID: "*",
SourceType: innerSource.T.String(),
TargetID: "*",
TargetType: innerTarget.T.String(),
})
}

if targetUnderlying {
id = xtype.OtherID(target.TypeAsJen().Call(id.Code))
}

return stmt, id, err
}

func findUnderlyingExtendMapping(ctx *MethodContext, source, target *xtype.Type) (underlyingSource, underlyingTarget bool) {
if source.Named {
if ctx.HasMethod(source.NamedType.Underlying(), target.NamedType) {
return true, false
}

if target.Named && ctx.HasMethod(source.NamedType.Underlying(), target.NamedType.Underlying()) {
return true, true
}
}

if target.Named && ctx.HasMethod(source.NamedType, target.NamedType.Underlying()) {
return false, true
}

return false, false
}
3 changes: 3 additions & 0 deletions config/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Common struct {
IgnoreMissing bool
SkipCopySameType bool
UseZeroValueOnPointerInconsistency bool
UseUnderlyingTypeMethods bool
}

func parseCommon(c *Common, cmd, rest string) (err error) {
Expand All @@ -25,6 +26,8 @@ func parseCommon(c *Common, cmd, rest string) (err error) {
c.SkipCopySameType, err = parseBool(rest)
case "useZeroValueOnPointerInconsistency":
c.UseZeroValueOnPointerInconsistency, err = parseBool(rest)
case "useUnderlyingTypeMethods":
c.UseUnderlyingTypeMethods, err = parseBool(rest)
case "":
err = fmt.Errorf("missing setting key")
default:
Expand Down
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [output](config/output.md)
- [skipCopySameType](config/skipCopySameType.md)
- [struct](config/struct.md)
- [useUnderlyingTypeMethods](config/useUnderlyingTypeMethods.md)
- [useZeroValueOnPointerInconsistency](config/useZeroValueOnPointerInconsistency.md)
- [wrapErrors](config/wrapErrors.md)
- [Migrations](migrations.md)
Expand Down
1 change: 1 addition & 0 deletions docs/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ These settings can be defined as [CLI argument](config/define.md#cli),
- [`ignoreUnexported [yes,no]` ignore unexported struct fields](config/ignoreUnexported.md)
- [`matchIgnoreCase [yes,no]` case-insensitive field matching](config/matchIgnoreCase.md)
- [`skipCopySameType [yes,no]` skip copying types when the source and target type are the same](config/skipCopySameType.md)
- [`useUnderlyingTypeMethods [yes|no]` use underlying types when looking for existing methods](config/useUnderlyingTypeMethods.md)
- [`useZeroValueOnPointerInconsistency [yes|no]` Use zero values for `*S` to `T` conversions](config/useZeroValueOnPointerInconsistency.md)
- [`wrapErrors [yes,no]` wrap errors with extra information](config/wrapErrors.md)
70 changes: 70 additions & 0 deletions docs/config/useUnderlyingTypeMethods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
`useUnderlyingTypeMethods [yes,no]` is a
[boolean setting](config/define.md#boolean) and can be defined as
[CLI argument](config/define.md#cli),
[converter comment](config/define.md#converter) or
[method comment](config/define.md#method). This setting is
[inheritable](config/define.md#inheritance).

For each type conversion, goverter will check if there is an existing method
that can be used. For named types only the type itself will be checked and not
the underlying type. For this type:

```go
type InputID int
```

`InputID` would be the _named_ type and `int` the _underlying_ type.

With `useUnderlyingTypeMethods`, goverter will check all named/underlying
combinations.

- named -> underlying
- underlying -> named
- underlying -> underlying

<!-- tabs:start -->

#### **input.go**

```go
package example

// goverter:converter
// goverter:extend ConvertUnderlying
type Converter interface {
// goverter:useUnderlyingTypeMethods
Convert(source Input) Output
}
func ConvertUnderlying(s int) string {
return ""
}
// these would be used too
// func ConvertUnderlying(s int) OutputID
// func ConvertUnderlying(s InputID) string

type InputID int
type OutputID string
type Input struct { ID InputID }
type Output struct { ID OutputID }
```

#### **generated/generated.go**

```go
package generated

import example "goverter/example"

type ConverterImpl struct{}

func (c *ConverterImpl) Convert(source example.Input) example.Output {
var output example.Output
output.ID = example.OutputID(example.ConvertUnderlying(int(source.ID)))
return output
}
```

<!-- tabs:end -->

Without the setting only the custom method with this signature could be used
for the conversion. `func ConvertUnderlying(s InputID) OutputID`
1 change: 1 addition & 0 deletions generator/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Config struct {

// BuildSteps that'll used for generation.
var BuildSteps = []builder.Builder{
&builder.UseUnderlyingTypeMethods{},
&builder.SkipCopy{},
&builder.BasicTargetPointerRule{},
&builder.Pointer{},
Expand Down
20 changes: 12 additions & 8 deletions generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package generator

import (
"fmt"
"go/types"
"sort"
"strings"

Expand Down Expand Up @@ -98,6 +99,7 @@ func (g *generator) buildMethod(genMethod *generatedMethod, errWrapper builder.E
SeenNamed: map[string]struct{}{},
TargetType: genMethod.Target,
Signature: genMethod.Signature(),
HasMethod: g.hasMethod,
}

var funcBlock []jen.Code
Expand Down Expand Up @@ -290,15 +292,17 @@ func (g *generator) Build(
if err := g.buildMethod(genMethod, errWrapper); err != nil {
return nil, nil, err
}
// try again to trigger the found method thingy above
return g.Build(ctx, sourceID, source, target, errWrapper)
return g.CallMethod(ctx, genMethod.Definition, sourceID, source, target, errWrapper)
}

for _, rule := range BuildSteps {
if rule.Matches(ctx, source, target) {
return rule.Build(g, ctx, sourceID, source, target)
}
}
return g.buildNoLookup(ctx, sourceID, source, target)
}

return nil, nil, builder.NewError(fmt.Sprintf("TypeMismatch: Cannot convert %s to %s", source.T, target.T))
func (g *generator) hasMethod(source, target types.Type) bool {
signature := xtype.Signature{Source: source.String(), Target: target.String()}
_, ok := g.extend[signature]
if !ok {
_, ok = g.lookup[signature]
}
return ok
}
35 changes: 35 additions & 0 deletions scenario/extend_named_underlying_both.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
input:
input.go: |
package structs
// goverter:converter
// goverter:extend ConvertUnderlying
type Converter interface {
// goverter:useUnderlyingTypeMethods
Convert(source Input) Output
}
func ConvertUnderlying(s int) string {
return ""
}
type InputID int
type OutputID string
type Input struct { ID InputID }
type Output struct { ID OutputID }
success:
- generated/generated.go: |
// 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.ID = execution.OutputID(execution.ConvertUnderlying(int(source.ID)))
return structsOutput
}
48 changes: 48 additions & 0 deletions scenario/extend_named_underlying_error.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
input:
input.go: |
package structs
// goverter:converter
// goverter:extend ConvertUnderlying
type Converter interface {
// goverter:useUnderlyingTypeMethods
Convert(source Input) Output
}
func ConvertUnderlying(s int) (string, error) {
return "", nil
}
type InputID int
type OutputID string
type Input struct { ID InputID }
type Output struct { ID OutputID }
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.InputID
| |
| | | int
| | |
source.ID*
target.ID*
| | |
| | | string
| |
| | github.com/jmattheis/goverter/execution.OutputID
|
| github.com/jmattheis/goverter/execution.Output
ReturnTypeMismatch: Cannot use
func github.com/jmattheis/goverter/execution.ConvertUnderlying(s int) (string, error)
in
func (github.com/jmattheis/goverter/execution.Converter).Convert(source github.com/jmattheis/goverter/execution.Input) github.com/jmattheis/goverter/execution.Output
because no error is returned as second return parameter
35 changes: 35 additions & 0 deletions scenario/extend_named_underlying_global.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
input:
input.go: |
package structs
// goverter:converter
// goverter:extend ConvertUnderlying
// goverter:useUnderlyingTypeMethods
type Converter interface {
Convert(source Input) Output
}
func ConvertUnderlying(s int) string {
return ""
}
type InputID int
type OutputID string
type Input struct { ID InputID }
type Output struct { ID OutputID }
success:
- generated/generated.go: |
// 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.ID = execution.OutputID(execution.ConvertUnderlying(int(source.ID)))
return structsOutput
}
35 changes: 35 additions & 0 deletions scenario/extend_named_underlying_source.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
input:
input.go: |
package structs
// goverter:converter
// goverter:extend ConvertUnderlying
type Converter interface {
// goverter:useUnderlyingTypeMethods
Convert(source Input) Output
}
func ConvertUnderlying(s int) OutputID {
return ""
}
type InputID int
type OutputID string
type Input struct { ID InputID }
type Output struct { ID OutputID }
success:
- generated/generated.go: |
// 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.ID = execution.ConvertUnderlying(int(source.ID))
return structsOutput
}
Loading

0 comments on commit 13d3233

Please sign in to comment.