This version of the module is deprecated: Renamed to gaato/requesto
A human-facing async HTTP client for MoonBit: verb functions, a client with defaults, body encoders, responses as values, and a step chain around a swappable transport.
Dependencies
moon add gaato/req///|
struct User {
id : Int
name : String
} derive(FromJson)
///|
async fn example() -> Unit raise {
let client = @req.Client::new(
base_url="https://api.example.com/v1",
headers=[("Accept", "application/json")],
auth=Bearer("token"),
)
defer client.close()
let user : User = client
.get("users/42", params=[("expand", "teams")])
.raise_for_status()
.json_as()
client.post("users", json={ "name": user.name }) |> ignore
println(@req.get("https://example.com/").text())
}| requests / httpx | axios / ky | Req | gaato/req | |
|---|---|---|---|---|
| Call | httpx.get(url, params=...) | ky.get(url, {searchParams}) | Req.get!(url, params: ...) | @req.get(url, params=[...]) |
| Client | httpx.Client(base_url=...) | axios.create({baseURL}) | Req.new(base_url: ...) | @req.Client::new(base_url=...) |
| JSON body | json=... | json: ... (ky) | json: ... | json=... |
| Non-2xx | value, raise_for_status() | throws | value | value, raise_for_status() |
| Read JSON | .json() | .json() / data | .body | .json(), .json_as() |
| Redirects | follow_redirects=, .history | maxRedirects / redirect | redirect:, max_redirects: | follow_redirects=, max_redirects=, .history |
| Cookies | client.cookies | the browser's | — | client.cookies(), cookies=Jar(jar) |
| Streaming | client.stream(...), iter_lines() | responseType: 'stream' | into: fun | client.stream(...), each_line |
| SSE | httpx_sse.connect_sse | EventSource | — | client.sse(...), each_event |
| Timeouts | httpx.Timeout(connect=...) | timeout | receive_timeout: | timeout=, connect_timeout=, ... |
| Proxy | proxy= | proxy (axios) | connect_options: | proxy=, trust_env |
| Hooks | event_hooks | interceptors / hooks | steps | steps=[on_request(...), ...] |
| Retries | HTTPAdapter(max_retries=Retry(...)) (off) | retry (ky, on) | retry: :safe_transient (on) | retry=Retry::new(...) (on) |
| Digest | HTTPDigestAuth / DigestAuth | — | — | auth=Digest(username=u, password=p) |
| Mock | httpx.MockTransport | — | Req.Test | @req.MockTransport |
test {
let page = @req.Url::parse("https://api.example.com/v1/items?page=1")
let next = page.resolve(@req.Url::parse("items?page=2"))
assert_eq(next.to_string(), "https://api.example.com/v1/items?page=2")
assert_eq(next.query_params(), [("page", "2")])
assert_true(page.same_origin(next))
}pub(all) suberror ReqError {
Timeout(request~ : Request, phase~ : Phase, timeout~ : Int)
Network(request~ : Request, phase~ : Phase, message~ : String)
Protocol(request~ : Request, message~ : String)
Proxy(request~ : Request, status~ : Int, message~ : String)
TooManyRedirects(request~ : Request, max~ : Int)
InvalidRedirect(request~ : Request, location~ : String, message~ : String)
ResponseNotRead(request~ : Request)
StreamConsumed(request~ : Request)
UnsupportedProtocol(url~ : String)
InvalidURL(url~ : String, message~ : String)
InvalidRequest(message~ : String)
Decoding(message~ : String)
Status(request~ : Request, response~ : Response)
}pub(all) enum Auth {
Basic(username~ : String, password~ : String)
Bearer(String)
Digest(username~ : String, password~ : String)
}pub struct Client {
// private fields
}test {
let client = @req.Client::new()
assert_eq(client.cookies().map(jar => jar.length()), Some(0))
client.close()
}fn Client::new(base_url? : String, headers? : Array[(String, String)], params? : Array[(String, String)], auth? : Auth, timeout? : Int, max_body_bytes? : Int?, steps? : Array[async (Request, async (Request) -> Response raise ReqError) -> Response raise ReqError], follow_redirects? : Bool, max_redirects? : Int, cookies? : Cookies, retry? : Retry, transport? : &Transport, clock? : &Clock, random? : () -> Double, connect_timeout? : Int, write_timeout? : Int, read_timeout? : Int, pool_timeout? : Int, max_connections? : Int, max_connections_per_origin? : Int, max_idle_per_origin? : Int, proxy? : String, trust_env? : Bool) -> Clienttest {
let client = @req.Client::new(
base_url="https://api.example.com/v1",
headers=[("Accept", "application/json")],
auth=Bearer("token"),
)
client.close()
}async fn Client::patch(self : Client, url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, follow_redirects? : Bool, retry? : Retry) -> Response raise ReqErrorasync fn Client::post(self : Client, url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, follow_redirects? : Bool, retry? : Retry) -> Response raise ReqErrorasync fn Client::put(self : Client, url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, follow_redirects? : Bool, retry? : Retry) -> Response raise ReqErrorasync fn Client::request(self : Client, http_method : String, url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, follow_redirects? : Bool, retry? : Retry) -> Response raise ReqErrorasync fn Client::sse(self : Client, http_method : String, url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, max_event_bytes? : Int, f : async (SseEvent) -> Unit) -> Unitasync test {
let client = @req.Client::new(
transport=@req.MockTransport::new(_ => {
status: 200,
headers: @http.Headers::from_array([("Content-Type", "text/event-stream")]),
body: b"id: 7\ndata: hello\n\n",
}),
)
let seen = []
client.sse("GET", "https://example.com/events", event => seen.push(event))
assert_eq(seen.map(event => (event.data, event.id)), [("hello", Some("7"))])
}async fn[T] Client::stream(self : Client, http_method : String, url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, follow_redirects? : Bool, retry? : Retry, f : async (Response) -> T) -> Tasync test {
let client = @req.Client::new(
transport=@req.MockTransport::new(_ => {
status: 200,
headers: @http.Headers::new(),
body: b"line one",
}),
)
let size = client.stream("GET", "https://example.com/export", response => {
let mut size = 0
response.each_chunk(chunk => size += chunk.length())
size
})
assert_eq(size, 8)
}test {
let clock = @simclock.VirtualClock()
let jar = @req.CookieJar::new(clock)
let first = @req.Client::new(clock~, cookies=Jar(jar))
let second = @req.Client::new(clock~, cookies=Jar(jar))
let shared = second.cookies().unwrap()
assert_true(physical_equal(shared, jar))
let none = @req.Client::new(cookies=Off)
assert_true(none.cookies() is None)
[first, second, none].each(client => client.close())
}pub struct MockTransport {
// private fields
}async test {
let client = @req.Client::new(
transport=@req.MockTransport::new(request => {
status: 200,
headers: @http.Headers::new(),
body: b"hi \{request.url}",
}),
)
let response = client.get("https://example.com/")
assert_eq(response.text(), "hi https://example.com/")
}impl Transport for MockTransportasync fn send_stream(self : MockTransport, request : Request) -> (ResponseHead, &BodyStream) raise HttpErrorasync fn MockTransport::send_stream(self : MockTransport, request : Request) -> (ResponseHead, &BodyStream) raise HttpErrorasync test {
let client = @req.Client::new(
transport=@req.MockTransport::new(_ => {
status: 200,
headers: @http.Headers::from_array([("Content-Type", "text/event-stream")]),
body: b"data: one\n\nevent: done\ndata: two\n\n",
}),
)
let seen = []
client.stream("GET", "https://example.com/events", response => {
response.each_event(event => seen.push("\{event.event} \{event.data}"))
})
assert_eq(seen, ["message one", "done two"])
}async test {
let client = @req.Client::new(
transport=@req.MockTransport::new(_ => {
status: 200,
headers: @http.Headers::new(),
body: b"{\"n\":1}\n{\"n\":2}\n",
}),
)
let lines = []
client.stream("GET", "https://example.com/feed.ndjson", response => {
response.each_line(line => lines.push(line))
})
assert_eq(lines, ["{\"n\":1}", "{\"n\":2}"])
}test {
let request = @http.Request::get("https://example.com/")
let response = @req.Response::new(200, request, content=b"cached")
assert_eq(response.text(), "cached")
assert_eq(response.url, "https://example.com/")
}pub struct Retry {
// private fields
}test {
let client = @req.Client::new(retry=@req.Retry::new(max_retries=5))
client.close()
let client = @req.Client::new(retry=@req.Retry::none())
client.close()
}async fn patch(url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, follow_redirects? : Bool, retry? : Retry) -> Response raise ReqErrorasync fn post(url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, follow_redirects? : Bool, retry? : Retry) -> Response raise ReqErrorasync fn put(url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, follow_redirects? : Bool, retry? : Retry) -> Response raise ReqErrorasync fn request(http_method : String, url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, follow_redirects? : Bool, retry? : Retry) -> Response raise ReqErrorasync test {
let response = try @req.request("GET", "ftp://example.com/") catch {
UnsupportedProtocol(..) => None
error => raise error
} noraise {
response => Some(response)
}
assert_true(response is None)
}async fn sse(http_method : String, url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, max_event_bytes? : Int, f : async (SseEvent) -> Unit) -> Unitasync fn[T] stream(http_method : String, url : String, params? : Array[(String, String)], headers? : Array[(String, String)], content? : Bytes, text? : String, data? : Array[(String, String)], files? : Array[Part], json? : Json, body_stream? : &BodyStream, body_length? : Int64, timeout? : Int, follow_redirects? : Bool, retry? : Retry, f : async (Response) -> T) -> TInstall
Download zipA human-facing async HTTP client for MoonBit: verb functions, a client with defaults, body encoders, responses as values, and a step chain around a swappable transport.
Dependencies