ugorji / go

idiomatic codec and rpc lib for msgpack, cbor, json, etc. msgpack.org[Go]
MIT License
1.85k stars 295 forks source link

Inline interface #287

Closed pyroxymat closed 5 years ago

pyroxymat commented 5 years ago

I was wondering if it is possible to inline interfaces/structs like this:

type jsonHeader struct {
    Header  Header      `codec:",inline"`
    Message interface{} `codec:",inline"`
}

Such that header and the message contents are on the same level.

Thus if the header and message struct were defined as follows:

type Header struct {
    id string
}

type EchoMessage struct {
    contents string
}

I would expect the following output after encoding it with json:

{"id": "my_id", "contents": "my_contents"}

EDIT: After some more thought I guess the issue is mainly in the decoding part since you don't know what the type of the message is when you decode it.

In my code so far I fixed this by running decode twice: 1 time to get the header (which contains the type) and then switching on the type and decoding the same JSON again for the message. I do not have a good solution on how to solve this while allowing inline encoding.

    decoder.ResetBytes(bytes)

    var header Header
    err := decoder.Decode(&header)
    if err != nil {
        return err
    }

    // We reset the decoder again because we incremented how much we read the last time
    decoder.ResetBytes(bytes)

    var msg Message
    switch header.MsgType {
    case MessageTypeEcho:
        msg = new(EchoMessage)
        // More message types here
    }
    decoder.Decode(&msg)
ugorji commented 5 years ago

what you can do is:

type Header struct {
    Id string
}

type EchoMessage struct {
    Contents string
}

type jsonHeader struct {
    Header  
    EchoMessage 
}

And they will be encoded as, and can be decoded from:

{"Id": "my_id", "Contents": "my_contents"}

We take advantage of go's support for embedded/anonymous types here.

But you will need to work within this constraint to design your usecase.

See https://godoc.org/github.com/ugorji/go/codec#Encoder.Encode (and search for anonymous)

ugorji commented 5 years ago

P.S. interfaces cannot be inlined, because we cannot know the structure until runtime, and the structure may change at runtime. This is specified in the docs.

ugorji commented 5 years ago

@wheldring ^^

Please respond here if/as necessary.

pyroxymat commented 5 years ago

Thanks for your help @ugorji. The main reason I cannot use the first solution is because there might be 40 messages, so that means I need to write 40 different headers.

In the end I solved my problem by writing a custom encoder like this:

func (h jsonHeader) CodecEncodeSelf(enc *codec.Encoder) {
    out := make(map[string]interface{})

    out["msg_type"] = h.Header.MsgType

    val := reflect.Indirect(reflect.ValueOf(h.Message))

    typ := val.Type()
    for i := 0; i < typ.NumField(); i++ {
        field := typ.Field(i)
        name := typ.Field(i).Name

        out[field.Tag.Get("codec")] = val.FieldByName(name).Interface()
    }

    enc.MustEncode(out)
}

With the decoder as said in the main message. Although it is not beautiful it works. I guess I can clean it up by writing a custom decoder to be even more idiomatic.