123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829(*****************************************************************************)(* *)(* Open Source License *)(* Copyright (C) 2016, OCamlPro. *)(* Copyright (c) 2020-2021 Nomadic Labs <contact@nomadic-labs.com> *)(* *)(* Permission is hereby granted, free of charge, to any person obtaining a *)(* copy of this software and associated documentation files (the "Software"),*)(* to deal in the Software without restriction, including without limitation *)(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *)(* and/or sell copies of the Software, and to permit persons to whom the *)(* Software is furnished to do so, subject to the following conditions: *)(* *)(* The above copyright notice and this permission notice shall be included *)(* in all copies or substantial portions of the Software. *)(* *)(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *)(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *)(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *)(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *)(* DEALINGS IN THE SOFTWARE. *)(* *)(*****************************************************************************)moduleUtils=structletsplit_pathpath=letl=String.lengthpathinletrecdo_slashesacci=ifi>=lthenList.revaccelseifpath.[i]='/'thendo_slashesacc(i+1)elsedo_componentacciianddo_componentaccij=ifj>=lthenifi=jthenList.revaccelseList.rev(String.subpathi(j-i)::acc)elseifpath.[j]='/'thendo_slashes(String.subpathi(j-i)::acc)jelsedo_componentacci(j+1)indo_slashes[]0letdecode_split_pathpath=path|>split_path|>List.mapUri.pct_decodeendletbool_of_strings=matchString.lowercase_asciiswith|""|"true"|"t"|"yes"|"y"->Oktrue|"false"|"f"|"no"|"n"->Okfalse|_->Error"Cannot parse boolean value"typemeth=[`GET|`POST|`DELETE|`PUT|`PATCH]letstring_of_meth=function|`GET->"GET"|`POST->"POST"|`DELETE->"DELETE"|`PUT->"PUT"|`PATCH->"PATCH"letmeth_of_string=function|"GET"->Some`GET|"POST"->Some`POST|"DELETE"->Some`DELETE|"PUT"->Some`PUT|"PATCH"->Some`PATCH|_->NonemoduleMethMap=Map.Make(structtypet=methletcompare=compareend)moduleStringMap=Map.Make(String)type(_,_)eq=Eq:('a,'a)eqmoduleInternal=structmoduleTy=structtype'awitness=..exceptionNot_equalmoduletypeTy=sigtypetvalwitness:twitnessvaleq:'awitness->('a,t)eqendtype'aid=(moduleTywithtypet='a)letnew_id(typea)()=letmoduleTy=structtypet=atype'awitness+=Ty:twitnessletwitness=Tyleteq(typeb):bwitness->(b,t)eq=function|Ty->Eq|_->raiseNot_equalendin(moduleTy:Tywithtypet=a)leteq:typeab.aid->bid->(a,b)eq=fun(moduleTyA)(moduleTyB)->TyB.eqTyA.witnessendtypedescr={name:string;descr:stringoption}type'aarg={id:'aTy.id;destruct:string->('a,string)result;construct:'a->string;descr:descr;}letfrom_argx=xletto_argx=xtype(_,_)path=|Root:('rkey,'rkey)path|Static:('rkey,'key)path*string->('rkey,'key)path|Dynamic:('rkey,'key)path*'aarg->('rkey,'key*'a)path|DynamicTail:('rkey,'key)path*'aarg->('rkey,'key*'alist)pathletrecsubst0:typeab.(a,a)path->(b,b)path=function|Root->Root|Static(path,name)->Static(subst0path,name)|Dynamic_->assertfalse(* impossible *)|DynamicTail_->assertfalse(* impossible *)letrecsubst1:typeabc.(a,a*c)path->(b,b*c)path=function|Root->assertfalse(* impossible *)|Static(path,name)->Static(subst1path,name)|Dynamic(path,arg)->Dynamic(subst0path,arg)|DynamicTail(path,arg)->DynamicTail(subst0path,arg)letrecsubst2:typeabcd.(a,(a*c)*d)path->(b,(b*c)*d)path=function|Root->assertfalse(* impossible *)|Static(path,name)->Static(subst2path,name)|Dynamic(path,arg)->Dynamic(subst1path,arg)|DynamicTail(path,arg)->DynamicTail(subst1path,arg)letrecsubst3:typeabcde.(a,((a*c)*d)*e)path->(b,((b*c)*d)*e)path=function|Root->assertfalse(* impossible *)|Static(path,name)->Static(subst3path,name)|Dynamic(path,arg)->Dynamic(subst2path,arg)|DynamicTail(path,arg)->DynamicTail(subst2path,arg)letfrom_pathx=xletto_pathx=xtype'aquery=(* inspired from Irmin.Ty.record. *)|Fields:('a,'b)query_fields*'b->'aqueryand('a,'b)query_fields=|F0:('a,'a)query_fields|F1:('a,'b)query_field*('a,'c)query_fields->('a,'b->'c)query_fieldsand('a,'b)query_field=|Single:{name:string;description:stringoption;ty:'barg;default:'b;get:'a->'b;}->('a,'b)query_field|Opt:{name:string;description:stringoption;ty:'barg;get:'a->'boption;}->('a,'boption)query_field|Flag:{name:string;description:stringoption;get:'a->bool;}->('a,bool)query_field|Multi:{name:string;description:stringoption;ty:'barg;get:'a->'blist;}->('a,'blist)query_fieldtypequery_kind=|Singleofdescr|Optionalofdescr|Flag|Multiofdescrletfield_name(typet):(_,t)query_field->_=function|Single{name;_}->name|Opt{name;_}->name|Flag{name;_}->name|Multi{name;_}->nameletfield_description(typet):(_,t)query_field->_=function|Single{description;_}->description|Opt{description;_}->description|Flag{description;_}->description|Multi{description;_}->descriptionletfield_kind(typet):(_,t)query_field->query_kind=function|Single{ty;_}->Singlety.descr|Opt{ty;_}->Optionalty.descr|Flag_->Flag|Multi{ty;_}->Multity.descrletfrom_queryx=xletto_queryx=xendopenInternalmoduleArg=structtypedescr=Internal.descr={name:string;descr:stringoption}type'at='aInternal.argtype'aarg='atletmake?descr~name~destruct~construct()=letid=Ty.new_id()inletdescr={name;descr}in{descr;id;construct;destruct}letlikearg?descrname={argwithid=Ty.new_id();descr={name;descr}}letdescr(ty:'aarg)=ty.descrletbool:boolarg=letstring_of_bool=functiontrue->"yes"|false->"no"inmake~name:"bool"~destruct:bool_of_string~construct:string_of_bool()letint=letint_of_strings=tryOk(int_of_strings)withFailure_->Error"Cannot parse integer value"inmake~name:"int"~destruct:int_of_string~construct:string_of_int()letfloat=letfloat_of_strings=tryOk(float_of_strings)withFailure_->Error"Cannot parse float value"inmake~name:"float"~destruct:float_of_string~construct:string_of_float()letint32=letint32_of_strings=tryOk(Int32.of_strings)withFailure_->Error"Cannot parse int32 value"inmake~name:"int32"~destruct:int32_of_string~construct:Int32.to_string()letint64=letint64_of_strings=tryOk(Int64.of_strings)withFailure_->Error"Cannot parse int64 value"inmake~name:"int64"~destruct:int64_of_string~construct:Int64.to_string()letstring=make~name:"string"~destruct:(funx->Okx)~construct:(funx->x)()leteqa1a2=trySome(Ty.eqa1.ida2.id)withInternal.Ty.Not_equal->NoneendmodulePath=structtype('a,'b)t=('a,'b)Internal.pathtype('a,'b)path=('a,'b)Internal.pathtype'prefixcontext=('prefix,'prefix)pathletroot=Rootletopen_root=Rootletadd_suffix(typeppr)(path:(p,pr)path)name=matchpathwith|DynamicTail_->invalid_arg"Resto.Path.add_suffix"|path->Static(path,name)letadd_arg(typeppr)(path:(p,pr)path)arg=matchpathwith|DynamicTail_->invalid_arg"Resto.Path.add_arg"|path->Dynamic(path,arg)letadd_final_args(typeppr)(path:(p,pr)path)arg=matchpathwith|DynamicTail_->invalid_arg"Resto.Path.add_final_arg"|path->DynamicTail(path,arg)letprefix:typeppra.(pr,a)path->(a,p)path->(pr,p)path=funp1p2->letrecprefix:typeprak.(pr,a)path->(a,k)path->(pr,k)path=funp1p2->matchp2with|Root->p1|Static(path,name)->add_suffix(prefixp1path)name|Dynamic(path,arg)->add_arg(prefixp1path)arg|DynamicTail(path,arg)->add_final_args(prefixp1path)arginmatchp1with|DynamicTail_->invalid_arg"Resto.Path.prefix"|_->prefixp1p2let(/)=add_suffixlet(/:)=add_arglet(/:*)=add_final_argsletsubst0=Internal.subst0letsubst1=Internal.subst1letsubst2=Internal.subst2letsubst3=Internal.subst3letto_segments:typeprp.(pr,p)path->stringlist=funpath->letrecflatten_rev:typeprp.(pr,p)path->stringlist=function|Root->[]|Static(p,s)->s::flatten_revp|Dynamic(p,arg)->Printf.sprintf"<%s>"arg.descr.name::flatten_revp|DynamicTail(p,arg)->Printf.sprintf"<%s>*"arg.descr.name::flatten_revpinList.rev@@flatten_revpathletto_stringpath="/"^String.concat"/"(to_segmentspath)endmoduleQuery=structtype'at='aInternal.querytype'aquery='aInternal.querytype('a,'b)field=('a,'b)Internal.query_fieldtype('a,'b,'c)open_query=('a,'c)query_fields->'b*('a,'b)query_fieldsletfield?descrnametydefaultget:(_,_)query_field=Single{name;description=descr;ty;default;get}letopt_field?descrnametyget:(_,_)query_field=Opt{name;description=descr;ty;get}letflag?descrnameget:(_,_)query_field=Flag{name;description=descr;get}letmulti_field?descrnametyget:(_,_)query_field=Multi{name;description=descr;ty;get}letquery:'b->('a,'b,'b)open_query=funcfs->(c,fs)letapp:typeabcd.(a,b,c->d)open_query->(a,c)query_field->(a,b,d)open_query=funrffs->letc,fs=r(F1(f,fs))in(c,fs)letseal:typeab.(a,b,a)open_query->at=funr->letc,fs=rF0inFields(fs,c)let(|+)=appletempty=Fields(F0,())type'aefield=Field:('a,'b)query_field->'aefieldletfold_fields(typefs)~f~initfs=letrecloop:typef._->(fs,f)query_fields->_=funacc->function|F0->acc|F1(field,fs)->loop(facc(Fieldfield))fsinloopinitfstype'aparsed_field=|Parsed:('a,'b)query_field*'boption->'aparsed_fieldletrecrebuild:typefsf._->(fs,f)query_fields->f->fs=funmapfsf->matchfswith|F0->f|F1(Singlefield,fs)->(matchStringMap.findfield.namemapwith|Parsed(Singlefield',v)->letEq=Ty.eqfield.ty.idfield'.ty.idinletv=matchvwithNone->field.default|Somev->vinrebuildmapfs(fv)|Parsed_->assertfalse)|F1(Optfield,fs)->(matchStringMap.findfield.namemapwith|Parsed(Optfield',v)->letEq=Ty.eqfield.ty.idfield'.ty.idinletv=matchvwithNone->None|Somev->vinrebuildmapfs(fv)|Parsed_->assertfalse)|F1(Flagfield,fs)->(matchStringMap.findfield.namemapwith|Parsed(Flag_,v)->letv=matchvwithNone->false|Somev->vinrebuildmapfs(fv)|Parsed_->assertfalse)|F1(Multifield,fs)->(matchStringMap.findfield.namemapwith|Parsed(Multifield',v)->letEq=Ty.eqfield.ty.idfield'.ty.idinletv=matchvwithNone->[]|Somev->vinrebuildmapfs(fv)|Parsed_->assertfalse)exceptionInvalidofstringtypeuntyped=(string*string)listletparse(Fields(fs,f))=letfields=fold_fields~f:(funmap(Fieldf)->StringMap.add(field_namef)(Parsed(f,None))map)~init:StringMap.emptyfsinfunquery->letfailfmt=Format.kasprintf(funs->raise(Invalids))fmtinletfields=List.fold_left(funfields(name,value)->matchStringMap.findnamefieldswith|exceptionNot_found->fields|Parsed(Single_,Some_)->fail"Duplicate argument '%s' in query string."name|Parsed(Opt_,Some_)->fail"Duplicate argument '%s' in query string."name|Parsed(Flag_,Some_)->fail"Duplicate argument '%s' in query string."name|Parsed(Singlef,None)->(matchf.ty.destructvaluewith|Errorerror->fail"Failed to parse argument '%s' (%S): %s"namevalueerror|Okv->StringMap.addname(Parsed(Singlef,Somev))fields)|Parsed(Optf,None)->(matchf.ty.destructvaluewith|Errorerror->fail"Failed to parse argument '%s' (%S): %s"namevalueerror|Okv->StringMap.addname(Parsed(Optf,Some(Somev)))fields)|Parsed(Flagf,None)->(matchbool_of_stringvaluewith|Okv->StringMap.addname(Parsed(Flagf,Somev))fields|Errorerror->fail"Failed to parse argument '%s' (%S): %s"namevalueerror)|Parsed(Multif,previous)->(matchf.ty.destructvaluewith|Errorerror->fail"Failed to parse argument '%s' (%S): %s"namevalueerror|Okv->letv=matchpreviouswithNone->[v]|Somel->v::linStringMap.addname(Parsed(Multif,Somev))fields))fieldsqueryinrebuildfieldsfsfendmoduleDescription=structtyperequest={recurse:bool}letrequest_query=letopenQueryinquery(funrecurse->{recurse})|+field"recurse"Arg.boolfalse(funt->t.recurse)|>sealtypenonrecquery_kind=query_kind=|SingleofArg.descr|OptionalofArg.descr|Flag|MultiofArg.descr[@@@ocaml.warning"-30"]type'schemaservice={description:stringoption;path:path_itemlist;meth:meth;query:query_itemlist;input:'schemaLazy.toption;output:'schemaLazy.t;error:'schemaLazy.t;}andpath_item=|PStaticofstring|PDynamicofArg.descr|PDynamicTailofArg.descrandquery_item={name:string;description:stringoption;kind:query_kind;}type'schemadirectory=|Empty|Staticof'schemastatic_directory|Dynamicofstringoptionand'schemastatic_directory={services:'schemaserviceMethMap.t;subdirs:'schemastatic_subdirectoriesoption;}and'schemastatic_subdirectories=|Suffixesof'schemadirectoryMap.Make(String).t|ArgofArg.descr*'schemadirectoryletrecpp_print_directoryppf=letopenFormatinfunction|Empty->fprintfppf"<empty>"|Staticdir->fprintfppf"@[%a@]"pp_print_static_directorydir|DynamicNone->fprintfppf"<dyntree>"|Dynamic(Somedescr)->fprintfppf"<dyntree> : %s"descrandpp_print_static_directoryppf=letopenFormatinfunction|{services;subdirs=None}whenMethMap.is_emptyservices->fprintfppf"{}"|{services;subdirs=None}->fprintfppf"@[<v>%a@]"pp_print_dispatch_servicesservices|{services;subdirs=Somesubdirs}whenMethMap.is_emptyservices->fprintfppf"%a"pp_print_static_subdirectoriessubdirs|{services;subdirs=Somesubdirs}->fprintfppf"@[<v>%a@ %a@]"pp_print_dispatch_servicesservicespp_print_static_subdirectoriessubdirsandpp_print_static_subdirectoriesppf=letopenFormatinfunction|Suffixesmap->letprint_bindingppf(name,tree)=fprintfppf"@[<hov 2>%s:@ %a@]"namepp_print_directorytreeinfprintfppf"@[<v>%a@]"(pp_print_list~pp_sep:pp_print_cutprint_binding)(StringMap.bindingsmap)|Arg(arg,tree)->fprintfppf"@[<hov 2>[:%s:]@ @[%a@]@]"arg.namepp_print_directorytreeandpp_print_dispatch_servicesppfservices=MethMap.iter(fun_s->matchswith|{description=None;meth;_}->Format.fprintfppf"<%s>"(string_of_methmeth)|{description=Somedescr;meth;_}->Format.fprintfppf"<%s> : %s"(string_of_methmeth)descr)servicesendmoduletypeENCODING=sigtype'attypeschemavalunit:unittvaluntyped:stringtvalconv:('a->'b)->('b->'a)->'bt->'atvalschema:?definitions_path:string->'at->schemavaldescription_request_encoding:Description.requesttvaldescription_answer_encoding:schemaDescription.directorytendmoduleMakeService(Encoding:ENCODING)=structmoduleInternal=structincludeInternaltype('query,'input,'output,'error)types={query:'queryquery;input:'inputinput;output:'outputEncoding.t;error:'errorEncoding.t;}and_input=|No_input:unitinput|Input:'inputEncoding.t->'inputinputtype(+'meth,'prefix,'params,'query,'input,'output,'error)iservice={description:stringoption;meth:'meth;path:('prefix,'params)path;types:('query,'input,'output,'error)types;}constraint'meth=[<meth]letfrom_servicex=xletto_servicex=xtype(_,_)eq=|Eq:(('query,'input,'output,'error)types,('query,'input,'output,'error)types)eqexceptionNot_equalleteq:typequery1input1output1error1query2input2output2error2.(query1,input1,output1,error1)types->(query2,input2,output2,error2)types->((query1,input1,output1,error1)types,(query2,input2,output2,error2)types)eq=funxy->ifObj.magicx==Obj.magicythenObj.magicEq(* FIXME *)elseraiseNot_equalendincludeInternalopenPathtype(+'meth,'prefix,'params,'query,'input,'output,'error)t=('meth,'prefix,'params,'query,'input,'output,'error)Internal.iservicetype(+'meth,'prefix,'params,'query,'input,'output,'error)service=('meth,'prefix,'params,'query,'input,'output,'error)tletget_service?description~query~output~errorpath=letinput=No_inputin{meth=`GET;description;path;types={query;input;output;error}}letpost_service?description~query~input~output~errorpath=letinput=Inputinputin{meth=`POST;description;path;types={query;input;output;error}}letdelete_service?description~query~output~errorpath=letinput=No_inputin{meth=`DELETE;description;path;types={query;input;output;error}}letput_service?description~query~input~output~errorpath=letinput=Inputinputin{meth=`PUT;description;path;types={query;input;output;error}}letpatch_service?description~query~input~output~errorpath=letinput=Inputinputin{meth=`PATCH;description;path;types={query;input;output;error}}letprefixpaths={swithpath=Path.prefixpaths.path}letsubst0s={swithpath=Internal.subst0s.path}letsubst1s={swithpath=Internal.subst1s.path}letsubst2s={swithpath=Internal.subst2s.path}letsubst3s={swithpath=Internal.subst3s.path}letmeth{meth;_}=methletquery:typeprpiqoe.(_,pr,p,q,i,o,e)service->qQuery.t=fun{types;_}->types.queryletinput_encoding:typeprpiqoe.(_,pr,p,q,i,o,e)service->iinput=fun{types;_}->types.inputletoutput_encoding:typeprpiqoe.(_,pr,p,q,i,o,e)service->oEncoding.t=fun{types;_}->types.outputleterror_encoding:typeprpiqoe.(_,pr,p,q,i,o,e)service->eEncoding.t=fun{types;_}->types.errortype('prefix,'params,'error)description_service=([`GET],'prefix,'params*stringlist,Description.request,unit,Encoding.schemaDescription.directory,'error)serviceletdescription_service?descriptionerrorpath=letdescription=matchdescriptionwithSomedescr->descr|None->"<TODO>"inget_service~description~query:Description.request_query~output:Encoding.description_answer_encoding~errorPath.(path/:*Arg.string)type'inputrequest={meth:meth;uri:Uri.t;input:'inputinput}letforge_request_args:typeprp.(pr,p)path->p->stringlist=funpathargs->letrecforge_request_args:typek.(pr,k)path->k->stringlist->stringlist=funpathargsacc->match(path,args)with|Root,_->acc|Static(path,name),args->forge_request_argspathargs(name::acc)|Dynamic(path,arg),(args,x)->forge_request_argspathargs(arg.constructx::acc)|DynamicTail(path,arg),(args,xs)->forge_request_argspathargs(List.fold_right(funxacc->arg.constructx::acc)xsacc)inforge_request_argspathargs[]letforge_request_query:typeq.qquery->q->(string*string)list=fun(Fields(fields,_))q->letrecloop:typet.(q,t)query_fields->_=function|F0->[]|F1(Single{name;ty;get;_},fields)->(name,ty.construct(getq))::loopfields|F1(Opt{name;ty;get;_},fields)->(matchgetqwith|None->loopfields|Somev->(name,ty.constructv)::loopfields)|F1(Flag{name;get;_},fields)->(matchgetqwith|false->loopfields|true->(name,"true")::loopfields)|F1(Multi{name;ty;get;_},fields)->(matchgetqwith|[]->loopfields|l->List.fold_right(funvacc->(name,ty.constructv)::acc)l(loopfields))inloopfieldsletforge_partial_request:typeprpiqoe.(_,pr,p,q,i,o,e)service->?base:Uri.t->p->q->irequest=funs?base:(uri=Uri.empty)argsquery->letpath=String.concat"/"(forge_request_argss.pathargs)inletprefix=Uri.pathuriinletprefixed_path=ifprefix=""thenpathelseprefix^"/"^pathinleturi=Uri.with_pathuriprefixed_pathinleturi=Uri.with_query'uri(forge_request_querys.types.queryquery)in{meth=s.meth;uri;input=s.types.input}letforge_partial_request=(forge_partial_request:(meth,_,_,_,_,_,_)service->_:>([<meth],_,_,_,_,_,_)service->_)letforge_request=(forge_partial_request:(meth,_,_,_,_,_,_)service->_:>([<meth],unit,_,_,_,_,_)service->_)end