rescript-labs / decco

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

Extending with custom decoders. #11

Closed mrmurphy closed 5 years ago

mrmurphy commented 5 years ago

I'd like to decode a JSON string into a date, so I've used [@decco.codec ...] and passed in a custom decoder I've written:

[@decco]
type t = {
  kind: string,
  name: string,
  [@decco.codec DeccoExt.Date.codec]
  modified: Js.Date.t,
  id: string,
};

But I get a compiler error that "Js.Date.t_encode" does not exist. It seems like the part where I specify the codec isn't working? In order to get around this, I've got to create aliases at the top of the file:

type date = Js.Date.t;
let date_encode = DeccoExt.Date.encoder;
let date_decode = DeccoExt.Date.decoder;

[@decco]
type t = {
  kind: string,
  name: string,
  [@decco.codec DeccoExt.Date.codec]
  modified: date,
  id: string,
};

Am I doing something wrong with @decco.codec?

mrmurphy commented 5 years ago

Ah, if I make my own module and use [@decco.codec] on the type declaration it works:

--- DeccoExt.re

module Date = {
  let encoder: Decco.encoder(Js.Date.t) =
    date => Js.Date.toISOString(date)->Decco.stringToJson;
  let decoder: Decco.decoder(Js.Date.t) =
    json => {
      switch (Decco.stringFromJson(json)) {
      | Result.Ok(v) => Js.Date.fromString(v)->Ok
      | Result.Error(_) as err => err
      };
    };
  let codec: Decco.codec(Js.Date.t) = (encoder, decoder);
  [@decco]
  type t = [@decco.codec codec] Js.Date.t;
};

--- Other File:

[@decco]
type t = {
  kind: string,
  name: string,
  modified: DeccoExt.Date.t,
  id: string,
};

Kind of a bummer that I have to use the type alias though, instead of a normal Js.Date.t, no?

ryb73 commented 5 years ago

The problem is that the annotation needs to go before the field type rather than before the name of the field:

[@decco]
type t = {
  kind: string,
  name: string,
  modified: [@decco.codec DeccoExt.Date.codec] Js.Date.t,
  id: string,
};

I'll leave this open as a reminder to call this out more explicitly in the docs.

mrmurphy commented 5 years ago

Riiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiight okay. Thanks!

mrmurphy commented 5 years ago

I just ran into the need for a Dict. I see that's an outstanding issue. https://github.com/ryb73/ppx_decco/issues/4

I thought I'd just use a custom decoder in the mean-time, but I realized that dict is a higher-order decoder, taking a sub-decoder like list and array do. Is there a way to use [@decco.codec] and provide a higher-order decoder? the type signatures aren't quite matching up for me, since the codec for a dict would roughly be ((encoder, a) => json, (decoder, json) => result(a))

mrmurphy commented 5 years ago

I tried dropping the type signatures to see if I could make the encoder the same way Decco does for its list and array types:

module Dict = {
  let encoder = Json.Encode.dict;

  let decoder = (insideDecoder, json) => {
    switch (Json.Decode.dict(insideDecoder, json)) {
    | exception (Json.Decode.DecodeError(e)) =>
      Result.Error({Decco.path: "unavailable", message: e, value: json})
    | v => Result.Ok(v)
    };
  };

  let codec = (encoder, decoder);

  [@decco]
  type t('a) = [@decco.codec codec] Js.Dict.t('a);
};

But I get the cryptic type error:

[27/162] Building src/DeccoExt.cmj
  We've found a bug for you!
  (No file name)

  This expression has type Js.Json.t
  It is not a function.
mrmurphy commented 5 years ago

@ryb73 any thoughts on writing custom implementations for higher-order decoders? I just ran into this again, trying to add support for a non-empty list implementation in my decco type. Is it currently possible? Or would it require some serious code changes?

ryb73 commented 5 years ago

Hey @mrmurphy, sorry for the delay.

I just tried the Dict example that you posted and it actually did work for me. Can you try it again and let me know if there are still problems? Maybe it accidentally got fixed 😄

As a side note, I've never seen that exception syntax and am curious how it works – do you know if there's documentation on that somewhere?

Re: the discussion earlier in the thread, I added a section to the readme which describes the attributes that decco recognizes. I understand that it's confusing that sometimes the [@decco.<xyz>] attribute comes before the record field name and sometimes comes after. I'm hoping that with the documentation I added it's more clear why that's the case.

Finally, I realized that you're the host of Reason Town. I love the podcast, I hope you guys keep it up!

mrmurphy commented 5 years ago

Welp, I think I figured out my problem. In the case of the Dict example above, and all other times I've tried this, I left off the type declarations, and added an extra argument to the decoder for the inner decoder, but I forgot to add an extra argument for the inner encoder. Once I added an extra arg to both the encoder and the decoder, the compiler was happy with me again.

For example:

module Loadable = {
  type _t('a) =
    | NotLoaded
    | Loaded('a);

  let encoder = (_, _) => Obj.magic(Js.Undefined.empty);

  let decoder = (_, _) => Result.Ok(NotLoaded);

  let codec = (encoder, decoder);

  [@decco]
  type t('a) = [@decco.codec codec] _t('a);
};