123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597(** Functors for creating Decoders. *)openUtiltype'valueexposed_error=|Decoder_errorofstring*'valueoption|Decoder_errorsof'valueexposed_errorlist|Decoder_tagofstring*'valueexposed_errortype('good,'bad)result=('good,'bad)My_result.t=Okof'good|Errorof'badtype('value,'a)exposed_decoder={run:'value->('a,'valueexposed_error)result}(** Signature of things that can be decoded. *)moduletypeDecodeable=sigtypevaluevalpp:Format.formatter->value->unitvalof_string:string->(value,string)resultvalof_file:string->(value,string)resultvalget_string:value->stringoptionvalget_int:value->intoptionvalget_float:value->floatoptionvalget_bool:value->booloptionvalget_null:value->unitoptionvalget_list:value->valuelistoptionvalget_key_value_pairs:value->(value*value)listoptionend(** User-facing Decoder interface. *)moduletypeS=sig(** The type of values to be decoded (e.g. JSON or Yaml). *)typevaluetypeerror=valueexposed_errorvalpp_error:Format.formatter->error->unitvalof_string:string->(value,error)resultvalof_file:string->(value,error)result(** The type of decoders.
Use the functions below to construct decoders for your data types.
To run a decoder, pass it to {!val:decode_value}.
*)type'adecoder(** {1 Primitives} *)(** Decode a [string]. *)valstring:stringdecoder(** Decode an [int]. *)valint:intdecoder(** Decode a [float]. *)valfloat:floatdecoder(** Decode a [bool]. *)valbool:booldecoder(** Decode a literal [value]. *)valvalue:valuedecoder(** {1 Data structures} *)(** Decode a collection into an OCaml list. *)vallist:'adecoder->'alistdecodervallist_filter:'aoptiondecoder->'alistdecoder(** {1 Object primitives} *)(** Decode an object, requiring a particular field. *)valfield:string->'adecoder->'adecoder(** Decode an object, requiring exactly one field. *)valsingle_field:(string->'adecoder)->'adecoder(** Decode an array, requiring a particular index. *)valindex:int->'adecoder->'adecoder(** Decode a nested object, requiring certain fields. *)valat:stringlist->'adecoder->'adecoder(** {1 Inconsistent structure} *)(** Helpful for dealing with optional fields. *)valmaybe:'adecoder->'aoptiondecodervalnullable:'adecoder->'aoptiondecoder(** Try a sequence of different decoders. *)valone_of:(string*'adecoder)list->'adecoder(** {1 Mapping} *)(** Map functions are useful for decoding complex objects.
For example, given an object with structure
{[
{
"name": "Joe"
"age": 42
}
]}
we want to decode it to our OCaml type
{[
type person =
{ name : string
; age : int
}
]}
We define a helper function to construct values of this type:
{[
let as_person name age =
{ name = name
; age = age
}
]}
The decoder looks like this:
{[
let person_decoder : person decoder =
map2 as_person
(field "name" string)
(field "age" int)
]}
*)(** Transform a decoder. *)valmap:('a->'b)->'adecoder->'bdecoder(** Try two decoders and then combine the result. We can use this to decode
objects with many fields.
*)valapply:('a->'b)decoder->'adecoder->'bdecoder(** {1 Working with object keys} *)valkeys:stringlistdecodervalkey_value_pairs:'vdecoder->(string*'v)listdecodervalkey_value_pairs_seq:(string->'vdecoder)->'vlistdecodervalkeys':'kdecoder->'klistdecodervalkey_value_pairs':'kdecoder->'vdecoder->('k*'v)listdecodervalkey_value_pairs_seq':'kdecoder->('k->'vdecoder)->'vlistdecoder(** {1 Fancy decoding} *)(** A decoder that always succeeds with the argument, ignoring the input. *)valsucceed:'a->'adecoder(** A decoder that always fails with the given message, ignoring the input. *)valfail:string->'adecodervalfail_with:error->'adecodervalfrom_result:('a,error)result->'adecoder(** Create decoders that depend on previous results. *)valand_then:('a->'bdecoder)->'adecoder->'bdecoder(** Recursive decoders.
[let my_decoder = fix (fun my_decoder -> ...)] allows you to define
[my_decoder] in terms of itself.
*)valfix:('adecoder->'adecoder)->'adecodermoduleInfix:sigval(>|=):'adecoder->('a->'b)->'bdecoderval(>>=):'adecoder->('a->'bdecoder)->'bdecoderval(<*>):('a->'b)decoder->'adecoder->'bdecoderendincludemoduletypeofInfix(** {1 Running decoders} *)(** Run a decoder on some input. *)valdecode_value:'adecoder->value->('a,error)result(** Run a decoder on a string. *)valdecode_string:'adecoder->string->('a,error)result(** Run a decoder on a file. *)valdecode_file:'adecoder->string->('a,error)result(** {1 Pipeline Decoders} *)modulePipeline:sig(**
Pipeline decoders present an alternative to the [mapN] style. They read
more naturally, but can lead to harder-to-understand type errors.
{[
let person_decoder : person decoder =
decode as_person
|> required "name" string
|> required "age" int
]}
*)valdecode:'a->'adecodervalrequired:string->'adecoder->('a->'b)decoder->'bdecodervalrequired_at:stringlist->'adecoder->('a->'b)decoder->'bdecodervaloptional:string->'adecoder->'a->('a->'b)decoder->'bdecodervaloptional_at:stringlist->'adecoder->'a->('a->'b)decoder->'bdecodervalcustom:'adecoder->('a->'b)decoder->'bdecoderendendmoduleMake(Decodeable:Decodeable):Swithtypevalue=Decodeable.valueandtype'adecoder=(Decodeable.value,'a)exposed_decoder=structtypevalue=Decodeable.valueletpp=Decodeable.pptypeerror=valueexposed_errorletrecpp_errorfmt=function|Decoder_error(msg,Somet)->Format.fprintffmt"@[%s, but got@ @[%a@]@]"msgppt|Decoder_error(msg,None)->Format.fprintffmt"@[%s@]"msg|Decoder_errorserrors->leterrors_trunc=My_list.take5errorsinletnot_shown=List.lengtherrors-5inFormat.fprintffmt"@[%a@ %s@]"(Format.pp_print_list~pp_sep:Format.pp_print_spacepp_error)errors_trunc(ifnot_shown>0thenPrintf.sprintf"(...%d errors not shown...)"not_shownelse"")|Decoder_tag(msg,error)->Format.fprintffmt"@[<2>%s:@ @[%a@]@]"msgpp_errorerrorlettag_error(msg:string)(error:error):error=Decoder_tag(msg,error)lettag_errors(msg:string)(errors:errorlist):error=Decoder_tag(msg,Decoder_errorserrors)letmerge_errorse1e2=matche1,e2with|Decoder_errorse1s,Decoder_errorse2s->Decoder_errors(e1s@e2s)|Decoder_errorse1s,_->Decoder_errors(e1s@[e2])|_,Decoder_errorse2s->Decoder_errors([e1]@e2s)|_->Decoder_errors[e1;e2]letreccombine_errors:('a,error)resultlist->('alist,errorlist)result=function|[]->Ok[]|result::rest->beginmatchresult,combine_errorsrestwith|Okx,Okxs->Ok(x::xs)|Errore,Errores->Error(e::es)|Errore,Ok_->Error[e]|Ok_,Errores->Erroresendletof_string:string->(value,error)result=funstring->Decodeable.of_stringstring|>My_result.map_err(funmsg->(Decoder_tag("Json parse error",Decoder_error(msg,None))))letof_file:string->(value,error)result=funfile->Decodeable.of_filefile|>My_result.map_err(funmsg->(Decoder_tag(Printf.sprintf"While reading %s"file,Decoder_error(msg,None))))type'adecoder=(value,'a)exposed_decoderletsucceedx={run=fun_->Okx}letfailmsg={run=funinput->Error(Decoder_error(msg,Someinput))}letfail_witherror={run=fun_->Errorerror}letfrom_result=function|Okok->succeedok|Errorerror->fail_witherrorletvalue={run=funinput->Okinput}letmapfdecoder={run=funinput->My_result.Infix.(decoder.runinput>|=f)}letapply:('a->'b)decoder->'adecoder->'bdecoder=funfdecoder->{run=funinput->matchf.runinput,decoder.runinputwith|Errore1,Errore2->Error(merge_errorse1e2)|Errore,_->Errore|_,Errore->Errore|Okg,Okx->Ok(gx)}letand_then(f:'a->'bdecoder)(decoder:'adecoder):'bdecoder={run=funinput->My_result.Infix.(decoder.runinput>>=funresult->(fresult).runinput)}letfix(f:'adecoder->'adecoder):'adecoder=letrecp=lazy(fr)andr={run=funvalue->(Lazy.forcep).runvalue}inrmoduleInfix=structlet(>|=)xf=mapfxlet(>>=)xf=and_thenfxlet(<*>)fx=applyfxendletmaybe(decoder:'adecoder):'aoptiondecoder={run=funinput->match(decoder.runinput)with|Okresult->Ok(Someresult)|Error_->OkNone}letnullable(decoder:'adecoder):'aoptiondecoder={run=funinput->matchDecodeable.get_nullinputwith|Some()->OkNone|None->decoder.runinput|>My_result.mapMy_opt.return|>My_result.map_err(tag_error"Expected null or")}letone_of:(string*'adecoder)list->'adecoder=fundecoders->letruninput=letrecgoerrors=function|(name,decoder)::rest->(matchdecoder.runinputwith|Okresult->Okresult|Errorerror->go(tag_errors(Printf.sprintf"%S decoder"name)[error]::errors)rest)|[]->Error(tag_errors"I tried the following decoders but they all failed"errors)ingo[]decodersin{run}letprimitive_decoder(get_value:value->'aoption)(message:string):'adecoder={run=funt->matchget_valuetwith|Somevalue->Okvalue|_->(failmessage).runt}letstring:stringdecoder=primitive_decoderDecodeable.get_string"Expected a string"letint:intdecoder=primitive_decoderDecodeable.get_int"Expected an int"letfloat:floatdecoder=primitive_decoderDecodeable.get_float"Expected a float"letbool:booldecoder=primitive_decoderDecodeable.get_bool"Expected a bool"letnull:'a->'adecoder=fundefault->primitive_decoderDecodeable.get_null"Expected a null"|>map(fun_->default)letlist:'adecoder->'alistdecoder=fundecoder->{run=funt->matchDecodeable.get_listtwith|None->(fail"Expected a list").runt|Somevalues->values|>My_list.mapi(funix->decoder.runx|>My_result.map_err(tag_error(Printf.sprintf"element %i"i)))|>combine_errors|>My_result.map_err(tag_errors"while decoding a list")}letlist_filter:'aoptiondecoder->'alistdecoder=fundecoder->letrecgoi=function|[]->Ok[]|v::vs->My_result.Infix.(decoder.runv|>My_result.map_err(tag_error(Printf.sprintf"element %i"i))>>=function|Somex->go(i+1)vs>>=funxs->My_result.return(x::xs)|None->go(i+1)vs)in{run=funt->matchDecodeable.get_listtwith|None->(fail"Expected a list").runt|Somevalues->go0values|>My_result.map_err(tag_error"while decoding a list")}letfield:string->'adecoder->'adecoder=funkeyvalue_decoder->{run=funt->letvalue=Decodeable.get_key_value_pairst|>My_opt.flat_map(My_list.find_map(fun(k,v)->matchDecodeable.get_stringkwith|Someswhens=key->Somev|_->None))inmatchvaluewith|Somevalue->value_decoder.runvalue|>My_result.map_err(tag_error(Printf.sprintf"in field %S"key))|None->(fail(Printf.sprintf"Expected an object with an attribute %S"key)).runt}letsingle_field:(string->'adecoder)->'adecoder=funvalue_decoder->{run=funt->matchDecodeable.get_key_value_pairstwith|Some[(key,value)]->beginmatchDecodeable.get_stringkeywith|Somekey->(value_decoderkey).runvalue|>My_result.map_err(tag_error(Printf.sprintf"in field %S"key))|None->(fail"Expected an object with a string key").runtend|_->(fail"Expected an object with a single attribute").runt}letindex:int->'adecoder->'adecoder=funidecoder->{run=funt->matchDecodeable.get_listtwith|Somel->letitem=trySome(List.nthli)with|Failure_->None|Invalid_argument_->Noneinbeginmatchitemwith|None->(fail("expected a list with at least "^string_of_inti^" elements")).runt|Someitem->decoder.runitemend|None->(fail"Expected a list").runt}letrecat:stringlist->'adecoder->'adecoder=funpathdecoder->matchpathwith|[key]->fieldkeydecoder|key::rest->fieldkey(atrestdecoder)|[]->fail"Must provide at least one key to 'at'"letkeys':'kdecoder->'klistdecoder=funkey_decoder->{run=funvalue->matchDecodeable.get_key_value_pairsvaluewith|Someassoc->assoc|>List.map(fun(key,_)->key_decoder.runkey)|>combine_errors|>My_result.map_err(tag_errors"Failed while decoding the keys of an object")|None->(fail"Expected an object").runvalue}letkeys=keys'stringletkey_value_pairs':'kdecoder->'vdecoder->('k*'v)listdecoder=funkey_decodervalue_decoder->{run=funvalue->matchDecodeable.get_key_value_pairsvaluewith|Someassoc->assoc|>List.mapMy_result.Infix.(fun(key_val,value_val)->key_decoder.runkey_val>>=funkey->value_decoder.runvalue_val>|=funvalue->(key,value))|>combine_errors|>My_result.map_err(tag_errors"Failed while decoding key-value pairs")|None->(fail"Expected an object").runvalue}letkey_value_pairsvalue_decoder=key_value_pairs'stringvalue_decoderletkey_value_pairs_seq':'kdecoder->('k->'vdecoder)->'vlistdecoder=funkey_decodervalue_decoder->{run=funvalue->matchDecodeable.get_key_value_pairsvaluewith|Someassoc->assoc|>List.mapMy_result.Infix.(fun(key_val,value_val)->key_decoder.runkey_val>>=funkey->(value_decoderkey).runvalue_val)|>combine_errors|>My_result.map_err(tag_errors"Failed while decoding key-value pairs")|None->(fail"Expected an object").runvalue}letkey_value_pairs_seqvalue_decoder=key_value_pairs_seq'stringvalue_decoderletdecode_value(decoder:'adecoder)(input:value):('a,error)result=decoder.runinputletdecode_string:'adecoder->string->('a,error)result=fundecoderstring->My_result.Infix.(of_stringstring>>=decode_valuedecoder)letdecode_file:'adecoder->string->('a,error)result=fundecoderfile->My_result.Infix.(of_filefile>>=decode_valuedecoder)modulePipeline=structletdecode=succeedletcustom:'adecoder->('a->'b)decoder->'bdecoder=funcustomDecodernext->applynextcustomDecoderletrequired:string->'adecoder->('a->'b)decoder->'bdecoder=funkeydecodernext->custom(fieldkeydecoder)nextletrequired_at:stringlist->'adecoder->('a->'b)decoder->'bdecoder=funpathdecodernext->custom(atpathdecoder)nextletoptional_decoder:valuedecoder->'adecoder->'a->'adecoder=funpath_decoderval_decoderdefault->letnull_ordecoder=one_of[("non-null",decoder);("null",nulldefault)]inlethandle_result:value->'adecoder=funinput->matchdecode_valuepath_decoderinputwith|OkrawValue->(* The field was present. *)decode_value(null_orval_decoder)rawValue|>from_result|Error_->(* The field was not present. *)succeeddefaultinvalue|>and_thenhandle_resultletoptional:string->'adecoder->'a->('a->'b)decoder->'bdecoder=funkeyval_decoderdefaultnext->custom(optional_decoder(fieldkeyvalue)val_decoderdefault)nextletoptional_at:stringlist->'adecoder->'a->('a->'b)decoder->'bdecoder=funpathval_decoderdefaultnext->custom(optional_decoder(atpathvalue)val_decoderdefault)nextendincludeInfixend