Decoding
Understand and create new YAML decoders.
Here you’ll find some useful information about the design of the YAML decoders.
Overview
Decoding the database is splitted into parsing the YAML files and decoding the result. Parsing is done by the Yaml_Parse
module and the Yaml_Decode
module controls the decoding of the various entry types. The Yaml
module then combines all functions into a single exported function.
Decode Entry Types
Decoder structure
All decodable main types share a common decoder layout. All values only relevant while decoding are inside a Decode
module at the same level as the type it is the decoder for.
(** Since this is the actual type, it's not really part of the decoding pipeline
but I'll keep it here to make the definitions complete. *)
type t = {
(* ... *)
}
module Decode = struct
open Json.Decode
open JsonStrict
type translation = { (* ... language-specific properties *) }
let translation json =
{
(* ... decode language-specific properties *)
}
type multilingual = {
(* ... language-independent properties *)
translations : translation TranslationMap.t;
}
let multilingual json =
{
(* ... decode language-independent properties *)
translations =
json |> field "translations" (TranslationMap.Decode.t translation);
}
let make_assoc locale_order json =
let open Option.Infix in
json |> multilingual |> fun multilingual ->
multilingual.translations |> TranslationMap.preferred locale_order
<&> fun (translation : translation) ->
( multilingual.id,
{
(* ... merge language-independent and language-specific properties *)
} )
end
Decoding types
Since language-specific values in the YAML files are always defined in dictionaries where the keys map to the locale identifiers, we always split the language-specific and the language-independent parts.
The language-specific part of a type is always defined as type translation
. The corresponding translation
decoder function is later used to decode the translation dictionary, also called translation map.
The language-independent part of a type is defined as type multilingual
and usually has the property translations
, which is the dictionary of all translations. The corresponding multilingual
decoder function uses the translation
decoder together with the translation map decoder from the TranslationMap
module to fully decode the dictionary of translations.
Using a list of preferred locales, sorted by preference, (Locale.Order.t
) we can retrieve the most preferred translation using TranslationMap.preferred
and then combine the translation values and the multilingual values to form the final record type. Following OCaml conventions, this function is called make
.
Often, in order to be used more easily, the make
function returns a pair of the respective entry’s identifier and the entry itself to create a Map
more easily. To hint at this specific use, the function is then called make_assoc
. If a nested type is only decoded in a context of collection type (like a list), this decoder can be provided using a make
variant as well (in this case, make_list
).
If other decodable types are nested withing a type, the multilingual
function usually takes the list of preferred locales as well to be able to pass it down to the nested types’ decoders.
Decode
module visibility conventions
Only the make
function or its variant should be exposed.
Naming conventions
- The language-specific tyüe is always named
translation
. - The record containing all language-independent values as well as the translation map is called
multilingual
and the translation map is at the propertytranslations
(just like in the source). - A decoder for a specific type has the same name as the type.
- The function resolving the translation map and returning the actual main type is called
make
. - A potential helper function that decodes the type as part of a collection is called
make_c
, wherec
is the name of the collection, e.g.make_list
.