ppx_spice
ppx_spice copied to clipboard
Customize encode/decode of variants with arguments
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?
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
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
}
}
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.
Did you write a custom codecs for (d)encode? Can you share your codecs?
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
},
]
}
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}
}
I think it is a recursive reference issue between type definition and value binding.
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.
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
}