ppx_spice icon indicating copy to clipboard operation
ppx_spice copied to clipboard

Customize encode/decode of variants with arguments

Open pd4d10 opened this issue 2 years ago • 9 comments

Hi, thanks for the work! Saves a lot of boilerplate code.

In my case, there is a little problem that the JSON's structure is more like this (described as TypeScript):

type File = {
  type: "file",
  size: number
}

type Directory = {
  type: "directory",
  files: File[]
}

type Item = File | Directory

The type key indicates the variant type, and the other fields are treated as arguments.

While the default encode/decode strategy is to convert to an array, as this example shows:

https://github.com/green-labs/ppx_spice/blob/eab7239dd60133b78144a35107e09e395b519c4a/examples/src/Variants2.res#L19

Is it possible to support this pattern?

pd4d10 avatar Dec 15 '22 03:12 pd4d10

Not sure I understand correctly, If you mean this:

{
   "files": [{"type": "file", "size": 1}, ... ]
}

decoded to the variant with arguement? It is not supported. But you can write your own codec to (d)encode. https://github.com/green-labs/ppx_spice/blob/main/examples/src/CustomCodecs.res

mununki avatar Dec 15 '22 04:12 mununki

Yeah, maybe it's clearer to split it into two topics:

The first topic is about customized encode/decode, as the title suggests. For example:

type file = { size: int }
type directory = { ... } // ignore it for now
type item = File(file) | Directory(directory)

@spice
type payload = {
  item: item
}

let payload = {
  item: File({ size: 1 })
}

will be encoded to:

{
  item: ["File", { size: 0 }]
}

While the desired result is:

{
  item: {
    type: "file"
    size: 0
  }
}

pd4d10 avatar Dec 15 '22 04:12 pd4d10

The second topic is about recursively decoding, for example, Directory's arguments would have another Directory or File. I didn't test it since I'm currently stuck at the first one. Will add more details in the future.

pd4d10 avatar Dec 15 '22 04:12 pd4d10

Did you write a custom codecs for (d)encode? Can you share your codecs?

mununki avatar Dec 16 '22 03:12 mununki

Spent some time to try the custom codecs, got errors: The value t_encode can't be found

module Meta = {
  @spice.codec(codecT)
  type rec t = File(string, file) | Directory(string, directory)
  @spice and file = {size: int}
  @spice and directory = {files: array<t>}

  let encoderT = t => {
    // ignore it for now
    Js.Json.parseExn("")
  }

  let decoderT = json => {
    switch json->Js.Json.classify {
    | Js.Json.JSONObject(obj) => {
        let path = obj->Js.Dict.get("path")->Option.flatMap(Js.Json.decodeString)->Option.getExn
        let type_ = obj->Js.Dict.get("type")->Option.flatMap(Js.Json.decodeString)->Option.getExn

        switch type_ {
        | "file" => File(path, json->file_decode->Result.getExn)->Ok
        | "directory" => Directory(path, json->directory_decode->Result.getExn)->Ok
        | _ => assert false
        }
      }
    | _ => assert false
    }
  }

  let codecT: Spice.codec<t> = (encoderT, decoderT)
}

JSON Examples:

{
  type: "directory",
  path: "/directory",
  files: [
    {
      type: "file",
      path: "/file-1",
      size: 1
    },
    {
      type: "file",
      path: "/file-2",
      size: 2
    },
  ]
}

pd4d10 avatar Dec 18 '22 08:12 pd4d10

c@pd4d10 Isn't there another parent-level field of JSON example? If it does, such as "data" in {"data": { "type": ... }}, you could write it as below:

module Meta = {
  @spice
  type rec t = File(string, file) | Directory(string, directory)
  @spice and file = {size: int}
  @spice and directory = {files: array<t>}

  let encoderT = t => {
    // ignore it for now
    let _ = t
    Js.Json.parseExn("")
  }

  let decoderT = json => {
    switch json->Js.Json.classify {
    | Js.Json.JSONObject(obj) => {
        let path = obj->Js.Dict.get("path")->Option.flatMap(Js.Json.decodeString)->Option.getExn
        let type_ = obj->Js.Dict.get("type")->Option.flatMap(Js.Json.decodeString)->Option.getExn

        switch type_ {
        | "file" => File(path, json->file_decode->Result.getExn)->Ok
        | "directory" => Directory(path, json->directory_decode->Result.getExn)->Ok
        | _ => assert false
        }
      }

    | _ => assert false
    }
  }

  let codecT: Spice.codec<t> = (encoderT, decoderT)

  @spice
  type data = {data: @spice.codec(codecT) t}
}

mununki avatar Dec 20 '22 04:12 mununki

I think it is a recursive reference issue between type definition and value binding.

mununki avatar Dec 20 '22 04:12 mununki

Isn't there another parent-level field

Unfortunately, there isn't

it is a recursive reference issue between type definition and value binding.

Get it. It seems to be a language limitation that the type rec ... and pattern is only for types.

pd4d10 avatar Dec 20 '22 07:12 pd4d10

I don't know about the required spec, but maybe it is a matter of data modeling. Maybe we can model the data as:

@spice
type type_ = @spice.as("file") File | @spice.as("directory") Directory
@spice
type file = {size: int}
@spice
type directory = array<file>
@spice
type t = {
	@spice.key("type") type_,
	path: string,
	files?: directory,
	file?: file
}

mununki avatar Dec 20 '22 15:12 mununki