rescript-labs / decco

Bucklescript PPX which generates JSON (de)serializers for user-defined types
MIT License
228 stars 27 forks source link

Support polymorphic variants? #39

Closed TomiS closed 3 years ago

TomiS commented 4 years ago

Writing this

[@decco]
type foo = [ | `bar | `baz];

gives an error: "This syntax is not yet handled by decco"

I assume this is not surprising to anyone. Just creating a ticket to show my interest in this feature. :)

TomiS commented 4 years ago

In case someone is searching for the same thing, it is possible to do this by using custom codecs. Here's one example that converts locale strings to polyvariants

module LocaleAsString = {
  type t = [ | `en | `fi | `sv | `pt];

  let t_encode: Decco.encoder(t) =
    locale =>
      switch (locale) {
      | `en => Js.Json.string("en")
      | `fi => Js.Json.string("fi")
      | `sv => Js.Json.string("sv")
      | `pt => Js.Json.string("pt")
      };

  let t_decode: Decco.decoder(t) =
    json =>
      switch (Js.Json.classify(json)) {
      | Js.Json.JSONString(str) =>
        switch (str) {
        | "en" => `en->Belt.Result.Ok
        | "fi" => `fi->Belt.Result.Ok
        | "sv" => `sv->Belt.Result.Ok
        | "pt" => `sv->Belt.Result.Ok
        | _ => Decco.error("Locale '" ++ str ++ "' is not supported.", json)
        }
      | _ =>
        Decco.error(
          "Trying to decode a field that is not a string to locale",
          json,
        )
      };
};

And usage in the type definition

[@decco]
type myRecord = {
  locale: LocaleAsString.t,
};
kittipatv commented 4 years ago

There is a less verbose way to do this. According to BuckleScript doc, [@bs.deriving jsConverter] can be used to convert polymorphic variant to JS string enum. Quoting from the doc:

[@bs.deriving jsConverter]
type fruit = [
  | `Apple
  | [@bs.as "miniCoconut"] `Kiwi
  | `Watermelon
];

let appleString = fruitToJs(`Apple); /* "Apple" */
let kiwiString = fruitToJs(`Kiwi); /* "miniCoconut" */

Those functions can be used as the base for conversion. In DeccoHelper.re

open Belt;

let encode = (x, ~tToJs) => tToJs(x) |> Decco.stringToJson;
let decode = (x, ~tFromJs, ~name) =>
  Decco.stringFromJson(x)
  ->Result.map(tFromJs)
  ->Result.flatMap(y =>
      switch (y) {
      | Some(value) => Ok(value)
      | None => Decco.error("Unknown " ++ name, x)
      }
    );

Then, we can use the helpers like this in Fruit.re (I changed fruit type to t, just for convenience):

[@bs.deriving jsConverter]
type t = [
  | `Apple
  | [@bs.as "miniCoconut"] `Kiwi
  | `Watermelon
];

let t_encode = DeccoHelper.encode(~tToJs);
let t_decode = DeccoHelper.decode(~tFromJs, ~name="Fruit");
kittipatv commented 4 years ago

This can be made more convenient with functor. Add this to DeccoHelper.re

module type PolymorphicVariantWithJsConverter = {
  type t;
  let tToJs: t => string;
  let tFromJs: Js.String.t => option(t);
  let name: string;
};

module MakePV = (PV: PolymorphicVariantWithJsConverter) => {
  include PV;

  let t_encode = encode(~tToJs);
  let t_decode = decode(~tFromJs, ~name);
};

Then, you can

module Fruit = DeccoHelper.MakePV({
  [@bs.deriving jsConverter]
  type t = [
    | `Apple
    | [@bs.as "miniCoconut"] `Kiwi
    | `Watermelon
  ];
  let name = "Fruit";
});
TomiS commented 4 years ago

@kittipatv Nice. That is indeed much more elegant way to do it.

yawaramin commented 4 years ago

However note that jsConverter does not support polymorphic variants with payloads.

farukg commented 4 years ago

Just to inform: maybe this got much simpler at least for polyvars without payload, i guess, since bucklescript 8.2 and string literals.

// any polyvar like 
let c= `color; 
// will compile to just a normal string in Javascript
var c = "color"

source: https://reasonml.org/blog/string-literal-types-in-reason

davesnx commented 3 years ago

I'm happy to work on that @ryb73. Is there anything that I should be aware of beforehand?

Thanks

ryb73 commented 3 years ago

Thanks @davesnx! There are a couple general tips here: https://github.com/reasonml-labs/decco/issues/25#issuecomment-561949183 https://github.com/reasonml-labs/decco/issues/6#issuecomment-511057326 PPXs are confusing, so feel free to ask questions

Pet3ris commented 3 years ago

@kittipatv is there a good workaround for polyvariants with variable tags?

E.g.,

type t = [#ENTERS | #REGULAR | #RETURNS | #FutureAddedValue(string)]

Current workaround I've used is just to manually encode these cases:

module Jump = DeccoHelper.MakePV({
  @bs.deriving(jsConverter)
  type t = [#ENTERS | #REGULAR | #RETURNS | #FutureAddedValue(string)]
  let tToJs = (val: t): string => switch val {
    | #ENTERS => "ENTERS"
    | #REGULAR => "REGULAR"
    | #RETURNS => "RETURNS"
    | #FutureAddedValue(value) => value
  }
  let tFromJs = (s: Js.String.t ): option<t> => switch s {
    | "ENTERS" => Some(#ENTERS)
    | "REGULAR" => Some(#REGULAR)
    | "RETURNS" => Some(#RETURNS)
    | other => Some(#FutureAddedValue(other))
  }
  let name = "Jump"
})
davesnx commented 3 years ago

Last version v1.4.0 supports polyvariants after merging https://github.com/reasonml-labs/decco/pull/64

Let me know if you found any issue with that

benadamstyles commented 3 years ago

I'm quite confused by this, I must be missing something obvious, but v1.4.0 only successfully decodes arrays to poly variants, not strings.

So the following fails:

{
  "format": "abc"
}
@decco.decode
type t = {
  format: [#abc]
}

But the following works:

{
  "format": ["abc"]
}
@decco.decode
type t = {
  format: [#abc]
}

Can this be right?

ryb73 commented 3 years ago

@benadamstyles This is correct. I think what you're looking for is described in #36

benadamstyles commented 3 years ago

@ryb37 Thanks for the quick response! I still don't understand – why are polyvariants, which are single values, being encoded to arrays?

ryb73 commented 3 years ago

Basically it's because they're not necessarily single values. For example (in Reason syntax because I'm not familiar with ReScript):

[@decco] type v = `something(int, string);
`something(123, "abc") |> v_encode; /* result: ["something", 123, "abc"] */

Part of me wishes in retrospect I'd have made it so that variants without attached data would be encoded to strings instead of arrays, but unfortunately that ship has sailed. I don't use Reason anymore so I'm not really doing any more Decco development myself, but would be happy to take a PR for issue #36.

benadamstyles commented 3 years ago

Ok thanks, I understand now. I'm not sure how #36 would help because that's about renaming, if I understand correctly, whereas I need a completely different representation... but if that's a breaking change I don't see any good way forward so I'll stick with manual decoders for now. Thanks again for the quick responses!

ryb73 commented 3 years ago

Hmm, it's possible I misunderstood what @mrmurphy was requesting, but my interpretation of #36 is that it would use a string instead of an array