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

Convert from {"type": "foo", ...} json data to variant values #47

Open
jchavarri opened this issue Dec 30, 2019 · 3 comments
Open

Convert from {"type": "foo", ...} json data to variant values #47

jchavarri opened this issue Dec 30, 2019 · 3 comments

Comments

@jchavarri
Copy link
Contributor

jchavarri commented Dec 30, 2019

Is there a simple way to convert from values like

{"type": "Image", "url": "https://example.com/ocean123.jpg"}
{"type": "Text", "title": "Cheeses Around the World", "body": "..."}

to variant values like

["Image", {"url": "https://example.com/ocean123.jpg"}]
["Text", {"title": "Cheeses Around the World", "body": "..."}]

In atdgen, there is a directive called ocaml.adapter that is similar to decco.codec but runs a bit earlier, and instead converts from Json.t to Json.t, just to do some preliminary massaging to the json values so they can be parsed properly.

Maybe something like this could exist in decco:

[@decco {adapter: (normalize, restore)}]
type t =
  | Image(image)
  | Text(string);

With a provided module to convert from json values where type key drives the variant used:

[@decco {adapter: TypeField.adapt}]
type t =
  | Image(image)
  | Text(string);

The TypeField module is also inspired in atdgen, which provides a similar thing out of the box: https://github.com/ahrefs/bs-atdgen-codec-runtime/blob/a40203875041e49bb78d2a46cb77f4b637865bae/src/atdgen_codec_runtime.ml#L15-L57

@jchavarri
Copy link
Contributor Author

I managed to do something with custom enc/decoders:

[@decco]
type image = {url: string};

[@decco]
type text = {
  title: string,
  body: string,
};

type document =
  | Image(image)
  | Text(text);

let encoder = doc =>
  switch (doc) {
  | Image(img) =>
    let obj = image_encode(img)->Js.Json.decodeObject->Option.getExn;
    Js.Dict.set(obj, "type", "image"->Js.Json.string);
    Js.Json.object_(obj);
  | Text(txt) =>
    let obj = text_encode(txt)->Js.Json.decodeObject->Option.getExn;
    Js.Dict.set(obj, "type", "text"->Js.Json.string);
    Js.Json.object_(obj);
  };

let decoder = json => {
  switch (json |> Js.Json.classify) {
  | JSONObject(obj) =>
    switch (
      Js.Dict.get(obj, "type")->Option.flatMap(Js.Json.decodeString),
      image_decode(json),
      text_decode(json),
    ) {
    | (Some(type_), Ok(img), _) when type_ == "image" => Image(img)->Ok
    | (Some(type_), _, Ok(txt)) when type_ == "text" => Text(txt)->Ok
    | _ => Error({ Decco.path: "", message: "Not a document", value: json })
    }
  | _ => Error({ Decco.path: "", message: "Expected JSONObject for document", value: json })
  };
};

let codec: Decco.codec(document) = (encoder, decoder);

[@decco]
type t = [@decco.codec codec] document;

But it's probably too convoluted...

@TomiS
Copy link

TomiS commented Feb 4, 2020

This is good stuff. Converting an object with a discriminator field (often called type) to a variant holding a record seems to be a really common pattern. I'm already in the process of making two custom decoders for exactly identical case. I also noticed it's quite error prone and decoders become quite bloated easily. So this would certainly be an area where improvement would probably be welcome.

@ryb73
Copy link
Member

ryb73 commented Feb 13, 2020

Interesting thought. I feel like this is along the same lines as some of the discussion in #30.

In the meantime, I think your example can be simplified slightly:

open Belt;

[@decco]
type discriminator = {
  [@decco.key "type"]
  type_: string,
};

[@decco]
type image = {
  [@decco.key "type"]
  type_: string,
  url: string,
};

[@decco]
type text = {
  [@decco.key "type"]
  type_: string,
  title: string,
  body: string,
};

type document =
  | Image(image)
  | Text(text);

let encoder = doc =>
  switch (doc) {
  | Image(img) => image_encode(img)
  | Text(txt) => text_encode(txt)
  };

[@warning "-4"]
let decoder = json =>
    switch (json |> discriminator_decode) {
    | Ok({type_: "image"}) => image_decode(json)->Result.map(v => Image(v))
    | Ok({type_: "text"}) => text_decode(json)->Result.map(v => Text(v))
    | _ =>
      Decco.error(
        "Expected JSON object with type of ['image' | 'text']",
        json,
      )
    };

let codec: Decco.codec(document) = (encoder, decoder);

[@decco]
type t = [@decco.codec codec] document;

Another option if you want straight json->json adapters is to do something like

[@decco]
type t =
  | Image(image)
  | Text(string);
let t_encode = (t) => t_encode(t) -> normalize;
let t_decode = (json) => t_decode(json) -> Result.map(restore);

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

3 participants