Compare commits

...

38 commits
v0.2.2 ... main

Author SHA1 Message Date
Simon Cruanes
a0a5b989b7
Update README.md
Some checks failed
build / Build (push) Has been cancelled
2025-05-18 01:00:41 -04:00
Antonin Décimo
27b3ca76c8 GHA: ocaml-compiler versions as strings
Some checks failed
build / Build (push) Has been cancelled
Otherwise, there's a bug where they get interpreted as floats… (it seems the bug doesn't appear here). Using strings is safer in general.
Update from OCaml 4.12 to 4.14.
2025-04-02 15:00:08 -04:00
Simon Cruanes
4b7d9ec769
make CURLOPT_NOSIGNAL=false the default; simplify
Some checks failed
build / Build (push) Has been cancelled
2025-01-29 21:55:16 -05:00
Stéphane Lavergne
ebf1e1fc12 Expose underlying no signal in a global setting 2025-01-29 21:46:19 -05:00
Simon Cruanes
742e51df97
chore: CI 2025-01-29 13:00:01 -05:00
Simon Cruanes
0faaf35969
Merge pull request #17 from c-cube/wip-stream-response
streaming response
2024-10-03 11:04:57 -04:00
Simon Cruanes
012016f9b4
test: add a test for http_stream 2024-10-03 10:01:37 -04:00
Simon Cruanes
49b265ce56
implement http_stream 2024-10-02 12:46:43 -04:00
Simon Cruanes
fbd71baa19
wip: stream response… 2024-10-02 11:43:49 -04:00
Simon Cruanes
d50a53ebda
chore: CI 2024-10-01 12:18:15 -04:00
Simon Cruanes
e97003644a
refactor: rename files 2024-10-01 12:17:53 -04:00
Simon Cruanes
48d18bd912
fix CI 2024-10-01 12:09:12 -04:00
Simon Cruanes
71a37788ac
feat: add Ezcurl.Cookies module, get/set/transfer them 2024-10-01 12:01:36 -04:00
Simon Cruanes
a8ad44d39e
feat ezcurl: wrap Curl.t in record; add cookie support 2024-10-01 10:59:55 -04:00
Simon Cruanes
5b4b71ee15
chore: update ocamlformat 2024-10-01 10:58:43 -04:00
Simon Cruanes
7c684f1869
chore: autogen opam files from dune 2024-09-04 14:21:02 -04:00
Simon Cruanes
ddf181fe83
prepare for 0.2.4 2023-11-25 00:13:05 -05:00
Simon Cruanes
d259d85eb7 depend on thread 2023-11-25 00:10:54 -05:00
Simon Cruanes
d41eb2ceb4 fix: global initialization logic is now hidden behind a mutex 2023-11-25 00:10:54 -05:00
Stéphane Lavergne
46dc012356 Typo 2023-08-09 11:46:04 -04:00
Simon Cruanes
648ca8f77f
changelog 2023-06-20 22:38:26 -04:00
Simon Cruanes
7939bfa9e3
CI: use setup-ocaml v2 for gh-pages 2023-06-09 23:17:45 -04:00
Simon Cruanes
9e0b84ecfd
makefile 2023-06-09 23:13:18 -04:00
Simon Cruanes
be03f25cc0
ocamlformat 2023-06-09 21:06:20 -04:00
Simon Cruanes
73899acdf2
more doc 2023-06-09 21:04:03 -04:00
Simon Cruanes
c6141c8b3d
prepare for 0.2.3 2023-04-04 09:49:48 -04:00
Simon Cruanes
90ad38de20
CI: disable mac OS 😡 2023-03-16 11:07:29 -04:00
Simon Cruanes
52c35dc2df
Merge pull request #13 from c-cube/fix-12
Fix #12: set size properly for POST
2023-03-13 15:36:42 -04:00
Simon Cruanes
5bb480235a
remove spurious eprintf 2023-03-09 11:10:50 -05:00
Simon Cruanes
849194728a
chore: makefile 2023-02-28 11:07:55 -05:00
Simon Cruanes
9e7d9be5c3
fix: workaround servers which do not understand Expect header 2023-02-28 11:07:20 -05:00
Simon Cruanes
91cfbc94ad
fix: correctly set size of payload for POST
close #12
2023-02-28 11:06:39 -05:00
Simon Cruanes
63392ea36b
re-disable CI on windows 2023-02-28 10:49:33 -05:00
zapashcanon
ac24129ed6
run tests on all platforms 2023-02-28 09:45:07 -05:00
zapashcanon
bb69dade38
add a basic test 2023-02-28 09:45:07 -05:00
zapashcanon
2665eda083
enable macOS and windows CI 2023-02-28 09:45:04 -05:00
Simon Cruanes
f70f25d951 make sure to setup PUT correctly
follow https://curl.se/libcurl/c/CURLOPT_UPLOAD.html and also set
the method to PUT.
2023-02-07 11:07:39 -05:00
Edwin Török
5ba64ee30a ezcurl: allow POST with non-form data
Currently ezcurl always sets CURLOPT_HTTPPOST, but that means it is not able
to send a custom body that is not a form (e.g. JSON is quite a common use-case
these days).

Add a special case: if the POST params is empty, *and* a 'content' is set, then
just set CURLOPT_POST, but not CURLOPT_HTTPPOST.

Example usage:
```
let contents = Ezjsonm.to_string json in
let config = EZ.Config.(verbose true default) in
match EZ.post ~config ~url ~params:[] ~content:(`String contents) () with
```

Signed-off-by: Edwin Török <edwin.torok@cloud.com>
2023-02-02 12:05:46 -05:00
22 changed files with 1023 additions and 613 deletions

View file

@ -18,9 +18,9 @@ jobs:
path: ~/.opam
key: opam-ubuntu-latest-4.12.0
- uses: avsm/setup-ocaml@v1
- uses: ocaml/setup-ocaml@v3
with:
ocaml-version: '4.12.0'
ocaml-compiler: '5.03'
- name: Pin
run: opam pin -n .

View file

@ -2,10 +2,10 @@ name: build
on:
push:
branches:
- master
- main
pull_request:
branches:
- master
- main
jobs:
run:
name: Build
@ -13,20 +13,20 @@ jobs:
matrix:
os:
- ubuntu-latest
#- macos-latest
#- windows-latest
# - macos-latest # build issues with `ar` (!!!)
#- windows-latest # certificate problem
ocaml-compiler:
- 4.03.x
- 4.12.x
- '4.08'
- '4.14'
- '5.3'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: ocaml/setup-ocaml@v2
- uses: ocaml/setup-ocaml@v3
with:
ocaml-compiler: ${{ matrix.ocaml-compiler }}
- run: opam pin -n .
- run: opam depext -yt ezcurl ezcurl-lwt
- run: opam install -t . --deps-only
- run: opam exec -- dune build
- run: opam exec -- dune build @install
- run: opam exec -- dune runtest
if: ${{ matrix.os == 'ubuntu-latest'}}

15
.ocamlformat Normal file
View file

@ -0,0 +1,15 @@
version = 0.26.2
profile=conventional
margin=80
if-then-else=k-r
parens-ite=true
parens-tuple=multi-line-only
sequence-style=terminator
type-decl=sparse
break-cases=toplevel
cases-exp-indent=2
field-space=tight-decl
leading-nested-match-parens=true
module-item-spacing=compact
quiet=true
ocaml-version=4.08.0

27
CHANGELOG.md Normal file
View file

@ -0,0 +1,27 @@
## 0.2.4
- fix: global initialization logic is now hidden behind a mutex
* depend on `thread`
## 0.2.3
- fix: workaround servers which do not understand "Expect" header
- fix: correctly set size of payload for POST
- make sure to setup 'PUT" correctly
- allow POST with non-form data
## 0.2.2
- fix: do not reset client if passed as argument
## 0.2.1
- fix setting of headers
## 0.2
- add default user agent
## 0.1
- initial release

View file

@ -2,18 +2,29 @@
all: build test
build:
@dune build @all
@dune build @install
test:
@dune runtest --no-buffer --force
test-autopromote:
@dune runtest --no-buffer --force --auto-promote
clean:
@dune clean
doc:
@dune build @doc
WATCH?= @install @runtest
watch:
@dune build @all -w
@dune build $(WATCH )-w
.PHONY: all build test watch
VERSION=$(shell awk '/^version:/ {print $$2}' ezcurl.opam)
update_next_tag:
@echo "update version to $(VERSION)..."
sed -i "s/NEXT_VERSION/$(VERSION)/g" $(wildcard src/**/*.ml) $(wildcard src/**/*.mli)
sed -i "s/NEXT_RELEASE/$(VERSION)/g" $(wildcard src/**/*.ml) $(wildcard src/**/*.mli)
.PHONY: all build test watch update_next_tag

View file

@ -1,6 +1,6 @@
# EZCurl [![build](https://github.com/c-cube/ezcurl/actions/workflows/main.yml/badge.svg)](https://github.com/c-cube/ezcurl/actions/workflows/main.yml)
A simple wrapper around OCurl, for easy tasks around http.
A simple wrapper around [OCurl](https://github.com/ygrek/ocurl/), for easy tasks around http.
**project goals**
@ -12,7 +12,7 @@ A simple wrapper around OCurl, for easy tasks around http.
## Installation
- for the synchronous library: `opam install ezcurl`
- for the lwt-baed library: `opam install ezcurl-lwt` (depends on `ezcurl`)
- for the lwt-based library: `opam install ezcurl-lwt` (depends on `ezcurl`)
## Usage
@ -33,7 +33,7 @@ val url : string = "https://curl.haxx.se/"
# let res = Ezcurl.get ~url ();;
...
# let content = match res with Ok c -> c | Error (_,s) -> failwith s;;
val content : Ezcurl_core.response =
val content : string Ezcurl_core.response =
...
# content.Ezcurl.code;;

4
dune
View file

@ -1,6 +1,6 @@
(alias
(name runtest)
(rule
(alias runtest)
(package ezcurl-lwt)
(deps (:file README.md))
(action

View file

@ -1 +1,41 @@
(lang dune 1.0)
(lang dune 3.0)
(name ezcurl)
(version 0.2.4)
(generate_opam_files true)
(license MIT)
(maintainers "simon.cruanes.2007@m4x.org")
(authors "Simon Cruanes")
(source (github c-cube/ezcurl))
(documentation https://c-cube.github.io/ezcurl/)
(package
(name ezcurl)
(synopsis "Friendly wrapper around OCurl")
(tags
("curl" "web" "http" "client"))
(depends
(ocurl
(>= 0.8))
(odoc :with-doc)
(ocaml
(>= 4.03))))
(package
(name ezcurl-lwt)
(synopsis "Friendly wrapper around OCurl, Lwt version")
(tags
("curl" "web" "http" "client" "lwt"))
(depends
(ezcurl
(= :version))
lwt
(mdx :with-test)
(odoc :with-doc)
(ocaml
(>= 4.03))))

View file

@ -1,26 +1,34 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
name: "ezcurl-lwt"
version: "0.2.2"
authors: ["Simon Cruanes"]
maintainer: "simon.cruanes.2007@m4x.org"
license: "MIT"
version: "0.2.4"
synopsis: "Friendly wrapper around OCurl, Lwt version"
build: [
["dune" "build" "@install" "-p" name "-j" jobs]
["dune" "build" "@doc" "-p" name] {with-doc}
#["dune" "runtest" "-p" name] {with-test}
]
depends: [
"ocurl" {>= "0.8.0"}
"ezcurl" { = version }
"lwt"
"dune" { >= "1.0" }
"odoc" {with-doc}
"mdx" {with-test}
"ocaml" { >= "4.03.0" }
]
tags: [ "curl" "web" "http" "client" "lwt" ]
homepage: "https://github.com/c-cube/ezcurl/"
doc: "https://c-cube.github.io/ezcurl/doc/1.2"
maintainer: ["simon.cruanes.2007@m4x.org"]
authors: ["Simon Cruanes"]
license: "MIT"
tags: ["curl" "web" "http" "client" "lwt"]
homepage: "https://github.com/c-cube/ezcurl"
doc: "https://c-cube.github.io/ezcurl/"
bug-reports: "https://github.com/c-cube/ezcurl/issues"
depends: [
"dune" {>= "3.0"}
"ezcurl" {= version}
"lwt"
"mdx" {with-test}
"odoc" {with-doc}
"ocaml" {>= "4.03"}
]
build: [
["dune" "subst"] {dev}
[
"dune"
"build"
"-p"
name
"-j"
jobs
"@install"
"@runtest" {with-test}
"@doc" {with-doc}
]
]
dev-repo: "git+https://github.com/c-cube/ezcurl.git"

View file

@ -1,23 +1,32 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
name: "ezcurl"
version: "0.2.2"
authors: ["Simon Cruanes"]
maintainer: "simon.cruanes.2007@m4x.org"
license: "MIT"
version: "0.2.4"
synopsis: "Friendly wrapper around OCurl"
build: [
["dune" "build" "@install" "-p" name "-j" jobs]
["dune" "build" "@doc" "-p" name] {with-doc}
#["dune" "runtest" "-p" name] {with-test}
]
depends: [
"ocurl" {>= "0.8.0"}
"dune" { >= "1.0" }
"odoc" {with-doc}
"ocaml" { >= "4.03.0" }
]
tags: [ "curl" "web" "http" "client" ]
homepage: "https://github.com/c-cube/ezcurl/"
doc: "https://c-cube.github.io/ezcurl/doc/1.2"
maintainer: ["simon.cruanes.2007@m4x.org"]
authors: ["Simon Cruanes"]
license: "MIT"
tags: ["curl" "web" "http" "client"]
homepage: "https://github.com/c-cube/ezcurl"
doc: "https://c-cube.github.io/ezcurl/"
bug-reports: "https://github.com/c-cube/ezcurl/issues"
depends: [
"dune" {>= "3.0"}
"ocurl" {>= "0.8"}
"odoc" {with-doc}
"ocaml" {>= "4.03"}
]
build: [
["dune" "subst"] {dev}
[
"dune"
"build"
"-p"
name
"-j"
jobs
"@install"
"@runtest" {with-test}
"@doc" {with-doc}
]
]
dev-repo: "git+https://github.com/c-cube/ezcurl.git"

View file

@ -1,373 +0,0 @@
let opt_iter ~f = function None -> () | Some x -> f x
module Config = struct
type t = {
verbose: bool;
authmethod: Curl.curlAuth list option;
max_redirects: int;
follow_location: bool;
username: string option;
password: string option;
user_agent: string option;
}
let default : t = {
verbose=false;
max_redirects = 50;
follow_location=true;
authmethod=None;
username=None;
password=None;
user_agent=Some "curl";
}
let password x self = {self with password=Some x}
let username x self = {self with username=Some x}
let verbose x self = { self with verbose=x}
let follow_location x self = {self with follow_location=x}
let max_redirects x self = {self with max_redirects=max 1 x}
let authmethod x self = {self with authmethod=Some x}
let string_of_authmethod = function
| Curl.CURLAUTH_ANY -> "any"
| Curl.CURLAUTH_BASIC -> "basic"
| Curl.CURLAUTH_DIGEST -> "digest"
| Curl.CURLAUTH_GSSNEGOTIATE -> "gss_negotiate"
| Curl.CURLAUTH_NTLM -> "ntlm"
| Curl.CURLAUTH_ANYSAFE -> "any_safe"
let str_of_str_opt = function
| None -> "<none>"
| Some s -> s
let pp out (self:t) =
let {
verbose; authmethod; max_redirects; follow_location;
username; password; user_agent
} = self in
Format.fprintf out
"{@[verbose=%B;@ max_redirects=%d;@ follow_location=%B;@ \
username=%s;@ password=%s;@ authmethod=%s;@ user_agent=%s@]}"
verbose max_redirects follow_location
(str_of_str_opt username) (str_of_str_opt password)
(match authmethod with
| None -> "none"
| Some l -> List.map string_of_authmethod l |> String.concat ",") (str_of_str_opt user_agent)
let to_string s = Format.asprintf "%a" pp s
end
type t = Curl.t
let _init = lazy (
Curl.global_init Curl.CURLINIT_GLOBALALL;
at_exit Curl.global_cleanup;
)
let make ?(set_opts=fun _ -> ()) () : t =
Lazy.force _init;
let c = Curl.init () in
Gc.finalise Curl.cleanup c;
set_opts c;
c
let delete = Curl.cleanup
(* set options *)
let _apply_config (self:t) (config:Config.t) : unit =
let {
Config.verbose; max_redirects; follow_location; authmethod;
username; password; user_agent
} = config in
Curl.set_verbose self verbose;
Curl.set_maxredirs self max_redirects;
Curl.set_followlocation self follow_location;
opt_iter user_agent ~f:(fun user_agent -> Curl.set_useragent self user_agent);
opt_iter authmethod ~f:(Curl.set_httpauth self);
opt_iter username ~f:(Curl.set_username self);
opt_iter password ~f:(Curl.set_password self);
()
let _set_headers (self:t) (headers: _ list) : unit =
let headers =
List.map (fun (k,v) -> k ^ ": " ^ v) headers
in
Curl.set_httpheader self headers;
()
let with_client ?set_opts f =
let c = make ?set_opts () in
try
let x = f c in
delete c;
x
with e ->
delete c;
raise e
type response_info = {
ri_response_time: float;
ri_redirect_count: int;
}
let pp_response_info out r =
let {ri_response_time; ri_redirect_count} = r in
Format.fprintf out "{@[response_time=%.3fs;@ redirect_count=%d@]}"
ri_response_time ri_redirect_count
let string_of_response_info s = Format.asprintf "%a" pp_response_info s
type response = {
code: int;
headers: (string * string) list;
body: string;
info: response_info;
}
let pp_response out r =
let pp_header out (s1,s2) =
Format.fprintf out "@[<2>%s:@ %s@]" s1 s2
in
let pp_headers out l =
Format.fprintf out "@[<v>%a@]" (Format.pp_print_list pp_header) l
in
let {code; body; headers; info; } = r in
Format.fprintf out "{@[code=%d;@ headers=@[%a@];@ info=%a;@ body=@[%a@]@]}"
code pp_headers headers pp_response_info info
Format.pp_print_text body
let string_of_response s = Format.asprintf "%a" pp_response s
type meth =
| GET
| POST of Curl.curlHTTPPost list
| PUT
| DELETE
| HEAD
| CONNECT
| OPTIONS
| TRACE
| PATCH
let string_of_meth = function
| GET -> "GET"
| POST _ -> "POST"
| PUT -> "PUT"
| DELETE -> "DELETE"
| HEAD -> "HEAD"
| CONNECT -> "CONNECT"
| OPTIONS -> "OPTIONS"
| TRACE -> "TRACE"
| PATCH -> "PATCH"
let pp_meth out m = Format.pp_print_string out (string_of_meth m)
module type IO = sig
type 'a t
val return : 'a -> 'a t
val (>>=) : 'a t -> ('a -> 'b t) -> 'b t
val (>|=) : 'a t -> ('a -> 'b) -> 'b t
val fail : exn -> 'a t
val perform : Curl.t -> Curl.curlCode t
end
module type S = sig
type 'a io
val http :
?tries:int ->
?client:t ->
?config:Config.t ->
?range:string ->
?content:[`String of string | `Write of (bytes -> int -> int)] ->
?headers:(string*string) list ->
url:string ->
meth:meth ->
unit ->
(response, Curl.curlCode * string) result io
(** General purpose HTTP call via cURL.
@param url the URL to query
@param meth which method to use (see {!meth})
@param tries how many times to retry in case of [CURLE_AGAIN] code
@param client a client to reuse (instead of allocating a new one)
@param range an optional
{{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests} byte range}
to fetch (either to get large pages
by chunks, or to resume an interrupted download).
@param config configuration to set
@param content the content to send as the query's body, either
a [`String s] to write a single string, or [`Write f]
where [f] is a callback that is called on a buffer [b] with len [n]
(as in [f b n]) and returns how many bytes it wrote in the buffer
[b] starting at index [0] (at most [n] bytes).
It must return [0] when the content is entirely written, and not
before.
@param headers headers of the query
*)
val get :
?tries:int ->
?client:t ->
?config:Config.t ->
?range:string ->
?headers:(string*string) list ->
url:string ->
unit ->
(response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:GET]
See {!http} for more info.
*)
val put :
?tries:int ->
?client:t ->
?config:Config.t ->
?headers:(string*string) list ->
url:string ->
content:[`String of string | `Write of (bytes -> int -> int)] ->
unit ->
(response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:PUT]
See {!http} for more info.
*)
val post :
?tries:int ->
?client:t ->
?config:Config.t ->
?headers:(string*string) list ->
?content:[`String of string | `Write of (bytes -> int -> int)] ->
params:Curl.curlHTTPPost list ->
url:string ->
unit ->
(response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:(POST params)]
See {!http} for more info.
*)
end
exception Parse_error of Curl.curlCode * string
let mk_res (self:t) headers body : (response,_) result =
let split_colon s =
match String.index s ':' with
| exception Not_found ->
raise (Parse_error (Curl.CURLE_CONV_FAILED, "header line without a ':': " ^ s))
| i ->
String.sub s 0 i,
String.trim (String.sub s (i+1) (String.length s-i-1))
in
try
let code = Curl.get_httpcode self in
let headers =
match headers with
| [] -> []
| _ :: tl -> List.map split_colon tl (* first one is "http1.1 NNN <descr>" *)
in
let info = {
ri_redirect_count=Curl.get_redirectcount self;
ri_response_time=Curl.get_totaltime self;
} in
Ok {headers; code; body; info}
with Parse_error (e, msg) ->
Error (e, Curl.strerror e ^ ": " ^ msg)
module Make(IO : IO)
: S with type 'a io = 'a IO.t
= struct
open IO
type 'a io = 'a IO.t
let content_read_fun_ content =
match content with
| `String s ->
let n = ref 0 in
(fun i ->
let len = min i (String.length s - !n) in
let r = String.sub s !n len in
n := !n + len;
r)
| `Write f ->
let buf = Bytes.create 1024 in
(fun i ->
let len = min i (Bytes.length buf) in
let n = f buf len in
Bytes.sub_string buf i n)
let http
?(tries=1) ?client ?(config=Config.default) ?range ?content ?(headers=[]) ~url ~meth ()
: _ result io =
let do_cleanup, self = match client with
| None -> true, make()
| Some c ->
false, c
in
_apply_config self config;
opt_iter range ~f:(fun s -> Curl.set_range self s);
(* TODO: ability to make content a stream with a `read` function *)
opt_iter content
~f:(fun content ->
Curl.set_readfunction self (content_read_fun_ content));
(* local state *)
let tries = max tries 1 in (* at least one attempt *)
let body = Buffer.create 64 in
let resp_headers = ref [] in
let resp_headers_done = ref false in (* once we get "\r\n" header line *)
Curl.set_url self url;
begin match meth with
| POST l -> Curl.set_httppost self l;
| GET -> Curl.set_httpget self true;
| PUT -> Curl.set_put self true;
| DELETE -> Curl.set_customrequest self "DELETE";
| HEAD -> Curl.set_customrequest self "HEAD"
| CONNECT -> Curl.set_customrequest self "CONNECT"
| OPTIONS -> Curl.set_customrequest self "OPTIONS"
| TRACE -> Curl.set_customrequest self "TRACE"
| PATCH -> Curl.set_customrequest self "PATCH"
end;
_set_headers self headers;
Curl.set_headerfunction self
(fun s0 ->
let s = String.trim s0 in
(* Printf.printf "got header %S\n%!" s0; *)
if s0 = "\r\n" then (
resp_headers_done := true;
) else (
(* redirection: drop previous headers *)
if !resp_headers_done then (
resp_headers_done := false;
resp_headers := [];
);
resp_headers := s :: !resp_headers;
);
String.length s0);
Curl.set_writefunction self
(fun s ->
Buffer.add_string body s;
String.length s);
let rec loop i =
IO.perform self >>= function
| Curl.CURLE_OK ->
let r = mk_res self (List.rev !resp_headers) (Buffer.contents body) in
if do_cleanup then Curl.cleanup self;
return r
| Curl.CURLE_AGAIN when i > 1 ->
loop (i-1) (* try again *)
| c ->
if do_cleanup then Curl.cleanup self;
return (Error (c, Curl.strerror c))
in
loop tries
let get ?tries ?client ?config ?range ?headers ~url () : _ result io =
http ?tries ?client ?config ?range ?headers ~url ~meth:GET ()
let post ?tries ?client ?config ?headers ?content ~params ~url () : _ result io =
http ?tries ?client ?config ?headers ?content ~url ~meth:(POST params) ()
let put ?tries ?client ?config ?headers ~url ~content () : _ result io =
http ?tries ?client ?config ?headers ~url ~content ~meth:PUT ()
end

View file

@ -1,155 +0,0 @@
(** {1 Core signatures and implementation} *)
module Config : sig
type t
val default : t
val verbose : bool -> t -> t
val authmethod : Curl.curlAuth list -> t -> t
val max_redirects : int -> t -> t
val follow_location : bool -> t -> t
val username : string -> t -> t
val password : string -> t -> t
val pp : Format.formatter -> t -> unit
val to_string : t -> string
end
type t = Curl.t
val make :
?set_opts:(t -> unit) ->
unit -> t
val delete : t -> unit
val with_client :
?set_opts:(t -> unit) ->
(t -> 'a) -> 'a
(** Make a temporary client, call the function with it, then cleanup *)
(* TODO: duphandle is deprecated, how do we iterate on options?
val copy : t -> t
*)
type response_info = {
ri_response_time: float;
ri_redirect_count: int;
}
val pp_response_info : Format.formatter -> response_info -> unit
val string_of_response_info : response_info -> string
type response = {
code: int;
headers: (string * string) list;
body: string;
info: response_info;
}
val pp_response : Format.formatter -> response -> unit
val string_of_response : response -> string
(** The {{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods} HTTP method}
to use *)
type meth =
| GET
| POST of Curl.curlHTTPPost list
| PUT
| DELETE
| HEAD
| CONNECT
| OPTIONS
| TRACE
| PATCH
val pp_meth : Format.formatter -> meth -> unit
val string_of_meth : meth -> string
(** {2 Underlying IO Monad} *)
module type IO = sig
type 'a t
val return : 'a -> 'a t
val (>>=) : 'a t -> ('a -> 'b t) -> 'b t
val (>|=) : 'a t -> ('a -> 'b) -> 'b t
val fail : exn -> 'a t
val perform : Curl.t -> Curl.curlCode t
end
(** {2 Main Signature} *)
module type S = sig
type 'a io
val http :
?tries:int ->
?client:t ->
?config:Config.t ->
?range:string ->
?content:[`String of string | `Write of (bytes -> int -> int)] ->
?headers:(string*string) list ->
url:string ->
meth:meth ->
unit ->
(response, Curl.curlCode * string) result io
(** General purpose HTTP call via cURL.
@param url the URL to query
@param meth which method to use (see {!meth})
@param tries how many times to retry in case of [CURLE_AGAIN] code
@param client a client to reuse (instead of allocating a new one)
@param range an optional
{{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests} byte range}
to fetch (either to get large pages
by chunks, or to resume an interrupted download).
@param config configuration to set
@param content the content to send as the query's body, either
a [`String s] to write a single string, or [`Write f]
where [f] is a callback that is called on a buffer [b] with len [n]
(as in [f b n]) and returns how many bytes it wrote in the buffer
[b] starting at index [0] (at most [n] bytes).
It must return [0] when the content is entirely written, and not
before.
@param headers headers of the query
*)
val get :
?tries:int ->
?client:t ->
?config:Config.t ->
?range:string ->
?headers:(string*string) list ->
url:string ->
unit ->
(response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:GET]
See {!http} for more info.
*)
val put :
?tries:int ->
?client:t ->
?config:Config.t ->
?headers:(string*string) list ->
url:string ->
content:[`String of string | `Write of (bytes -> int -> int)] ->
unit ->
(response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:PUT]
See {!http} for more info.
*)
val post :
?tries:int ->
?client:t ->
?config:Config.t ->
?headers:(string*string) list ->
?content:[`String of string | `Write of (bytes -> int -> int)] ->
params:Curl.curlHTTPPost list ->
url:string ->
unit ->
(response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:(POST params)]
See {!http} for more info.
*)
end
module Make(IO : IO) : S with type 'a io = 'a IO.t

View file

@ -3,4 +3,4 @@
(name ezcurl_core)
(public_name ezcurl.core)
(flags :standard -warn-error -32)
(libraries curl))
(libraries curl threads))

524
src/core/ezcurl_core.ml Normal file
View file

@ -0,0 +1,524 @@
let opt_iter ~f = function
| None -> ()
| Some x -> f x
module Config = struct
type t = {
verbose: bool;
authmethod: Curl.curlAuth list option;
max_redirects: int;
follow_location: bool;
username: string option;
password: string option;
user_agent: string option;
}
let default : t =
{
verbose = false;
max_redirects = 50;
follow_location = true;
authmethod = None;
username = None;
password = None;
user_agent = Some "curl";
}
let password x self = { self with password = Some x }
let username x self = { self with username = Some x }
let verbose x self = { self with verbose = x }
let follow_location x self = { self with follow_location = x }
let max_redirects x self = { self with max_redirects = max 1 x }
let authmethod x self = { self with authmethod = Some x }
let string_of_authmethod = function
| Curl.CURLAUTH_ANY -> "any"
| Curl.CURLAUTH_BASIC -> "basic"
| Curl.CURLAUTH_DIGEST -> "digest"
| Curl.CURLAUTH_GSSNEGOTIATE -> "gss_negotiate"
| Curl.CURLAUTH_NTLM -> "ntlm"
| Curl.CURLAUTH_ANYSAFE -> "any_safe"
let str_of_str_opt = function
| None -> "<none>"
| Some s -> s
let pp out (self : t) =
let {
verbose;
authmethod;
max_redirects;
follow_location;
username;
password;
user_agent;
} =
self
in
Format.fprintf out
"{@[verbose=%B;@ max_redirects=%d;@ follow_location=%B;@ username=%s;@ \
password=%s;@ authmethod=%s;@ user_agent=%s@]}"
verbose max_redirects follow_location (str_of_str_opt username)
(str_of_str_opt password)
(match authmethod with
| None -> "none"
| Some l -> List.map string_of_authmethod l |> String.concat ",")
(str_of_str_opt user_agent)
let to_string s = Format.asprintf "%a" pp s
end
type t = { curl: Curl.t } [@@unboxed]
type client = t
let _top_mutex = Mutex.create ()
let _with_mutex f =
Mutex.lock _top_mutex;
match f () with
| res ->
Mutex.unlock _top_mutex;
res
| exception e ->
Mutex.unlock _top_mutex;
raise e
let _init =
let initialized = ref false in
fun () ->
_with_mutex @@ fun () ->
if not !initialized then (
initialized := true;
Curl.global_init Curl.CURLINIT_GLOBALALL;
at_exit Curl.global_cleanup
)
let make ?(set_opts = fun _ -> ()) ?cookiejar_file
?(enable_session_cookies = false) () : t =
_init ();
let curl = Curl.init () in
Gc.finalise Curl.cleanup curl;
opt_iter cookiejar_file ~f:(fun file ->
Curl.set_cookiejar curl file;
Curl.set_cookiefile curl file);
if enable_session_cookies then Curl.set_cookiefile curl "";
set_opts curl;
{ curl }
let delete (self : t) = Curl.cleanup self.curl
let _cfg_no_signal = ref false (* default: 0 *)
let _get_no_signal () : bool = _with_mutex @@ fun () -> !_cfg_no_signal
let set_no_signal v = _with_mutex @@ fun () -> _cfg_no_signal := v
module Cookies = struct
let reload_cookiejar (self : t) : unit =
Curl.set_cookielist self.curl "RELOAD"
let flush_cookiejar (self : t) : unit = Curl.set_cookielist self.curl "FLUSH"
let get_cookies self = Curl.get_cookielist self.curl
let set_cookies self (l : string list) =
List.iter (Curl.set_cookielist self.curl) l
let transfer c1 c2 = set_cookies c2 @@ get_cookies c1
end
(* set options *)
let _apply_config (self : t) (config : Config.t) : unit =
let {
Config.verbose;
max_redirects;
follow_location;
authmethod;
username;
password;
user_agent;
} =
config
in
Curl.set_verbose self.curl verbose;
Curl.set_maxredirs self.curl max_redirects;
Curl.set_followlocation self.curl follow_location;
opt_iter user_agent ~f:(fun user_agent ->
Curl.set_useragent self.curl user_agent);
opt_iter authmethod ~f:(Curl.set_httpauth self.curl);
opt_iter username ~f:(Curl.set_username self.curl);
opt_iter password ~f:(Curl.set_password self.curl);
Curl.set_nosignal self.curl (_get_no_signal ());
()
let _set_headers (self : t) (headers : _ list) : unit =
let headers = List.map (fun (k, v) -> k ^ ": " ^ v) headers in
Curl.set_httpheader self.curl headers;
()
let with_client ?set_opts f =
let c = make ?set_opts () in
try
let x = f c in
delete c;
x
with e ->
delete c;
raise e
type response_info = {
ri_response_time: float;
ri_redirect_count: int;
}
let pp_response_info out r =
let { ri_response_time; ri_redirect_count } = r in
Format.fprintf out "{@[response_time=%.3fs;@ redirect_count=%d@]}"
ri_response_time ri_redirect_count
let string_of_response_info s = Format.asprintf "%a" pp_response_info s
type 'body response = {
code: int;
headers: (string * string) list;
body: 'body;
info: response_info;
}
let pp_response_with ppbody out r =
let pp_header out (s1, s2) = Format.fprintf out "@[<2>%s:@ %s@]" s1 s2 in
let pp_headers out l =
Format.fprintf out "@[<v>%a@]" (Format.pp_print_list pp_header) l
in
let { code; body; headers; info } = r in
Format.fprintf out "{@[code=%d;@ headers=@[%a@];@ info=%a;@ body=@[%a@]@]}"
code pp_headers headers pp_response_info info ppbody body
let pp_response = pp_response_with Format.pp_print_text
let string_of_response s = Format.asprintf "%a" pp_response s
type meth =
| GET
| POST of Curl.curlHTTPPost list
| PUT
| DELETE
| HEAD
| CONNECT
| OPTIONS
| TRACE
| PATCH
let string_of_meth = function
| GET -> "GET"
| POST _ -> "POST"
| PUT -> "PUT"
| DELETE -> "DELETE"
| HEAD -> "HEAD"
| CONNECT -> "CONNECT"
| OPTIONS -> "OPTIONS"
| TRACE -> "TRACE"
| PATCH -> "PATCH"
let pp_meth out m = Format.pp_print_string out (string_of_meth m)
module type IO = sig
type 'a t
val return : 'a -> 'a t
val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
val ( >|= ) : 'a t -> ('a -> 'b) -> 'b t
val fail : exn -> 'a t
val perform : Curl.t -> Curl.curlCode t
end
module type S = sig
type 'a io
val http :
?tries:int ->
?client:t ->
?config:Config.t ->
?range:string ->
?content:[ `String of string | `Write of bytes -> int -> int ] ->
?headers:(string * string) list ->
url:string ->
meth:meth ->
unit ->
(string response, Curl.curlCode * string) result io
(** General purpose HTTP call via cURL.
@param url the URL to query
@param meth which method to use (see {!meth})
@param tries how many times to retry in case of [CURLE_AGAIN] code
@param client a client to reuse (instead of allocating a new one)
@param range an optional
{{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests} byte range}
to fetch (either to get large pages
by chunks, or to resume an interrupted download).
@param config configuration to set
@param content the content to send as the query's body, either
a [`String s] to write a single string, or [`Write f]
where [f] is a callback that is called on a buffer [b] with len [n]
(as in [f b n]) and returns how many bytes it wrote in the buffer
[b] starting at index [0] (at most [n] bytes).
It must return [0] when the content is entirely written, and not
before.
@param headers headers of the query
*)
(** Push-stream of bytes
@since NEXT_RELEASE *)
class type input_stream = object
method on_close : unit -> unit
method on_input : bytes -> int -> int -> unit
end
val http_stream :
?tries:int ->
?client:t ->
?config:Config.t ->
?range:string ->
?content:[ `String of string | `Write of bytes -> int -> int ] ->
?headers:(string * string) list ->
url:string ->
meth:meth ->
write_into:#input_stream ->
unit ->
(unit response, Curl.curlCode * string) result io
(** HTTP call via cURL, with a streaming response body.
@since NEXT_RELEASE *)
val get :
?tries:int ->
?client:t ->
?config:Config.t ->
?range:string ->
?headers:(string * string) list ->
url:string ->
unit ->
(string response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:GET]
See {!http} for more info.
*)
val put :
?tries:int ->
?client:t ->
?config:Config.t ->
?headers:(string * string) list ->
url:string ->
content:[ `String of string | `Write of bytes -> int -> int ] ->
unit ->
(string response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:PUT]
See {!http} for more info.
*)
val post :
?tries:int ->
?client:t ->
?config:Config.t ->
?headers:(string * string) list ->
?content:[ `String of string | `Write of bytes -> int -> int ] ->
params:Curl.curlHTTPPost list ->
url:string ->
unit ->
(string response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:(POST params)]
See {!http} for more info.
*)
end
exception Parse_error of Curl.curlCode * string
let mk_res (self : t) headers body : (_ response, _) result =
let split_colon s =
match String.index s ':' with
| exception Not_found ->
raise
(Parse_error (Curl.CURLE_CONV_FAILED, "header line without a ':': " ^ s))
| i ->
( String.sub s 0 i,
String.trim (String.sub s (i + 1) (String.length s - i - 1)) )
in
try
let code = Curl.get_httpcode self.curl in
let headers =
match headers with
| [] -> []
| _ :: tl ->
List.map split_colon tl (* first one is "http1.1 NNN <descr>" *)
in
let info =
{
ri_redirect_count = Curl.get_redirectcount self.curl;
ri_response_time = Curl.get_totaltime self.curl;
}
in
Ok { headers; code; body; info }
with Parse_error (e, msg) -> Error (e, Curl.strerror e ^ ": " ^ msg)
module Make (IO : IO) : S with type 'a io = 'a IO.t = struct
open IO
type 'a io = 'a IO.t
let content_read_fun_ content =
match content with
| `String s ->
let n = ref 0 in
fun i ->
let len = min i (String.length s - !n) in
let r = String.sub s !n len in
n := !n + len;
r
| `Write f ->
let buf = Bytes.create 1024 in
fun i ->
let len = min i (Bytes.length buf) in
let n = f buf len in
Bytes.sub_string buf i n
let content_size_ = function
| `String s -> Some (String.length s)
| `Write _ -> None
class type input_stream = object
method on_close : unit -> unit
method on_input : bytes -> int -> int -> unit
end
type http_state_ = {
client: client;
do_cleanup: bool;
mutable resp_headers: string list;
mutable resp_headers_done: bool;
}
let http_setup_ ?client ?(config = Config.default) ?range ?content
?(headers = []) ~url ~meth () : http_state_ =
let headers = ref headers in
let do_cleanup, self =
match client with
| None -> true, make ()
| Some c -> false, c
in
_apply_config self config;
opt_iter range ~f:(fun s -> Curl.set_range self.curl s);
(* TODO: ability to make content a stream with a `read` function *)
opt_iter content ~f:(fun content ->
Curl.set_readfunction self.curl (content_read_fun_ content);
(* also set size if known *)
match content_size_ content, meth with
| None, _ ->
headers :=
("expect", "") :: ("transfer-encoding", "chunked") :: !headers
| Some size, POST _ -> Curl.set_postfieldsize self.curl size
| Some size, _ -> Curl.set_infilesize self.curl size);
(* local state *)
let st =
{
do_cleanup;
client = self;
resp_headers = [];
resp_headers_done = false;
}
in
(* once we get "\r\n" header line *)
Curl.set_url self.curl url;
(match meth with
| POST [] when content <> None -> Curl.set_post self.curl true
| POST l -> Curl.set_httppost self.curl l
| GET -> Curl.set_httpget self.curl true
| PUT ->
Curl.set_customrequest self.curl "PUT";
Curl.set_upload self.curl true
| DELETE -> Curl.set_customrequest self.curl "DELETE"
| HEAD -> Curl.set_customrequest self.curl "HEAD"
| CONNECT -> Curl.set_customrequest self.curl "CONNECT"
| OPTIONS -> Curl.set_customrequest self.curl "OPTIONS"
| TRACE -> Curl.set_customrequest self.curl "TRACE"
| PATCH -> Curl.set_customrequest self.curl "PATCH");
_set_headers self !headers;
Curl.set_headerfunction self.curl (fun s0 ->
let s = String.trim s0 in
(* Printf.printf "got header %S\n%!" s0; *)
if s0 = "\r\n" then
st.resp_headers_done <- true
else (
(* redirection: drop previous headers *)
if st.resp_headers_done then (
st.resp_headers_done <- false;
st.resp_headers <- []
);
st.resp_headers <- s :: st.resp_headers
);
String.length s0);
st
let http ?(tries = 1) ?client ?config ?range ?content ?headers ~url ~meth () :
(string response, _) result io =
(* at least one attempt *)
let tries = max tries 1 in
let st =
http_setup_ ?client ?config ?range ?content ?headers ~url ~meth ()
in
let body = Buffer.create 64 in
Curl.set_writefunction st.client.curl (fun s ->
Buffer.add_string body s;
String.length s);
let rec loop i =
IO.perform st.client.curl >>= function
| Curl.CURLE_OK ->
let r =
mk_res st.client (List.rev st.resp_headers) (Buffer.contents body)
in
if st.do_cleanup then Curl.cleanup st.client.curl;
return r
| Curl.CURLE_AGAIN when i > 1 -> loop (i - 1) (* try again *)
| c ->
if st.do_cleanup then Curl.cleanup st.client.curl;
return (Error (c, Curl.strerror c))
in
loop tries
let http_stream ?(tries = 1) ?client ?config ?range ?content ?headers ~url
~meth ~(write_into : #input_stream) () : (unit response, _) result io =
let tries = max tries 1 in
let st =
http_setup_ ?client ?config ?range ?content ?headers ~url ~meth ()
in
Curl.set_writefunction st.client.curl (fun s ->
let n = String.length s in
write_into#on_input (Bytes.unsafe_of_string s) 0 n;
n);
let rec loop i =
IO.perform st.client.curl >>= function
| Curl.CURLE_OK ->
let r = mk_res st.client (List.rev st.resp_headers) () in
write_into#on_close ();
if st.do_cleanup then Curl.cleanup st.client.curl;
return r
| Curl.CURLE_AGAIN when i > 1 -> loop (i - 1) (* try again *)
| c ->
write_into#on_close ();
if st.do_cleanup then Curl.cleanup st.client.curl;
return (Error (c, Curl.strerror c))
in
loop tries
let get ?tries ?client ?config ?range ?headers ~url () : _ result io =
http ?tries ?client ?config ?range ?headers ~url ~meth:GET ()
let post ?tries ?client ?config ?headers ?content ~params ~url () :
_ result io =
http ?tries ?client ?config ?headers ?content ~url ~meth:(POST params) ()
let put ?tries ?client ?config ?headers ~url ~content () : _ result io =
http ?tries ?client ?config ?headers ~url ~content ~meth:PUT ()
end

229
src/core/ezcurl_core.mli Normal file
View file

@ -0,0 +1,229 @@
(** Core signatures and implementation *)
(** Configuration for the client. *)
module Config : sig
type t
val default : t
val verbose : bool -> t -> t
val authmethod : Curl.curlAuth list -> t -> t
val max_redirects : int -> t -> t
val follow_location : bool -> t -> t
val username : string -> t -> t
val password : string -> t -> t
val pp : Format.formatter -> t -> unit
val to_string : t -> string
end
type t = private { curl: Curl.t } [@@unboxed]
(** A client, i.e. a cURL instance.
The wrapping record has been present since NEXT_RELEASE *)
val make :
?set_opts:(Curl.t -> unit) ->
?cookiejar_file:string ->
?enable_session_cookies:bool ->
unit ->
t
(** Create a new client.
@param set_opts called before returning the client, to set options
@param cookiejar_file if provided, tell curl to use the given file path to store/load cookies (since NEXT_RELEASE)
@param enable_session_cookies if provided, enable cookie handling in curl so it store/load cookies (since NEXT_RELEASE)
*)
val delete : t -> unit
(** Delete the client. It cannot be used anymore. *)
val with_client : ?set_opts:(Curl.t -> unit) -> (t -> 'a) -> 'a
(** Make a temporary client, call the function with it, then cleanup. *)
val set_no_signal : bool -> unit
(** Set no_signal default value for each new client instance. Default is [true].
See [CURLOPT_NOSIGNAL].
@since NEXT_RELEASE *)
(** Cookie handling.
@since NEXT_RELEASE *)
module Cookies : sig
val flush_cookiejar : t -> unit
(** If [cookiejar_file] was provided in {!make}, this flushes the current set of cookies
to the provided file.
@since NEXT_RELEASE *)
val reload_cookiejar : t -> unit
(** If [cookiejar_file] was provided in {!make}, this reloads cookies from
the provided file.
@since NEXT_RELEASE *)
val get_cookies : t -> string list
(** Get cookie list (in netscape format) *)
val set_cookies : t -> string list -> unit
(** Set cookie list (in netscape format) *)
val transfer : t -> t -> unit
(** [transfer c1 c2] copies cookies in [c1] into [c2] *)
end
(* TODO: duphandle is deprecated, how do we iterate on options?
val copy : t -> t
*)
type response_info = {
ri_response_time: float;
(** Total time (in seconds) for the request/response pair.
See {!Curl.get_totaltime}. *)
ri_redirect_count: int;
(** Number of redirects cURL followed.
See {!Curl.get_redirectcount}. *)
}
(** Metadata about a response from the server. *)
val pp_response_info : Format.formatter -> response_info -> unit
val string_of_response_info : response_info -> string
type 'body response = {
code: int;
(** Response code. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status *)
headers: (string * string) list; (** Response headers *)
body: 'body; (** Response body, or [""] *)
info: response_info; (** Information about the response *)
}
(** Response for a given request. *)
val pp_response_with :
(Format.formatter -> 'a -> unit) -> Format.formatter -> 'a response -> unit
val pp_response : Format.formatter -> string response -> unit
val string_of_response : string response -> string
(** The {{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods} HTTP method}
to use *)
type meth =
| GET
| POST of Curl.curlHTTPPost list
| PUT
| DELETE
| HEAD
| CONNECT
| OPTIONS
| TRACE
| PATCH
val pp_meth : Format.formatter -> meth -> unit
val string_of_meth : meth -> string
(** {2 Underlying IO Monad} *)
module type IO = sig
type 'a t
val return : 'a -> 'a t
val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
val ( >|= ) : 'a t -> ('a -> 'b) -> 'b t
val fail : exn -> 'a t
val perform : Curl.t -> Curl.curlCode t
end
(** {2 Main Signature} *)
module type S = sig
type 'a io
val http :
?tries:int ->
?client:t ->
?config:Config.t ->
?range:string ->
?content:[ `String of string | `Write of bytes -> int -> int ] ->
?headers:(string * string) list ->
url:string ->
meth:meth ->
unit ->
(string response, Curl.curlCode * string) result io
(** General purpose HTTP call via cURL.
@param url the URL to query
@param meth which method to use (see {!meth})
@param tries how many times to retry in case of [CURLE_AGAIN] code
@param client a client to reuse (instead of allocating a new one)
@param range an optional
{{: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests} byte range}
to fetch (either to get large pages
by chunks, or to resume an interrupted download).
@param config configuration to set
@param content the content to send as the query's body, either
a [`String s] to write a single string, or [`Write f]
where [f] is a callback that is called on a buffer [b] with len [n]
(as in [f b n]) and returns how many bytes it wrote in the buffer
[b] starting at index [0] (at most [n] bytes).
It must return [0] when the content is entirely written, and not
before.
@param headers headers of the query
*)
(** Push-based stream of bytes
@since NEXT_RELEASE *)
class type input_stream = object
method on_close : unit -> unit
method on_input : bytes -> int -> int -> unit
end
val http_stream :
?tries:int ->
?client:t ->
?config:Config.t ->
?range:string ->
?content:[ `String of string | `Write of bytes -> int -> int ] ->
?headers:(string * string) list ->
url:string ->
meth:meth ->
write_into:#input_stream ->
unit ->
(unit response, Curl.curlCode * string) result io
(** HTTP call via cURL, with a streaming response body.
The body is given to [write_into] by chunks,
then [write_into#on_close ()] is called
and the response is returned.
@since NEXT_RELEASE *)
val get :
?tries:int ->
?client:t ->
?config:Config.t ->
?range:string ->
?headers:(string * string) list ->
url:string ->
unit ->
(string response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:GET]
See {!http} for more info.
*)
val put :
?tries:int ->
?client:t ->
?config:Config.t ->
?headers:(string * string) list ->
url:string ->
content:[ `String of string | `Write of bytes -> int -> int ] ->
unit ->
(string response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:PUT]
See {!http} for more info.
*)
val post :
?tries:int ->
?client:t ->
?config:Config.t ->
?headers:(string * string) list ->
?content:[ `String of string | `Write of bytes -> int -> int ] ->
params:Curl.curlHTTPPost list ->
url:string ->
unit ->
(string response, Curl.curlCode * string) result io
(** Shortcut for [http ~meth:(POST params)]
See {!http} for more info.
*)
end
module Make (IO : IO) : S with type 'a io = 'a IO.t

View file

@ -1,7 +0,0 @@
include Ezcurl_core
include Make(struct
include Lwt
let perform = Curl_lwt.perform
end)

7
src/lwt/ezcurl_lwt.ml Normal file
View file

@ -0,0 +1,7 @@
include Ezcurl_core
include Make (struct
include Lwt
let perform = Curl_lwt.perform
end)

View file

@ -1,16 +0,0 @@
(** {1 Synchronous API} *)
include Ezcurl_core
include Ezcurl_core.Make(struct
type 'a t = 'a
let return x = x
let (>>=) x f = f x
let (>|=) x f = f x
let fail e = raise e
let perform c =
try Curl.perform c; Curl.CURLE_OK
with Curl.CurlException (c, _, _) -> c
end)

19
src/sync/ezcurl.ml Normal file
View file

@ -0,0 +1,19 @@
(** {1 Synchronous API} *)
include Ezcurl_core
include Ezcurl_core.Make (struct
type 'a t = 'a
let return x = x
let ( >>= ) x f = f x
let ( >|= ) x f = f x
let fail e = raise e
let perform c =
try
Curl.perform c;
Curl.CURLE_OK
with Curl.CurlException (c, _, _) -> c
end)

39
test/basic_test.expected Normal file
View file

@ -0,0 +1,39 @@
get: OK
body=```
version = 0.26.2
profile=conventional
margin=80
if-then-else=k-r
parens-ite=true
parens-tuple=multi-line-only
sequence-style=terminator
type-decl=sparse
break-cases=toplevel
cases-exp-indent=2
field-space=tight-decl
leading-nested-match-parens=true
module-item-spacing=compact
quiet=true
ocaml-version=4.08.0
```
streaming get: OK
body=```
version = 0.26.2
profile=conventional
margin=80
if-then-else=k-r
parens-ite=true
parens-tuple=multi-line-only
sequence-style=terminator
type-decl=sparse
break-cases=toplevel
cases-exp-indent=2
field-space=tight-decl
leading-nested-match-parens=true
module-item-spacing=compact
quiet=true
ocaml-version=4.08.0
```
same buf? true

30
test/basic_test.ml Normal file
View file

@ -0,0 +1,30 @@
let body = ref ""
let url =
"https://raw.githubusercontent.com/c-cube/ezcurl/refs/heads/main/.ocamlformat"
let () =
match Ezcurl.get ~url () with
| Error (code, msg) ->
Format.eprintf "curl error: code `%s` (%s)@." (Curl.strerror code) msg
| Ok res ->
body := res.body;
Format.printf "get: OK@.body=```@.%s@.```@." !body
let () =
let buf = Buffer.create 32 in
match
Ezcurl.http_stream ~meth:GET ~url
~write_into:
(object
method on_input bs i len = Buffer.add_subbytes buf bs i len
method on_close () = ()
end)
()
with
| Error (code, msg) ->
Format.eprintf "curl error: code `%s` (%s)@." (Curl.strerror code) msg
| Ok _res ->
let new_body = Buffer.contents buf in
Format.printf "streaming get: OK@.body=```@.%s@.```@." new_body;
Format.printf "same buf? %b@." (new_body = !body)

3
test/dune Normal file
View file

@ -0,0 +1,3 @@
(test
(name basic_test)
(libraries ezcurl))