Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to use multiple types #73

Open
dignifiedquire opened this issue Mar 8, 2018 · 6 comments
Open

Ability to use multiple types #73

dignifiedquire opened this issue Mar 8, 2018 · 6 comments

Comments

@dignifiedquire
Copy link
Member

Sometimes commands have different outputs depending on the flags, but I would still like to get type safety using MakeTypedEncoder. So I had the idea to allow for the following interface

var myCmd = &cmds.Command{
  // ... 
  Types: []interface{type1, type2},
  Encoders: cmds.EncoderMap{
    cmds.Text: []Encoder{
      MakeTypedEncoder(func(req *cmds.Request, w io.Writer, t1 type1) error {
        // encode t1
      }),
      MakeTypedEncoder(func(req *cmds.Request, w io.Writer, t2 type2) error {
        // encode t2
      }),
    }
}

Where the library would go through and find the correct encoder given a certain type.

In addition I am seeing instances where I would really like to have default encoders registered for certain types, or for certain interfaces. So with the above a nice addition would be to be able to say register these encoders in a global fashion on cmds and then they are used as fallback if no matching typed encoder is found.

cmds.RegisterTypedEncoder(MakeTypedEncoder(func(req *cmds.Request, w io.Writer, t1 type1) error {
  // encode t1
}))
cmds.RegisterTypedEncoder(MakeTypedEncoder(func(req *cmds.Request, w io.Writer, t2 type2) error {
  // encode t2
}))

var myCmd = &cmds.Command{
  // ... 
  Types: []interface{type1, type2},
  // Encoders is not here, but it still works, because the encoders for type 1 and type 2 have been registered before
}
@dignifiedquire dignifiedquire changed the title Ability to use multiplye types Ability to use multiple types Mar 8, 2018
@keks
Copy link
Contributor

keks commented Apr 16, 2018

Hey @dignifiedquire, I opened a PR that addresses part of what you stated: #99. However, your suggestion lacks the most tricky part of the problem: How does the Decoder know what type to use?

The cleanest approach IMHO is to give hints in the JSON, such as

{
  "type": "fooType",
  "value": {
    // ...
  }
}

This is probably not what you meant, but we already have big problems telling a value and an error apart.

The decoding part is not yet in the PR, but I'm working on it.

@keks
Copy link
Contributor

keks commented Apr 16, 2018

Actually, before I dash ahead, let me know if you want JSON that looks like this.

@dignifiedquire
Copy link
Member Author

Generally I think having some type information is a good way to go, but this would mean the json would be very different in the multi case vs the singular case. One hacky way I could see is to try the different types, and use the one that doesn't fail, with the first one to succeed being the one that is used.

@keks
Copy link
Contributor

keks commented Apr 16, 2018

use the one that doesn't fail

Well, JSON decoding in Go rarely fails. It only fails if the JSON is invalid or if the types don't match, e.g. you unmarshal a string into an int. Missing or superfluous fields don't make the parsing fail. Also it's very hard to see whether a field has been set in the JSON if you don't decode into a map[string]interface{} (which is also hacky).

I was also thinking about using values like

{
  "type": "fooType",
  // ...
}

i.e. instead of capsuling the value just extend with a type field. That would be reasonable easy to parse, but difficuly to create generically: https://play.golang.org/p/IK7-3Ccp8IC so we would need a wrapper type for every concrete type. That sucks. Alternatively, we could encode, then decode into a map, extent the map with a type field, then encode the map. That is much more hacky than anything else and more of a polemic than a suggestion.

In general, this can not be used for existing commands anyway because it breaks the API. And when we design a new API (i.e. over general sockets so we can use ws://) we'll probably want to add type hints in all cases, because that makes things easier to work with.

I really don't know where to go from here. We could just leave it to the caller, who can always put a type with custom UnmarshalJSON method into Type field. But yeah, with all that baggage from the past, this is really difficult to do cleanly.

@keks
Copy link
Contributor

keks commented Apr 16, 2018

Crazy idea: fork https://github.com/mailru/easyjson, which generates type-specific json marshaling and unmarshaling code, to include a type field in the generated json and make it check that field upon parsing. This feels like two really small changes and tracking their master seems possible, but a lot of effort. Also having to generate code is inconvenient.

@keks
Copy link
Contributor

keks commented Apr 16, 2018

Actually if we start generating code it's easy, we just need to build structs that wrap our types. To give an example, consider the (made up) message type AddStatus:

// hand-written
type AddStatus struct {
  Name string
  Hash string
  Size int
  Completed int
}

// generated
type AddStatusMsg struct {
  AddStatus
  Type string
}

I mean we could also write that by hand, it's probably faster than typing the generator line and running it. But it just feels stupid writing code like that by hand.

@keks keks mentioned this issue Oct 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants