src/client.bend
emerging-ezhttp@0.8.0.0 · 108 lines · 0 definitions
1# ezhttp/src/client: fetch a URL. URL and HTTP parsing joined to the wire effect.
2# Answers a structured Response (status + headers + body), not a hub-style
3# "0\n…" string contract.
4import Base
5import emerging-ezhttp@0.8.0.0/src/url.bend as Url
6import emerging-ezhttp@0.8.0.0/src/http.bend as Http
7import emerging-ezhttp@0.8.0.0/src/body.bend as Body
8import emerging-ezhttp@0.8.0.0/src/wire.bend as Wire
9import emerging-ezhttp@0.8.0.0/src/auth.bend as Auth
10
11# structured client result: transport/parse error, or an HTTP response
12type Response is Data:
13 Err{why: String}
14 Response{status: U32, headers: List<&2, Http.Header>, body: String}
15
16# transport status line: first line is a code, rest is the payload
17def wire.code(raw: String) -> Maybe<&2, U32>:
18 U32.read(Http.first(String.split(raw, '\n')))
19
20# text after the first newline
21def wire.text.of(c: Http.Cut) -> String:
22 match c:
23 case Http.NoCut{}:
24 ""
25 case Http.Cut{_before, after}:
26 after
27
28def wire.text(raw: String) -> String:
29 wire.text.of(Http.divide(raw, "\n"))
30
31# whether the wire status is success (0)
32def wire.ok.of(m: Maybe<&2, U32>) -> Bool:
33 match m:
34 case None{}:
35 False{}
36 case Some{n}:
37 U32.is_eq(n, 0)
38
39def wire.ok(raw: String) -> Bool:
40 wire.ok.of(wire.code(raw))
41
42# a torn or successful HTTP parse as a client Response
43def from.reply(r: Http.Reply) -> Response:
44 match r:
45 case Http.Torn{why}:
46 Err{why}
47 case Http.Reply{status, headers, body}:
48 Response{status, headers, body}
49
50# a finished exchange
51def from.raw(raw: String, ok: Bool) -> Response:
52 match ok:
53 case False{}:
54 Err{wire.text(raw)}
55 case True{}:
56 from.reply(Http.parse(wire.text(raw)))
57
58# HTTP(S) exchange with a pre-built request
59def http.talk(secure: Bool, host: String, port: U32, req: String)
60 -> IO(Response):
61 do IO<Response>:
62 +raw : String <- Wire.wire.talk(secure, host, port, req)
63 IO.pure(Response, from.raw(raw, wire.ok(raw)))
64
65# a URL that came apart into an HTTP exchange
66def http.loc(method: String, l: Url.Loc, headers: List<&2, Http.Header>,
67 body: String) -> IO(Response):
68 match l:
69 case Url.Bad{why}:
70 IO.pure(Response, Err{why})
71 case Url.Loc{+scheme, +host, port, path}:
72 http.talk(String.eq(scheme, "https"), host, port,
73 Http.request(method, host, path, headers, body))
74
75# GET
76def client.get(url: String, headers: List<&2, Http.Header>) -> IO(Response):
77 http.loc("GET", Url.parse(url), headers, "")
78
79# HEAD (RFC 9110 §9.3.2)
80def client.head(url: String, headers: List<&2, Http.Header>) -> IO(Response):
81 http.loc("HEAD", Url.parse(url), headers, "")
82
83# POST
84def client.post(url: String, headers: List<&2, Http.Header>, body: Body.Body)
85 -> IO(Response):
86 http.loc("POST", Url.parse(url), headers, Body.body.payload(body))
87
88# PUT
89def client.put(url: String, headers: List<&2, Http.Header>, body: Body.Body)
90 -> IO(Response):
91 http.loc("PUT", Url.parse(url), headers, Body.body.payload(body))
92
93# DELETE
94def client.delete(url: String, headers: List<&2, Http.Header>, body: Body.Body)
95 -> IO(Response):
96 http.loc("DELETE", Url.parse(url), headers, Body.body.payload(body))
97
98# OPTIONS
99def client.options(url: String, headers: List<&2, Http.Header>) -> IO(Response):
100 http.loc("OPTIONS", Url.parse(url), headers, "")
101
102# Authorization: Bearer … (auth.bend)
103def client.bearer(token: String) -> Http.Header:
104 Auth.auth.bearer(token)
105
106# Authorization: Basic … (auth.bend; RFC 7617, RFC 4648 Base64)
107def client.basic(user: String, password: String) -> Http.Header:
108 Auth.auth.basic(user, password)