123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182(** Type-safe request builder with phantom types.
This module provides a builder pattern for HTTP requests with compile-time
enforcement of:
- POST requires a body before execution
- GET/DELETE are ready to execute immediately
Example usage:
{[
(* GET request - returns parsed JSON list *)
new_get client "/positions"
|> query_param "user" user
|> query_option "limit" string_of_int limit
|> fetch_json_list position_of_yojson
(* POST with body *)
new_post client "/order"
|> header_list auth_headers
|> with_body body
|> fetch_json order_of_yojson
(* Raw execution for custom handling *)
new_get client "/health"
|> fetch
|> fun (status, body) -> ...
]} *)moduleC=ClientmoduleAuth=Polymarket_common.Authtypeready(** Phantom type indicating request is ready to execute *)typenot_ready(** Phantom type indicating request needs a body before execution *)typemethod_=GET|POST|DELETE|DELETE_WITH_BODYtype'statet={client:C.t;method_:method_;path:string;params:C.params;headers:(string*string)list;body:stringoption;}(** Request builder type. ['state] tracks whether request is ready to execute.
*)(** {1 Request Constructors} *)letnew_get(client:C.t)(path:string):readyt={client;method_=GET;path;params=[];headers=[];body=None}letnew_post(client:C.t)(path:string):not_readyt={client;method_=POST;path;params=[];headers=[];body=None}letnew_delete(client:C.t)(path:string):readyt={client;method_=DELETE;path;params=[];headers=[];body=None}letnew_delete_with_body(client:C.t)(path:string):not_readyt={client;method_=DELETE_WITH_BODY;path;params=[];headers=[];body=None;}(** {1 Query Parameter Builders} *)letadd_param(key:string)(value:string)(req:'at):'at={reqwithparams=(key,[value])::req.params}letquery_param(key:string)(value:string)(req:'at):'at=add_paramkeyvaluereqletquery_option(key:string)(to_string:'b->string)(value:'boption)(req:'at):'at=matchvaluewithSomev->add_paramkey(to_stringv)req|None->reqletquery_add(key:string)(value:stringoption)(req:'at):'at=query_optionkeyFun.idvaluereqletquery_bool(key:string)(value:booloption)(req:'at):'at=query_optionkeystring_of_boolvaluereqletquery_list(key:string)(to_string:'b->string)(values:'blistoption)(req:'at):'at=matchvalueswith|Some(_::_asvs)->add_paramkey(String.concat","(List.mapto_stringvs))req|_->reqletquery_each(key:string)(to_string:'b->string)(values:'blistoption)(req:'at):'at=matchvalueswith|Somevs->List.fold_left(funaccv->add_paramkey(to_stringv)acc)reqvs|None->req(** {1 Header Builders} *)letheader_add(key:string)(value:string)(req:'at):'at={reqwithheaders=(key,value)::req.headers}letheader_list(hs:(string*string)list)(req:'at):'at={reqwithheaders=hs@req.headers}(** {1 Auth} *)letwith_l1_auth~private_key~address~nonce(req:'at):'at=letheaders=Auth.build_l1_headers~private_key~address~noncein{reqwithheaders=headers@req.headers}letmethod_to_string=function|GET->"GET"|POST->"POST"|DELETE->"DELETE"|DELETE_WITH_BODY->"DELETE"letwith_l2_auth~credentials~address(req:'at):'at=letmethod_=method_to_stringreq.method_inletbody=Option.value~default:""req.bodyinletheaders=Auth.build_l2_headers~credentials~address~method_~path:req.path~bodyin{reqwithheaders=headers@req.headers}(** {1 Body} *)letwith_body(body:string)(req:not_readyt):readyt={reqwithbody=Somebody}(** {1 Execution} *)letfetch(req:readyt):int*string=leturi=C.build_uri(C.base_urlreq.client)req.pathreq.paramsinmatchreq.method_with|GET->C.do_get~headers:req.headersreq.clienturi|DELETE->C.do_delete~headers:req.headersreq.clienturi|POST->letbody_str=Option.getreq.bodyinC.do_post~headers:req.headersreq.clienturi~body:body_str|DELETE_WITH_BODY->letbody_str=Option.getreq.bodyinC.do_delete_with_body~headers:req.headersreq.clienturi~body:body_str(** {1 Response Parsers}
These execute the request and parse the response in one step. *)letfetch_json?(expected_fields:stringlistoption)?(context:string="")(parser:Yojson.Safe.t->'a)(req:readyt):('a,C.error)result=letstatus,body=fetchreqinC.handle_responsestatusbody(funb->matchexpected_fieldswith|Somefields->C.parse_with_field_check~expected_fields:fields~contextbparser|None->Json.parseparserb|>Result.map_errorC.to_error)letfetch_json_list?(expected_fields:stringlistoption)?(context:string="")(parser:Yojson.Safe.t->'a)(req:readyt):('alist,C.error)result=letstatus,body=fetchreqinC.handle_responsestatusbody(funb->matchexpected_fieldswith|Somefields->C.parse_list_with_field_check~expected_fields:fields~contextb(Ppx_yojson_conv_lib.Yojson_conv.list_of_yojsonparser)|None->Json.parse_listparserb|>Result.map_errorC.to_error)letfetch_text(req:readyt):(string,C.error)result=letstatus,body=fetchreqinC.handle_responsestatusbody(funb->Okb)letfetch_unit(req:readyt):(unit,C.error)result=letstatus,body=fetchreqinmatchstatuswith|200|201|204->Ok()|_->Error(C.parse_error~statusbody)