flachnetz / poly

Poly helps with polymorphic (de)serialization to and from json in golang.
MIT License
0 stars 0 forks source link

Support for recursive poly struct? #1

Open fengye87 opened 10 months ago

fengye87 commented 10 months ago

I bumped into this project while looking for a lib to solve marshaling my poly struct to JSON (and back from it too). But my poly struct is recursive. Say like

type Shape interface {}

type Square struct {
    ...
    Child Shape
}

type Triangle struct {
    ...
    Child Shape
}

So Child would not marshal/unmarshal as expected here. Any way to support it?

oliverbestmann commented 10 months ago

This should still work if you define Child as a poly value, the same as on the top level of the example in the readme.

fengye87 commented 10 months ago

Thanks for pointing it out. I managed to make it work. (BTW: The sample code in the README.md is flawed.) The code I'm using:

package main

import (
    "encoding/json"
    "fmt"

    "github.com/flachnetz/poly"
    "github.com/samber/lo"
)

func main() {
    bytes := lo.Must(json.Marshal(PolySpeark{Animal{
        Speaker: PolySpeark{Dog{}},
    }}))

    fmt.Println(string(bytes))

    var speaker PolySpeark
    lo.Must0(json.Unmarshal(bytes, &speaker))
    speaker.Value.Speak()
}

type Speaker interface{ Speak() }

type Dog struct{ IsDog bool }

func (Dog) Speak() {
    fmt.Println("wuff")
}

type Cat struct{ IsCat bool }

func (Cat) Speak() {
    fmt.Println("miau")
}

type Animal struct {
    Speaker PolySpeark
}

func (c Animal) Speak() {
    c.Speaker.Value.Speak()
}

type PolySpeark poly.Poly[Speaker, poly.TypeItem[Animal, poly.TypeItem[Dog, poly.TypeItem[Cat, poly.Nil]]]]

func (p PolySpeark) MarshalJSON() ([]byte, error) {
    return json.Marshal((*poly.Poly[Speaker, poly.TypeItem[Animal, poly.TypeItem[Dog, poly.TypeItem[Cat, poly.Nil]]]])(&p))
}

func (p *PolySpeark) UnmarshalJSON(bytes []byte) error {
    return json.Unmarshal(bytes, (*poly.Poly[Speaker, poly.TypeItem[Animal, poly.TypeItem[Dog, poly.TypeItem[Cat, poly.Nil]]]])(p))
}

Now the problem becomes: I have dozens of implementations of Speaker, and those last several lines would become a nightmare. Any thoughts?

oliverbestmann commented 10 months ago

It works by using a type alias and pointers. I'll need to have another look on why exactly this is the case. I've probably only implemented json.Marshaller for *Poly. I'll have a look later:

Your example with the necessary changes:


package main

import (
    "encoding/json"
    "fmt"

    "github.com/flachnetz/poly"
    "github.com/samber/lo"
)

func main() {
    bytes := lo.Must(json.Marshal(
        &PolySpeark{
            Animal{
                Speaker: &PolySpeark{Dog{}},
            },
        },
        ),
    )

    fmt.Println(string(bytes))

    var speaker PolySpeark
    lo.Must0(json.Unmarshal(bytes, &speaker))
    speaker.Value.Speak()
}

type Speaker interface{ Speak() }

type Dog struct{ IsDog bool }

func (Dog) Speak() {
    fmt.Println("wuff")
}

type Cat struct{ IsCat bool }

func (Cat) Speak() {
    fmt.Println("miau")
}

type Animal struct {
    Speaker *PolySpeark
}

func (c Animal) Speak() {
    c.Speaker.Value.Speak()
}

type PolySpeark = poly.Poly[Speaker, poly.TypeItem[Animal, poly.TypeItem[Dog, poly.TypeItem[Cat, poly.Nil]]]]
fengye87 commented 10 months ago

Type alias works in the example, but not in my real case. I have multiple implementations that is recursive. Try include a Speaker *PolySpeark field in Dog, the go compiler would complain about "invalid use of type alias" then.

I have a rough thought here. How about pushing reflection further and also taking in implementation types registration? Something like:

type Final struct {
    ...
    Poly Iface
}

var ifacePoly = poly.New[Iface]().Impls(ImplA{}, ImplB{})

func (f Final) MarshalJSON(...) ... {
    return ifacePoly.MarshalJSON(f)
}

type Iface interface {
    ...
}

type ImplA struct {
    ...
}

type ImplB struct {
    ....
    Poly Iface
}

The benefits are:

  1. All recursive fields under Final whose type is Iface would get marshaled as expected magically.
  2. If the code is in a lib, the lib user can register more impls to ifacePoly and take the magic for free.