Current section

34 Versions

Jump to

Compare versions

6 files changed
+540 additions
-30 deletions
  @@ -1,18 +1,30 @@
1 - # gleam_http
1 + # Gleam HTTP
2 2
3 - A Gleam library that provides HTTP types to be used by HTTP servers and
4 - clients written in Gleam.
3 + Types and functions for HTTP clients and servers!
5 4
5 + ## HTTP Service Example
6 6
7 - ## Quick start
7 + ```rust
8 + import gleam/elli
9 + import gleam/http.{Request, Response}
10 + import gleam/bit_builder.{BitBuilder}
11 + import gleam/bit_string
8 12
9 - ```sh
10 - # Build the project
11 - rebar3 compile
13 + // Define a HTTP service
14 + //
15 + pub fn my_service(req: Request(BitString)) -> Response(BitBuilder) {
16 + let body = "Hello, world!"
17 + |> bit_string.from_string
18 + |> bit_builder.from_bit_string
12 19
13 - # Run the eunit tests
14 - rebar3 eunit
20 + http.response(200)
21 + |> http.prepend_resp_header("made-with", "Gleam")
22 + |> http.set_resp_body(body)
23 + }
15 24
16 - # Run the Erlang REPL
17 - rebar3 shell
25 + // Start it on port 3000 using the Elli web server
26 + //
27 + pub fn start() {
28 + elli.start(my_service, on_port: 3000)
29 + }
18 30 ```
  @@ -1,10 +1,10 @@
1 1 {<<"app">>,<<"gleam_http">>}.
2 2 {<<"build_tools">>,[<<"rebar3">>]}.
3 - {<<"description">>,<<"A Gleam library">>}.
3 + {<<"description">>,<<"Types and functions for HTTP clients and servers!">>}.
4 4 {<<"files">>,
5 5 [<<"LICENSE">>,<<"README.md">>,<<"rebar.config">>,<<"rebar.lock">>,
6 - <<"src/gleam/http.gleam">>,<<"src/gleam_http.app.src">>,
7 - <<"src/gleam_http_native.erl">>]}.
6 + <<"src/gleam/http.gleam">>,<<"src/gleam/http/middleware.gleam">>,
7 + <<"src/gleam_http.app.src">>,<<"src/gleam_http_native.erl">>]}.
8 8 {<<"licenses">>,[<<"Apache 2.0">>]}.
9 9 {<<"links">>,[]}.
10 10 {<<"name">>,<<"gleam_http">>}.
  @@ -13,4 +13,4 @@
13 13 [{<<"app">>,<<"gleam_stdlib">>},
14 14 {<<"optional">>,false},
15 15 {<<"requirement">>,<<"0.10.0">>}]}]}.
16 - {<<"version">>,<<"1.0.0">>}.
16 + {<<"version">>,<<"1.1.0">>}.
  @@ -2,16 +2,17 @@
2 2 {src_dirs, ["src", "gen/src"]}.
3 3
4 4 {profiles, [
5 - {test, [
6 - {pre_hooks, [{compile, "gleam build ."}]},
7 - {src_dirs, ["src", "test", "gen/src", "gen/test"]}
8 - ]},
9 -
10 - {dev, [
11 - {pre_hooks, [{compile, "gleam build ."}]}
12 - ]}
5 + {test, [{src_dirs, ["src", "test", "gen/src", "gen/test"]}]}
13 6 ]}.
14 7
8 + {shell, [
9 + % {config, "config/sys.config"},
10 + {apps, [gleam_stdlib]}
11 + ]}.
12 +
13 +
14 + {project_plugins, [rebar_gleam, rebar3_hex]}.
15 +
15 16 {deps, [
16 17 {gleam_stdlib, "0.10.0"}
17 18 ]}.
  @@ -1,8 +1,24 @@
1 - import gleam/string
1 + //// Functions for working with HTTP data structures in Gleam.
2 + ////
3 + //// This module makes it easy to create and modify Requests and Responses, data types.
4 + //// A general HTTP message type is defined that enables functions to work on both requests and responses.
5 + ////
6 + //// This module does not implement a HTTP client or HTTP server, but it can be used as a base for them.
2 7
3 - /// HTTP methods as defined by RFC 2616, and PATCH which is defined by RFC
4 - /// 5789.
5 - ///
8 + // TODO: validate_req
9 + // TODO: set_resp_header
10 + // https://github.com/elixir-plug/plug/blob/dfebbebeb716c43c7dee4915a061bede06ec45f1/lib/plug/conn.ex#L776
11 + // TODO: set_req_header
12 + // https://github.com/elixir-plug/plug/blob/dfebbebeb716c43c7dee4915a061bede06ec45f1/lib/plug/conn.ex#L776
13 + import gleam/list
14 + import gleam/option.{None, Option, Some}
15 + import gleam/string
16 + import gleam/string_builder
17 + import gleam/uri.{Uri}
18 + import gleam/dynamic.{Dynamic}
19 +
20 + /// HTTP standard method as defined by [RFC 2616](https://tools.ietf.org/html/rfc2616),
21 + /// and PATCH which is defined by [RFC 5789](https://tools.ietf.org/html/rfc5789).
6 22 pub type Method {
7 23 Get
8 24 Post
  @@ -50,5 +66,410 @@ pub fn method_to_string(method) {
50 66 }
51 67 }
52 68
53 - pub external fn method_from_erlang(anything) -> Result(Method, Nil) =
69 + /// The two URI schemes for HTTP
70 + ///
71 + pub type Scheme {
72 + Http
73 + Https
74 + }
75 +
76 + /// Convert a scheme into a string.
77 + ///
78 + /// # Examples
79 + ///
80 + /// > scheme_to_string(Http)
81 + /// "http"
82 + ///
83 + /// > scheme_to_string(Https)
84 + /// "https"
85 + ///
86 + pub fn scheme_to_string(scheme: Scheme) -> String {
87 + case scheme {
88 + Http -> "http"
89 + Https -> "https"
90 + }
91 + }
92 +
93 + /// Parse a HTTP scheme from a string
94 + ///
95 + /// # Examples
96 + ///
97 + /// > scheme_to_string("http")
98 + /// Ok(Http)
99 + ///
100 + /// > scheme_to_string("ftp")
101 + /// Error(Nil)
102 + ///
103 + pub fn scheme_from_string(scheme: String) -> Result(Scheme, Nil) {
104 + case string.lowercase(scheme) {
105 + "http" -> Ok(Http)
106 + "https" -> Ok(Https)
107 + _ -> Error(Nil)
108 + }
109 + }
110 +
111 + pub external fn method_from_dynamic(Dynamic) -> Result(Method, Nil) =
54 112 "gleam_http_native" "method_from_erlang"
113 +
114 + /// A HTTP header is a key-value pair. Header keys should be all lowercase
115 + /// characters.
116 + pub type Header =
117 + tuple(String, String)
118 +
119 + // TODO: document
120 + pub type Request(body) {
121 + Request(
122 + method: Method,
123 + headers: List(Header),
124 + body: body,
125 + scheme: Scheme,
126 + host: String,
127 + port: Option(Int),
128 + path: String,
129 + query: Option(String),
130 + )
131 + }
132 +
133 + // TODO: document
134 + pub type Response(body) {
135 + Response(status: Int, headers: List(Header), body: body)
136 + }
137 +
138 + // TODO: document
139 + pub type Service(in, out) =
140 + fn(Request(in)) -> Response(out)
141 +
142 + /// Return the uri that a request was sent to.
143 + ///
144 + pub fn req_to_uri(request: Request(a)) -> Uri {
145 + Uri(
146 + scheme: Some(scheme_to_string(request.scheme)),
147 + userinfo: None,
148 + host: Some(request.host),
149 + port: request.port,
150 + path: request.path,
151 + query: request.query,
152 + fragment: None,
153 + )
154 + }
155 +
156 + /// Construct a request from a URI.
157 + ///
158 + pub fn req_from_uri(uri: Uri) -> Result(Request(String), Nil) {
159 + try scheme = uri.scheme
160 + |> option.unwrap("")
161 + |> scheme_from_string
162 + try host = uri.host
163 + |> option.to_result(Nil)
164 + let req = Request(
165 + method: Get,
166 + headers: [],
167 + body: "",
168 + scheme: scheme,
169 + host: host,
170 + port: uri.port,
171 + path: uri.path,
172 + query: uri.query,
173 + )
174 + Ok(req)
175 + }
176 +
177 + /// Construct an empty Response.
178 + ///
179 + /// The body type of the returned response is `Nil`, and should be set with a
180 + /// call to `set_resp_body`.
181 + ///
182 + pub fn response(status: Int) -> Response(Nil) {
183 + Response(status: status, headers: [], body: Nil)
184 + }
185 +
186 + /// Return the non-empty segments of a request path.
187 + ///
188 + pub fn path_segments(request: Request(body)) -> List(String) {
189 + request.path
190 + |> uri.path_segments
191 + }
192 +
193 + /// Decode the query of a request.
194 + pub fn get_query(
195 + request: Request(body),
196 + ) -> Result(List(tuple(String, String)), Nil) {
197 + case request.query {
198 + Some(query_string) -> uri.parse_query(query_string)
199 + None -> Ok([])
200 + }
201 + }
202 +
203 + // TODO: test
204 + // TODO: escape
205 + // TODO: record update syntax
206 + /// Set the query of the request.
207 + ///
208 + pub fn set_query(
209 + req: Request(body),
210 + query: List(tuple(String, String)),
211 + ) -> Request(body) {
212 + let pair = fn(t: tuple(String, String)) {
213 + string_builder.from_strings([t.0, "=", t.1])
214 + }
215 + let query = query
216 + |> list.map(pair)
217 + |> list.intersperse(string_builder.from_string("&"))
218 + |> string_builder.concat
219 + |> string_builder.to_string
220 + |> Some
221 + let Request(
222 + method: method,
223 + headers: headers,
224 + body: body,
225 + scheme: scheme,
226 + host: host,
227 + port: port,
228 + path: path,
229 + query: _,
230 + ) = req
231 + Request(
232 + method: method,
233 + headers: headers,
234 + body: body,
235 + scheme: scheme,
236 + host: host,
237 + port: port,
238 + path: path,
239 + query: query,
240 + )
241 + }
242 +
243 + /// Get the value for a given header.
244 + ///
245 + /// If the request does not have that header then `Error(Nil)` is returned.
246 + ///
247 + pub fn get_req_header(
248 + request: Request(body),
249 + key: String,
250 + ) -> Result(String, Nil) {
251 + list.key_find(request.headers, string.lowercase(key))
252 + }
253 +
254 + /// Get the value for a given header.
255 + ///
256 + /// If the request does not have that header then `Error(Nil)` is returned.
257 + ///
258 + pub fn get_resp_header(
259 + response: Response(body),
260 + key: String,
261 + ) -> Result(String, Nil) {
262 + list.key_find(response.headers, string.lowercase(key))
263 + }
264 +
265 + // TODO: use record update syntax
266 + // TODO: document
267 + // TODO: test
268 + // https://github.com/elixir-plug/plug/blob/dfebbebeb716c43c7dee4915a061bede06ec45f1/lib/plug/conn.ex#L809
269 + pub fn prepend_req_header(
270 + request: Request(body),
271 + key: String,
272 + value: String,
273 + ) -> Request(body) {
274 + let Request(method, headers, body, scheme, host, port, path, query) = request
275 + let headers = [tuple(string.lowercase(key), value), ..headers]
276 + Request(method, headers, body, scheme, host, port, path, query)
277 + }
278 +
279 + // TODO: use record update syntax
280 + // TODO: document
281 + // https://github.com/elixir-plug/plug/blob/dfebbebeb716c43c7dee4915a061bede06ec45f1/lib/plug/conn.ex#L809
282 + pub fn prepend_resp_header(
283 + response: Response(body),
284 + key: String,
285 + value: String,
286 + ) -> Response(body) {
287 + let Response(status, headers, body) = response
288 + let headers = [tuple(string.lowercase(key), value), ..headers]
289 + Response(status, headers, body)
290 + }
291 +
292 + /// Set the body of the response, overwriting any existing body.
293 + ///
294 + pub fn set_resp_body(
295 + response: Response(old_body),
296 + body: new_body,
297 + ) -> Response(new_body) {
298 + let Response(status: status, headers: headers, ..) = response
299 + Response(status: status, headers: headers, body: body)
300 + }
301 +
302 + // TODO: test
303 + // TODO: record update syntax
304 + /// Set the body of the request, overwriting any existing body.
305 + ///
306 + pub fn set_req_body(
307 + req: Request(old_body),
308 + body: new_body,
309 + ) -> Request(new_body) {
310 + let Request(
311 + method: method,
312 + headers: headers,
313 + scheme: scheme,
314 + host: host,
315 + port: port,
316 + path: path,
317 + query: query,
318 + ..,
319 + ) = req
320 + Request(
321 + method: method,
322 + headers: headers,
323 + body: body,
324 + scheme: scheme,
325 + host: host,
326 + port: port,
327 + path: path,
328 + query: query,
329 + )
330 + }
331 +
332 + // TODO: test
333 + // TODO: record update syntax
334 + /// Set the method of the request.
335 + ///
336 + pub fn set_method(req: Request(body), method: Method) -> Request(body) {
337 + let Request(
338 + method: _,
339 + headers: headers,
340 + body: body,
341 + scheme: scheme,
342 + host: host,
343 + port: port,
344 + path: path,
345 + query: query,
346 + ) = req
347 + Request(
348 + method: method,
349 + headers: headers,
350 + body: body,
351 + scheme: scheme,
352 + host: host,
353 + port: port,
354 + path: path,
355 + query: query,
356 + )
357 + }
358 +
359 + // TODO: test
360 + /// Update the body of a response using a given function.
361 + ///
362 + pub fn map_resp_body(
363 + response: Response(old_body),
364 + transform: fn(old_body) -> new_body,
365 + ) -> Response(new_body) {
366 + response.body
367 + |> transform
368 + |> set_resp_body(response, _)
369 + }
370 +
371 + // TODO: test
372 + /// Update the body of a request using a given function.
373 + ///
374 + pub fn map_req_body(
375 + request: Request(old_body),
376 + transform: fn(old_body) -> new_body,
377 + ) -> Request(new_body) {
378 + request.body
379 + |> transform
380 + |> set_req_body(request, _)
381 + }
382 +
383 + // TODO: test
384 + /// Update the body of a response using a given result returning function.
385 + ///
386 + /// If the given function returns an `Ok` value the body is set, if it returns
387 + /// an `Error` value then the error is returned.
388 + ///
389 + pub fn try_map_resp_body(
390 + response: Response(old_body),
391 + transform: fn(old_body) -> Result(new_body, error),
392 + ) -> Result(Response(new_body), error) {
393 + try body = transform(response.body)
394 + Ok(set_resp_body(response, body))
395 + }
396 +
397 + /// Create a response that redirects to the given uri.
398 + ///
399 + pub fn redirect(uri: String) -> Response(String) {
400 + Response(
401 + status: 303,
402 + headers: [tuple("location", uri)],
403 + body: string.append("You are being redirected to ", uri),
404 + )
405 + }
406 +
407 + /// A request with commonly used default values. This request can be used as a
408 + /// an initial value and then update to create the desired request.
409 + ///
410 + pub fn default_req() -> Request(String) {
411 + Request(
412 + method: Get,
413 + headers: [],
414 + body: "",
415 + scheme: Https,
416 + host: "localhost",
417 + port: None,
418 + path: "",
419 + query: None,
420 + )
421 + }
422 +
423 + // TODO: test
424 + // TODO: record update syntax
425 + /// Set the method of the request.
426 + ///
427 + pub fn set_host(req: Request(body), host: String) -> Request(body) {
428 + let Request(
429 + method: method,
430 + headers: headers,
431 + body: body,
432 + scheme: scheme,
433 + host: _,
434 + port: port,
435 + path: path,
436 + query: query,
437 + ) = req
438 + Request(
439 + method: method,
440 + headers: headers,
441 + body: body,
442 + scheme: scheme,
443 + host: host,
444 + port: port,
445 + path: path,
446 + query: query,
447 + )
448 + }
449 +
450 + // TODO: test
451 + // TODO: record update syntax
452 + /// Set the path of the request.
453 + ///
454 + pub fn set_path(req: Request(body), path: String) -> Request(body) {
455 + let Request(
456 + method: method,
457 + headers: headers,
458 + body: body,
459 + scheme: scheme,
460 + host: host,
461 + port: port,
462 + path: _,
463 + query: query,
464 + ) = req
465 + Request(
466 + method: method,
467 + headers: headers,
468 + body: body,
469 + scheme: scheme,
470 + host: host,
471 + port: port,
472 + path: path,
473 + query: query,
474 + )
475 + }
  @@ -0,0 +1,76 @@
1 + import gleam/http.{Delete, Patch, Post, Put, Request, Response, Service}
2 + import gleam/list
3 + import gleam/result
4 +
5 + pub type Middleware(before_req, before_resp, after_req, after_resp) =
6 + fn(Service(before_req, before_resp)) -> Service(after_req, after_resp)
7 +
8 + /// A middleware that transform the response body returned by the service using
9 + /// a given function.
10 + ///
11 + pub fn map_resp_body(
12 + service: Service(req, a),
13 + with mapper: fn(a) -> b,
14 + ) -> Service(req, b) {
15 + fn(req) {
16 + req
17 + |> service
18 + |> http.map_resp_body(mapper)
19 + }
20 + }
21 +
22 + /// A middleware that prepends a header to the request.
23 + ///
24 + pub fn prepend_resp_header(
25 + service: Service(req, resp),
26 + key: String,
27 + value: String,
28 + ) -> Service(req, resp) {
29 + fn(req) {
30 + req
31 + |> service
32 + |> http.prepend_resp_header(key, value)
33 + }
34 + }
35 +
36 + fn ensure_post(req: Request(a)) {
37 + case req.method {
38 + Post -> Ok(req)
39 + _ -> Error(Nil)
40 + }
41 + }
42 +
43 + fn get_override_method(req) {
44 + try query_params = http.get_query(req)
45 + try method = list.key_find(query_params, "_method")
46 + try method = http.parse_method(method)
47 + case method {
48 + Put | Patch | Delete -> Ok(method)
49 + _ -> Error(Nil)
50 + }
51 + }
52 +
53 + /// A middleware that overrides an incoming POST request with a method given in
54 + /// the request's `_method` query paramerter. This is useful as web browsers
55 + /// typically only support GET and POST requests, but our application may
56 + /// expect other HTTP methods that are more semantically correct.
57 + ///
58 + /// The methods PUT, PATCH, and DELETE are accepted for overriding, all others
59 + /// are ignored.
60 + ///
61 + /// The `_method` query paramerter can be specified in a HTML form like so:
62 + ///
63 + /// <form method="POST" action="/item/1?_method=DELETE">
64 + /// <button type="submit">Delete item</button>
65 + /// </form>
66 + ///
67 + pub fn method_override(service: Service(req, resp)) -> Service(req, resp) {
68 + fn(req) {
69 + req
70 + |> ensure_post
71 + |> result.then(get_override_method)
72 + |> result.map(http.set_method(req, _))
73 + |> result.unwrap(req)
74 + |> service
75 + }
76 + }
Loading more files…