-
-
Notifications
You must be signed in to change notification settings - Fork 61
Description
Is your feature request related to a problem? Please describe.
Writing comments on functions was good enough when I had one or two converters. But now, with 10s of converters and after using goverter in our mainstream toolchains, writing the mapping comments became very hard, error prune, and time consuming.
I may summarize our problems in the following points:
- New comers need to learn new DSL (comments) and usage hacks to get around corner cases in our case.
- IDE don't help with autocomplete for available actions (map, autoMap, default, ...etc)
- We can't share reusable code mappings between different converters while the mapping is exactly the same. Similar to Support interface embedding for seperation of concerns #189 . But one big different from Support interface embedding for seperation of concerns #189 is that in our case the mapping itself is the same but the mapped types are not (for example, SourceA -> TargetA and SourceB -> TargetB should have the same comments to map fields, but they are different types).
- No ability to build dynamic mappings, one use case is described in Support regex for mapping field names #122
- typos in comments are hard to discover specially when the typo is in
// goverter:prefix.
Describe the solution you'd like
Well, I don't have a full solution in mind. I was just thinking if we can use something like https://github.com/goadesign/goa or https://gorm.io/gen/database_to_structs.html (which I already use 😁).
At the moment, I have couple of approaches in mind:
- Keep the interface/vars definitions and write code in the same file to define mappings
- Like goa and gorm/gen, completely abstract the converter definition and generate everything.
I prefer the second approach for better abstraction and I gain no value of managing the interface definition myself.
Maybe something like the following:
Details
package def
// Below is very basic illustration of definition builder.
type Converter struct {
// config holds all configurations that normally added as interface comments
config any
// methods holds definitions for all methods for that converter interface
methods []*Method
}
// NewConverter creates a new converter instance.
// config may be omitted as AFAIK there is no required configurations that must be provided in the converter level
func NewConverter(config any) *Converter {
return &Converter{config: config}
}
func (c *Converter) Extend(path string) *Converter {
// ...
return c
}
// Method creates a new method on this converter instance.
// config may be replaced by multiple arguments to properly model the method Signature.
func (c *Converter) Method(config any, builders ...func(*Method) *Method) *Converter {
c.methods = append(c.methods, NewMethod(config).Builders(builders...))
return c
}
type MethodMaps struct {
source, target string
}
func NewMethodMaps(source string, target string) *MethodMaps {
return &MethodMaps{source: source, target: target}
}
type Method struct {
config any
maps []*MethodMaps
}
func NewMethod(config any) *Method {
return &Method{config: config}
}
func (m *Method) Map(source, target string) *Method {
m.maps = append(m.maps, NewMethodMaps(source, target))
return m
}
func (m *Method) Builders(fns ...func(*Method) *Method) *Method {
mm := m
for _, fn := range fns {
mm = fn(mm)
}
return mm
}
func (m *Method) AutoMap(source, target string) *Method {
// ...
return m
}
func (m *Method) Ignore(fields ...string) *Method {
// ...
return m
}
func Generate(converters []*Converter) error {
// do the magic
return nil
}
// In my code, I should have an entrypoint that constructs the converters and pass them to Generate func.
// goverter may also support this mode somehow in its CLI.
func main() {
converters := []*Converter{
NewConverter("converterA").
Method("ConvertAToB", aToBMapField),
NewConverter("converterB").
Method("ConvertAToB", aToBMapField),
}
err := Generate(converters)
if err != nil {
panic(err)
}
}
func aToBMapField(m *Method) *Method {
mappings := map[string]string{
"id": "ID",
"created_at": "Audit.CreatedAt",
}
for source, target := range mappings {
m.Map(source, target)
}
return m
}Describe alternatives you've considered
None
Please 👍 this issue if you like this functionality. If you have a specific use-case in mind, feel free to comment it.