Closed TomiS closed 3 years ago
In case someone is searching for the same thing, it is possible to do this by using custom codecs. Here's one example that converts locale strings to polyvariants
module LocaleAsString = {
type t = [ | `en | `fi | `sv | `pt];
let t_encode: Decco.encoder(t) =
locale =>
switch (locale) {
| `en => Js.Json.string("en")
| `fi => Js.Json.string("fi")
| `sv => Js.Json.string("sv")
| `pt => Js.Json.string("pt")
};
let t_decode: Decco.decoder(t) =
json =>
switch (Js.Json.classify(json)) {
| Js.Json.JSONString(str) =>
switch (str) {
| "en" => `en->Belt.Result.Ok
| "fi" => `fi->Belt.Result.Ok
| "sv" => `sv->Belt.Result.Ok
| "pt" => `sv->Belt.Result.Ok
| _ => Decco.error("Locale '" ++ str ++ "' is not supported.", json)
}
| _ =>
Decco.error(
"Trying to decode a field that is not a string to locale",
json,
)
};
};
And usage in the type definition
[@decco]
type myRecord = {
locale: LocaleAsString.t,
};
There is a less verbose way to do this. According to BuckleScript doc, [@bs.deriving jsConverter]
can be used to convert polymorphic variant to JS string enum. Quoting from the doc:
[@bs.deriving jsConverter]
type fruit = [
| `Apple
| [@bs.as "miniCoconut"] `Kiwi
| `Watermelon
];
let appleString = fruitToJs(`Apple); /* "Apple" */
let kiwiString = fruitToJs(`Kiwi); /* "miniCoconut" */
Those functions can be used as the base for conversion. In DeccoHelper.re
open Belt;
let encode = (x, ~tToJs) => tToJs(x) |> Decco.stringToJson;
let decode = (x, ~tFromJs, ~name) =>
Decco.stringFromJson(x)
->Result.map(tFromJs)
->Result.flatMap(y =>
switch (y) {
| Some(value) => Ok(value)
| None => Decco.error("Unknown " ++ name, x)
}
);
Then, we can use the helpers like this in Fruit.re
(I changed fruit
type to t
, just for convenience):
[@bs.deriving jsConverter]
type t = [
| `Apple
| [@bs.as "miniCoconut"] `Kiwi
| `Watermelon
];
let t_encode = DeccoHelper.encode(~tToJs);
let t_decode = DeccoHelper.decode(~tFromJs, ~name="Fruit");
This can be made more convenient with functor. Add this to DeccoHelper.re
module type PolymorphicVariantWithJsConverter = {
type t;
let tToJs: t => string;
let tFromJs: Js.String.t => option(t);
let name: string;
};
module MakePV = (PV: PolymorphicVariantWithJsConverter) => {
include PV;
let t_encode = encode(~tToJs);
let t_decode = decode(~tFromJs, ~name);
};
Then, you can
module Fruit = DeccoHelper.MakePV({
[@bs.deriving jsConverter]
type t = [
| `Apple
| [@bs.as "miniCoconut"] `Kiwi
| `Watermelon
];
let name = "Fruit";
});
@kittipatv Nice. That is indeed much more elegant way to do it.
However note that jsConverter does not support polymorphic variants with payloads.
Just to inform: maybe this got much simpler at least for polyvars without payload, i guess, since bucklescript 8.2 and string literals.
// any polyvar like
let c= `color;
// will compile to just a normal string in Javascript
var c = "color"
source: https://reasonml.org/blog/string-literal-types-in-reason
I'm happy to work on that @ryb73. Is there anything that I should be aware of beforehand?
Thanks
Thanks @davesnx! There are a couple general tips here: https://github.com/reasonml-labs/decco/issues/25#issuecomment-561949183 https://github.com/reasonml-labs/decco/issues/6#issuecomment-511057326 PPXs are confusing, so feel free to ask questions
@kittipatv is there a good workaround for polyvariants with variable tags?
E.g.,
type t = [#ENTERS | #REGULAR | #RETURNS | #FutureAddedValue(string)]
Current workaround I've used is just to manually encode these cases:
module Jump = DeccoHelper.MakePV({
@bs.deriving(jsConverter)
type t = [#ENTERS | #REGULAR | #RETURNS | #FutureAddedValue(string)]
let tToJs = (val: t): string => switch val {
| #ENTERS => "ENTERS"
| #REGULAR => "REGULAR"
| #RETURNS => "RETURNS"
| #FutureAddedValue(value) => value
}
let tFromJs = (s: Js.String.t ): option<t> => switch s {
| "ENTERS" => Some(#ENTERS)
| "REGULAR" => Some(#REGULAR)
| "RETURNS" => Some(#RETURNS)
| other => Some(#FutureAddedValue(other))
}
let name = "Jump"
})
Last version v1.4.0 supports polyvariants after merging https://github.com/reasonml-labs/decco/pull/64
Let me know if you found any issue with that
I'm quite confused by this, I must be missing something obvious, but v1.4.0 only successfully decodes arrays to poly variants, not strings.
So the following fails:
{
"format": "abc"
}
@decco.decode
type t = {
format: [#abc]
}
But the following works:
{
"format": ["abc"]
}
@decco.decode
type t = {
format: [#abc]
}
Can this be right?
@benadamstyles This is correct. I think what you're looking for is described in #36
@ryb37 Thanks for the quick response! I still don't understand – why are polyvariants, which are single values, being encoded to arrays?
Basically it's because they're not necessarily single values. For example (in Reason syntax because I'm not familiar with ReScript):
[@decco] type v = `something(int, string);
`something(123, "abc") |> v_encode; /* result: ["something", 123, "abc"] */
Part of me wishes in retrospect I'd have made it so that variants without attached data would be encoded to strings instead of arrays, but unfortunately that ship has sailed. I don't use Reason anymore so I'm not really doing any more Decco development myself, but would be happy to take a PR for issue #36.
Ok thanks, I understand now. I'm not sure how #36 would help because that's about renaming, if I understand correctly, whereas I need a completely different representation... but if that's a breaking change I don't see any good way forward so I'll stick with manual decoders for now. Thanks again for the quick responses!
Hmm, it's possible I misunderstood what @mrmurphy was requesting, but my interpretation of #36 is that it would use a string instead of an array
Writing this
gives an error: "This syntax is not yet handled by decco"
I assume this is not surprising to anyone. Just creating a ticket to show my interest in this feature. :)