Decoder composition
A decco decoder must composed of 4 elements:
- a function
encoder
which handle serialization - a function
decoder
which handle deserialization - a variable
codec
which contains these functions (as a tuple) - a type
t
who will be the type that we wish to decode/generate
Handle the serialization
When I need to work with a string enumeration, I tend to use the directive @bs.deriving(jsConverter)
which generate automaticaly the functions allowing the permutation between a string and a type. Here is an example :
@deriving(jsConverter)
type brand = [
| #sony
| #microsoft
| #toyota
| #apple
];
Js.log(brandToJs(#microsoft)); /* log "microsoft" */
brandFromJs("microsoft")->Belt.Option.forEach(v => Js.log(v)); /* log the generated id of the type */
A small but interesting details here is the brandFromJs
function, it returns an option(string)
type because it's possible to give an unexisting enum value and so return the None
value.
Let's keep this brand type and write the serialization function :
@deriving(jsConverter)
type brand = [
| #sony
| #microsoft
| #toyota
| #apple
];
let encoder: Decco.encoder<brand> = (brand: brand) => {
brand->brandToJs->Decco.stringToJson;
};
Here, I declared the types explicitly to make it clearer but you can also let the inference does it job !
About the encoder
function, it takes a parameter of our type we need to convert and transform it in a string
in order to invoke the Decco.stringToJson
function who will make the JSON conversion.
The serialization is handled ! Nothing more is necessary, we can move on to the deserialization !
Handle the deserialization
It's quite the same like the serialization but with the error case to handle :
let decoder: Decco.decoder<brand> = json => {
switch (json->Decco.stringFromJson) {
| Belt.Result.Ok(v) => switch (v->brandFromJs) {
| None => Decco.error(~path="", "Invalid enum " ++ v, json)
| Some(v) => v->Ok
}
| Belt.Result.Error(_) as err => err
};
};
In this example, we just need to note that Decco.stringFromJson
function return a Belt.Result.t
type and to raise an error we need to invoke the Decco.error
function.
And the rest
It remains now the 2 variables to create that will be showed like that :
let codec: Decco.codec(brand) = (encoder, decoder);
[@decco]
type t = [@decco.codec codec] brand;
We imperatively associate the right type and here we are, we have our own decoder ! Let's bring this together into a module :
module BrandCodec = {
@deriving(jsConverter)
type brand = [
| #sony
| #microsoft
| #toyota
| #apple
]
let encoder: Decco.encoder<brand> = (brand: brand) => {
brand->brandToJs->Decco.stringToJson;
}
let decoder: Decco.decoder<brand> = json => {
switch (json->Decco.stringFromJson) {
| Belt.Result.Ok(v) => switch (v->brandFromJs) {
| None => Decco.error(~path="", "Invalid enum " ++ v, json)
| Some(v) => v->Ok
}
| Belt.Result.Error(_) as err => err
}
}
let codec: Decco.codec<brand> = (encoder, decoder)
@decco
type t = @decco.codec(codec) brand
};
We can now use the t
type without using @decco.codec
:
/*...*/
@decco
type console = {
id: string,
name: string,
brand: BrandCodec.t
};