src/client.bend

emerging-ezhttp@0.9.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.9.0.0/src/url.bend as Url
6import emerging-ezhttp@0.9.0.0/src/http.bend as Http
7import emerging-ezhttp@0.9.0.0/src/body.bend as Body
8import emerging-ezhttp@0.9.0.0/src/wire.bend as Wire
9import emerging-ezhttp@0.9.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)