module Result = struct include Result let ( >>= ) : type a b e. (a, e) result -> (a -> (b, e) result) -> (b, e) result = fun r f -> match r with | Ok x -> f x | Error _ as e -> e end open Result module Meth = struct type t = [ `GET | `POST | `HEAD | `PUT | `DELETE | `OPTIONS | `TRACE | `CONNECT | `PATCH | `Other of string ] let to_string = function | `GET -> "GET" | `POST -> "POST" | `HEAD -> "HEAD" | `PUT -> "PUT" | `DELETE -> "DELETE" | `OPTIONS -> "OPTIONS" | `TRACE -> "TRACE" | `CONNECT -> "CONNECT" | `PATCH -> "PATCH" | `Other s -> s let pp fmt t = Format.fprintf fmt "%s" (to_string t) end module Header = struct type t = (string * string) list let empty = [] let to_cmd t = t |> List.map (fun (k, v) -> [ "-H"; Printf.sprintf "%s: %s" k v ]) |> List.concat let pp fmt t = Format.pp_print_list ~pp_sep:Format.pp_print_newline (fun fmt (k, v) -> Format.fprintf fmt "%s: %s\n" k v) fmt t end module Response = struct type t = Http.response = { code: int; headers: Header.t; body: string } let default = { code = 0; headers = []; body = "" } let of_stdout s = let lexbuf = Lexing.from_string s in try Ok (Http.response default lexbuf) with e -> Error e let pp fmt t = Format.fprintf fmt "{code=%d;@ headers=%a;@ body=\"%s\"}" t.code Header.pp t.headers t.body end module Process_result = struct type t = { status: Unix.process_status; stderr: string; stdout: string } let pp_process_status fmt = function | Unix.WEXITED n -> Format.fprintf fmt "Exit code %d" n | Unix.WSIGNALED n -> Format.fprintf fmt "Signal %d" n | Unix.WSTOPPED n -> Format.fprintf fmt "Stopped %d" n let pp fmt t = Format.fprintf fmt "{status=%a;@ stderr=\"%s\";@ stdout=\"%s\"}" pp_process_status t.status t.stderr t.stdout end module Error = struct type t = | Invalid_request of string | Bad_exit of Process_result.t | Failed_to_read_response of exn * Process_result.t | Exn of exn let pp fmt = function | Bad_exit p -> Format.fprintf fmt "Non 0 exit code %a@.%a" Process_result.pp_process_status p.Process_result.status Process_result.pp p | Failed_to_read_response (e, _) -> Format.fprintf fmt "Couldn't read response:@ %s" (Printexc.to_string e) | Invalid_request r -> Format.fprintf fmt "Invalid request: %s" r | Exn e -> Format.fprintf fmt "Exception: %s" (Printexc.to_string e) end module Request = struct type t = { meth: Meth.t; url: string; headers: Header.t; body: string } let make ?(headers = Header.empty) ?(body = "") ~url ~meth () = { meth; url; headers; body } let has_body t = String.length t.body > 0 let validate t = if has_body t && List.mem t.meth [ `GET; `HEAD ] then Error (Error.Invalid_request "No body is allowed with GET/HEAD methods") else Ok t let to_cmd_args t = List.concat [ [ "-X"; Meth.to_string t.meth ]; Header.to_cmd t.headers; [ t.url ]; (if has_body t then [ "--data-binary"; "@-" ] else []); ] let pp fmt t = Format.fprintf fmt "{@ meth=%a;@ url=\"%s\";@ headers=\"%a\";@ body=\"%s\"@ }" Meth.pp t.meth t.url Header.pp t.headers t.body end let result_of_process_result t = match t.Process_result.status with | Unix.WEXITED 0 -> Ok t | _ -> Error (Error.Bad_exit t) let run prog args stdin_str = let stdout, stdin, stderr = let prog = prog :: List.map Filename.quote args |> String.concat " " in Unix.open_process_full prog [||] in if String.length stdin_str > 0 then output_string stdin stdin_str; (try close_out stdin with _ -> ()); let stdout_fd = Unix.descr_of_in_channel stdout in let stderr_fd = Unix.descr_of_in_channel stderr in let in_buf, err_buf = Buffer.(create 128, create 128) in let read_buf_len = 512 in let read_buf = Bytes.create read_buf_len in let input ch = match input ch read_buf 0 read_buf_len with | 0 -> Error `Eof | s -> Ok s in let rec loop = function | [] -> () | read_list -> let can_read, _, _ = Unix.select read_list [] [] 1.0 in let to_remove = List.fold_left (fun to_remove fh -> let rr, buf = if fh = stderr_fd then input stderr, err_buf else input stdout, in_buf in match rr with | Ok len -> Buffer.add_subbytes buf read_buf 0 len; to_remove | Error `Eof -> fh :: to_remove) [] can_read in read_list |> List.filter (fun fh -> not (List.mem fh to_remove)) |> loop in ignore (loop [ stdout_fd; stderr_fd ]); let status = Unix.close_process_full (stdout, stdin, stderr) in { Process_result.status; stdout = Buffer.contents in_buf; stderr = Buffer.contents err_buf; } let run ?(exe = "curl") ?(args = []) req = Request.validate req >>= fun req -> let args = ("-si" :: Request.to_cmd_args req) @ args in let res = try result_of_process_result (run exe args req.Request.body) with e -> Error (Error.Exn e) in res >>= fun res -> match Response.of_stdout res.Process_result.stdout with | Ok r -> Ok r | Error e -> Error (Error.Failed_to_read_response (e, res)) let get ?exe ?args ?headers url = run ?exe ?args (Request.make ?headers ~url ~meth:`GET ()) let head ?exe ?args ?headers url = run ?exe ?args (Request.make ?headers ~url ~meth:`HEAD ()) let delete ?exe ?args ?headers url = run ?exe ?args (Request.make ?headers ~url ~meth:`DELETE ()) let post ?exe ?args ?headers ?body url = run ?exe ?args (Request.make ?body ?headers ~url ~meth:`POST ()) let put ?exe ?args ?headers ?body url = run ?exe ?args (Request.make ?body ?headers ~url ~meth:`PUT ())