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

Customize encode/decode of variants with arguments #35

Open
pd4d10 opened this issue Dec 15, 2022 · 9 comments
Open

Customize encode/decode of variants with arguments #35

pd4d10 opened this issue Dec 15, 2022 · 9 comments

Comments

@pd4d10
Copy link

pd4d10 commented Dec 15, 2022

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:

"language": ["ReScript", "awesome"]

Is it possible to support this pattern?

@mununki
Copy link
Member

mununki commented Dec 15, 2022

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

@pd4d10
Copy link
Author

pd4d10 commented Dec 15, 2022

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
Copy link
Author

pd4d10 commented Dec 15, 2022

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.

@mununki
Copy link
Member

mununki commented Dec 16, 2022

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

@pd4d10
Copy link
Author

pd4d10 commented Dec 18, 2022

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
    },
  ]
}

@mununki
Copy link
Member

mununki commented Dec 20, 2022

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
Copy link
Member

mununki commented Dec 20, 2022

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

@pd4d10
Copy link
Author

pd4d10 commented Dec 20, 2022

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.

@mununki
Copy link
Member

mununki commented Dec 20, 2022

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
}

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