jmattheis / goverter

Generate type-safe Go converters by simply defining an interface
https://goverter.jmattheis.de/
MIT License
496 stars 46 forks source link

Support `mapExtend` with more usage #37

Closed waltcow closed 1 year ago

waltcow commented 1 year ago

Currently

  1. mapExtend can not support pass pointer type of source as parameter
  2. mapExtend may support pass particular field of source as parameter
// goverter:converter
type Converter interface {
    // goverter:mapExtend FullName ExtendFullName
    // goverter:mapExtend Age DefaultAge
    Convert(source *Input) *Output

    // goverter:mapExtend LastName FullName ExtendWithSpecName
    ConvertMeta(source *Input) *OutputMeta
}

type Input struct {
    ID        int
    FirstName string
    LastName  string
}
type Output struct {
    ID       int
    FullName string
    Age      int
}

type OutputMeta struct {
    ID       int
    FullName string
}

func ExtendFullName(source *Input) string {
    return source.FirstName + " " + source.LastName
}
func DefaultAge() int { return 42 }

func ExtendWithSpecName(name string) string {
    return name + " Spec"
}

Code generated

// Code generated by github.com/jmattheis/goverter, DO NOT EDIT.

package generated

import mapextend "github.com/jmattheis/goverter/example/mapextend"

type ConverterImpl struct{}

func (c *ConverterImpl) Convert(source *mapextend.Input) *mapextend.Output {
    var pMapextendOutput *mapextend.Output
    if source != nil {
        mapextendOutput := c.mapextendInputToMapextendOutput(*source)
        pMapextendOutput = &mapextendOutput
    }
    return pMapextendOutput
}
func (c *ConverterImpl) ConvertMeta(source *mapextend.Input) *mapextend.OutputMeta {
    var pMapextendOutputMeta *mapextend.OutputMeta
    if source != nil {
        mapextendOutputMeta := c.mapextendInputToMapextendOutputMeta(*source)
        pMapextendOutputMeta = &mapextendOutputMeta
    }
    return pMapextendOutputMeta
}
func (c *ConverterImpl) mapextendInputToMapextendOutput(source mapextend.Input) mapextend.Output {
    var mapextendOutput mapextend.Output
    mapextendOutput.ID = source.ID
    mapextendOutput.FullName = mapextend.ExtendFullName(&source)
    mapextendOutput.Age = mapextend.DefaultAge()
    return mapextendOutput
}
func (c *ConverterImpl) mapextendInputToMapextendOutputMeta(source mapextend.Input) mapextend.OutputMeta {
    var mapextendOutputMeta mapextend.OutputMeta
    mapextendOutputMeta.ID = source.ID
    mapextendOutputMeta.FullName = mapextend.ExtendWithSpecName(source.LastName)
    return mapextendOutputMeta
}
jmattheis commented 1 year ago
  1. The extend methods must be on non pointer types, f.ex. this works
    
    // goverter:converter
    type Converter interface {
    // goverter:mapExtend FullName ExtendFullName
    Convert(source *Input) *Output
    }

type Input struct { FirstName string LastName string } type Output struct { FullName string }

func ExtendFullName(source Input) string { return source.FirstName + " " + source.LastName }


goverter will simplify the input types until there isn't a pointer, and then do the mapping.

2. Could you give a real use-case for this? It doesn't seem like a blocking issue because you can always define the method with the full source type and then only reference a field from it.
waltcow commented 1 year ago

@jmattheis

  1. when I use Protobuf define my schema ,code generate by protoc-gen-go will nested some fields in struct eg:
type User struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Id          string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`                
    DisplayName string `protobuf:"bytes,2,opt,name=displayName,proto3" json:"displayName,omitempty"` 
    Avatar      string `protobuf:"bytes,3,opt,name=avatar,proto3" json:"avatar,omitempty"`       
        Car        *Car `protobuf:"bytes,4,opt,name=car,proto3" json:"car,omitempty"`       
}

type Car struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

        Id          string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`                
        Name          string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` 
}

when you want to map User.Car.Name to UserDTO.CarName,i will use annotation like mapExtend CarName GetUserCarName, GetUserCarName method signature as follow

func GetUserCarName(source User) string {
  return User.Car.Name
}

User contains nested fields like Mutex, which is not recommended pass as value type

waltcow commented 1 year ago

@jmattheis

  1. when I convert field like s3 object key, I want to prepend entry point URL, other methods in the same converter will encounter similar situation. I don't want to declare similar mapping function repeatedly
jmattheis commented 1 year ago
  1. Okay, but this will require some bigger reworks of goverter as using a pointer in just the custom defined function isn't enough. If you've a pointer input somewhere, it is almost guaranteed that goverter will generate code that will copy the value. E.g.
    func (c *ConverterImpl) mapextendInputToMapextendOutput(source mapextend.Input) mapextend.Output {
    var mapextendOutput mapextend.Output
    mapextendOutput.FullName = mapextend.ExtendFullName(&source)
    ...
    return mapextendOutput
    }

    The input/source parameter is a non pointer and will copy the object.

  2. Okay, yes, I thought about it, and it sounds like a valid use-case.
jmattheis commented 1 year ago

With v0.12.0 it's possible to define a field mapping and do a conversion of the given fields. See https://goverter.jmattheis.de/#/conversion/custom?id=mapping-method so the second point from the first comment is solved.

jmattheis commented 1 year ago

@waltcow Could you try out v0.14.0? With this release, Goverter should prefer using struct pointers when building converter methods.

The syntax has slightly changed. Your Converter interface from the first comment should look like this:

// goverter:converter
type Converter interface {
    // goverter:map . FullName | ExtendFullName
    // goverter:map Age | DefaultAge
    Convert(source *Input) *Output

    // goverter:map LastName FullName | ExtendWithSpecName
    ConvertMeta(source *Input) *OutputMeta
}