main.bend

emerging-ezhttp@0.11.0.0 · 214 lines · 0 definitions

1# ezhttp: HTTP/1.1 client and server for Bend 2, with auth, cookie and CORS helpers.
2#
3# Shared message types, BYO JSON via encode/decode. TLS for the client stays
4# at the wire/runtime layer.
5import Base
6import emerging-ezhttp@0.11.0.0/src/url.bend as Url
7import emerging-ezhttp@0.11.0.0/src/pct.bend as Pct
8import emerging-ezhttp@0.11.0.0/src/http.bend as Http
9import emerging-ezhttp@0.11.0.0/src/body.bend as Body
10import emerging-ezhttp@0.11.0.0/src/client.bend as Client
11import emerging-ezhttp@0.11.0.0/src/server.bend as Server
12import emerging-ezhttp@0.11.0.0/src/cookie.bend as Cookie
13import emerging-ezhttp@0.11.0.0/src/cache.bend as Cache
14import emerging-ezhttp@0.11.0.0/src/cors.bend as Cors
15import emerging-ezhttp@0.11.0.0/src/html.bend as Html
16
17# URL parse re-export
18def parse_url(url: String) -> Url.Loc:
19  Url.parse(url)
20
21# percent-encode a path (RFC 3986 §2.1), as Python's urllib.parse.quote(text):
22# unreserved characters and "/" stay, every other character becomes %XX per
23# UTF-8 byte, upper-case hex (SPEC.md HTTP-URL)
24def quote_path(text: String) -> String:
25  Pct.quote(True{}, text)
26
27# percent-encode a path segment, query key or value (RFC 3986 §2.1), as
28# urllib.parse.quote(text, safe=""): like quote_path, but "/" becomes %2F
29def quote_component(text: String) -> String:
30  Pct.quote(False{}, text)
31
32# response parse re-export
33def parse_response(raw: String) -> Http.Reply:
34  Http.parse(raw)
35
36# request parse re-export (server inbound)
37def parse_request(raw: String) -> Http.Request:
38  Http.ask(raw)
39
40# format a response message (server outbound)
41def format_response(status: U32, headers: List<&2, Http.Header>, body: String)
42  -> String:
43  Http.respond(status, headers, body)
44
45# empty request body
46def empty_body() -> Body.Body:
47  Body.body.empty()
48
49# UTF-8 text request body
50def text_body(text: String) -> Body.Body:
51  Body.body.text(text)
52
53# octet-list request body
54def octets_body(bs: List<&2, U32>) -> Body.Body:
55  Body.body.octets(bs)
56
57# BYO JSON (or any codec): encode a value with the caller's to_text
58def encode(~A: Data, ~to_text: A -> String, value: A) -> Body.Body:
59  Body.body.encode(A, to_text, value)
60
61# BYO JSON (or any codec): decode body text with the caller's from_text
62def decode(
63  ~A: Data,
64  ~from_text: String -> Maybe<&2, A>,
65  text: String
66) -> Maybe<&2, A>:
67  Body.body.decode(A, from_text, text)
68
69# Bearer token header helper
70def bearer(token: String) -> Http.Header:
71  Client.client.bearer(token)
72
73# Basic credentials header (RFC 7617)
74def basic(user: String, password: String) -> Http.Header:
75  Client.client.basic(user, password)
76
77# Set-Cookie header from a parsed cookie (RFC 6265)
78def set_cookie(cookie: Cookie.Cookie) -> Http.Header:
79  Cookie.cookie.set(cookie)
80
81# parse one Set-Cookie field value
82def parse_cookie(line: String) -> Maybe<&2, Cookie.Cookie>:
83  Cookie.cookie.parse(line)
84
85# Cookie request header for a list of cookies
86def cookie_header(cs: List<&2, Cookie.Cookie>) -> Http.Header:
87  Cookie.cookie.request(cs)
88
89# parse a Cache-Control field value (RFC 9111)
90def cache_control(value: String) -> Cache.Cc:
91  Cache.cache.parse(value)
92
93# freshness of a Cache-Control value at an age in seconds
94def fresh(value: String, age: Nat) -> Bool:
95  Cache.cache.fresh(Cache.cache.parse(value), age)
96
97# CORS preflight or simple headers on a handler reply
98def cors_reply(cfg: Cors.Cfg, req: Http.Request, reply: Http.Reply) -> Http.Reply:
99  Cors.cors.on(cfg, req, reply)
100
101# escape text for HTML as markupsafe and Jinja's autoescape do: & < > " ' as
102# &amp; &lt; &gt; &#34; &#39;, every other character as itself
103def html_escape(text: String) -> String:
104  Html.escape(text)
105
106# GET with no extra headers
107def http.get(url: String) -> IO(Client.Response):
108  Client.client.get(url, [])
109
110# GET with headers
111def http.get_with(url: String, headers: List<&2, Http.Header>)
112  -> IO(Client.Response):
113  Client.client.get(url, headers)
114
115# GET with headers and a timeout in milliseconds that bounds connecting,
116# sending and receiving; with `follow`, redirects are followed (GET only, at
117# most 10 hops, see src/redirect.bend)
118def http.get_opts(
119  url: String,
120  headers: List<&2, Http.Header>,
121  timeout_ms: U32,
122  follow: Bool
123) -> IO(Client.Response):
124  Client.client.get.opts(url, headers, timeout_ms, follow)
125
126# whether a response is an exchange that ran out of time
127def http.timed_out(r: Client.Response) -> Bool:
128  Client.client.timed_out(r)
129
130# HEAD with no extra headers
131def http.head(url: String) -> IO(Client.Response):
132  Client.client.head(url, [])
133
134# HEAD with headers
135def http.head_with(url: String, headers: List<&2, Http.Header>)
136  -> IO(Client.Response):
137  Client.client.head(url, headers)
138
139# POST with a body and no extra headers
140def http.post(url: String, body: Body.Body) -> IO(Client.Response):
141  Client.client.post(url, [], body)
142
143# POST with headers and a body
144def http.post_with(
145  url: String,
146  headers: List<&2, Http.Header>,
147  body: Body.Body
148) -> IO(Client.Response):
149  Client.client.post(url, headers, body)
150
151# PUT with a body and no extra headers
152def http.put(url: String, body: Body.Body) -> IO(Client.Response):
153  Client.client.put(url, [], body)
154
155# PUT with headers and a body
156def http.put_with(url: String, headers: List<&2, Http.Header>, body: Body.Body)
157  -> IO(Client.Response):
158  Client.client.put(url, headers, body)
159
160# DELETE with a body and no extra headers
161def http.delete(url: String, body: Body.Body) -> IO(Client.Response):
162  Client.client.delete(url, [], body)
163
164# DELETE with headers and a body
165def http.delete_with(
166  url: String,
167  headers: List<&2, Http.Header>,
168  body: Body.Body
169) -> IO(Client.Response):
170  Client.client.delete(url, headers, body)
171
172# OPTIONS with no extra headers
173def http.options(url: String) -> IO(Client.Response):
174  Client.client.options(url, [])
175
176# OPTIONS with headers
177def http.options_with(url: String, headers: List<&2, Http.Header>)
178  -> IO(Client.Response):
179  Client.client.options(url, headers)
180
181# listen on host:port and serve up to `limit` connections
182def http.serve(
183  ~handle: Http.Request -> Http.Reply,
184  host: String,
185  port: U32,
186  limit: U32
187) -> IO(Unit):
188  Server.server.serve(handle, host, port, limit)
189
190# listen on host:port and serve up to `limit` connections with a handler that
191# does IO; a failed connection is logged and the next one is served
192def http.serve_io(
193  ~handle: Http.Request -> IO(Http.Reply),
194  host: String,
195  port: U32,
196  limit: U32
197) -> IO(Unit):
198  Server.server.serve_io(handle, host, port, limit)
199
200# listen on host:port and serve with an IO handler until the process stops
201@unsafe def http.serve_forever(
202  ~handle: Http.Request -> IO(Http.Reply),
203  host: String,
204  port: U32
205) -> IO(Unit):
206  Server.server.serve_forever(handle, host, port)
207
208# listen on host:port, serve one connection, then stop
209def http.serve_once(
210  ~handle: Http.Request -> Http.Reply,
211  host: String,
212  port: U32
213) -> IO(Unit):
214  Server.server.once(handle, host, port)