aboutsummaryrefslogtreecommitdiff
path: root/aoc2023/build/packages/gleam_http/src
diff options
context:
space:
mode:
authorJ.J <thechairman@thechairman.info>2024-05-30 21:50:02 -0400
committerJ.J <thechairman@thechairman.info>2024-05-30 21:50:02 -0400
commit612fd986ab1e00b6d34dc1937136250e08e89325 (patch)
treea3c93952040c6afdf348b5831619a45db7ba0a2e /aoc2023/build/packages/gleam_http/src
parent231c2b688d1e6cf0846d46e883da30e042a9c6cf (diff)
downloadgleam_aoc-612fd986ab1e00b6d34dc1937136250e08e89325.tar.gz
gleam_aoc-612fd986ab1e00b6d34dc1937136250e08e89325.zip
cleanup
Diffstat (limited to 'aoc2023/build/packages/gleam_http/src')
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam/http.gleam560
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam/http/cookie.gleam128
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam/http/request.gleam267
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam/http/response.gleam141
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam/http/service.gleam82
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam@http.erl626
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam@http@cookie.erl153
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam@http@request.erl202
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam@http@response.erl97
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam@http@service.erl82
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam_http.app.src12
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam_http_native.erl88
-rw-r--r--aoc2023/build/packages/gleam_http/src/gleam_http_native.mjs38
13 files changed, 2476 insertions, 0 deletions
diff --git a/aoc2023/build/packages/gleam_http/src/gleam/http.gleam b/aoc2023/build/packages/gleam_http/src/gleam/http.gleam
new file mode 100644
index 0000000..a892006
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam/http.gleam
@@ -0,0 +1,560 @@
+//// Functions for working with HTTP data structures in Gleam.
+////
+//// This module makes it easy to create and modify Requests and Responses, data types.
+//// A general HTTP message type is defined that enables functions to work on both requests and responses.
+////
+//// This module does not implement a HTTP client or HTTP server, but it can be used as a base for them.
+
+import gleam/dynamic.{type DecodeError, type Dynamic, DecodeError}
+import gleam/string
+import gleam/bit_array
+import gleam/result
+import gleam/list
+import gleam/bool
+
+/// HTTP standard method as defined by [RFC 2616](https://tools.ietf.org/html/rfc2616),
+/// and PATCH which is defined by [RFC 5789](https://tools.ietf.org/html/rfc5789).
+pub type Method {
+ Get
+ Post
+ Head
+ Put
+ Delete
+ Trace
+ Connect
+ Options
+ Patch
+
+ /// Non-standard but valid HTTP methods.
+ Other(String)
+}
+
+// TODO: check if the a is a valid HTTP method (i.e. it is a token, as per the
+// spec) and return Ok(Other(s)) if so.
+pub fn parse_method(s) -> Result(Method, Nil) {
+ case string.lowercase(s) {
+ "connect" -> Ok(Connect)
+ "delete" -> Ok(Delete)
+ "get" -> Ok(Get)
+ "head" -> Ok(Head)
+ "options" -> Ok(Options)
+ "patch" -> Ok(Patch)
+ "post" -> Ok(Post)
+ "put" -> Ok(Put)
+ "trace" -> Ok(Trace)
+ _ -> Error(Nil)
+ }
+}
+
+pub fn method_to_string(method: Method) -> String {
+ case method {
+ Connect -> "connect"
+ Delete -> "delete"
+ Get -> "get"
+ Head -> "head"
+ Options -> "options"
+ Patch -> "patch"
+ Post -> "post"
+ Put -> "put"
+ Trace -> "trace"
+ Other(s) -> s
+ }
+}
+
+/// The two URI schemes for HTTP
+///
+pub type Scheme {
+ Http
+ Https
+}
+
+/// Convert a scheme into a string.
+///
+/// # Examples
+///
+/// > scheme_to_string(Http)
+/// "http"
+///
+/// > scheme_to_string(Https)
+/// "https"
+///
+pub fn scheme_to_string(scheme: Scheme) -> String {
+ case scheme {
+ Http -> "http"
+ Https -> "https"
+ }
+}
+
+/// Parse a HTTP scheme from a string
+///
+/// # Examples
+///
+/// > scheme_from_string("http")
+/// Ok(Http)
+///
+/// > scheme_from_string("ftp")
+/// Error(Nil)
+///
+pub fn scheme_from_string(scheme: String) -> Result(Scheme, Nil) {
+ case string.lowercase(scheme) {
+ "http" -> Ok(Http)
+ "https" -> Ok(Https)
+ _ -> Error(Nil)
+ }
+}
+
+pub fn method_from_dynamic(value: Dynamic) -> Result(Method, List(DecodeError)) {
+ case do_method_from_dynamic(value) {
+ Ok(method) -> Ok(method)
+ Error(_) -> Error([DecodeError("HTTP method", dynamic.classify(value), [])])
+ }
+}
+
+pub type MultipartHeaders {
+ /// The headers for the part have been fully parsed.
+ MultipartHeaders(
+ headers: List(Header),
+ /// The remaining content that has not yet been parsed. This will contain
+ /// the body for this part, if any, and can be parsed with the
+ /// `parse_multipart_body` function.
+ remaining: BitArray,
+ )
+ /// More input is required to parse the headers for this part.
+ MoreRequiredForHeaders(
+ /// Call this function to continue parsing the headers for this part.
+ continuation: fn(BitArray) -> Result(MultipartHeaders, Nil),
+ )
+}
+
+pub type MultipartBody {
+ /// The body for the part has been fully parsed.
+ MultipartBody(
+ // The rest of the body for this part. The full body of the part is this
+ // concatenated onto the end of each chunk returned by any previous
+ // `MoreRequiredForBody` returns.
+ chunk: BitArray,
+ /// This is `True` if this was the last part in the multipart message,
+ /// otherwise there are more parts to parse.
+ done: Bool,
+ /// The remaining content that has not yet been parsed. This will contain
+ /// the next part if `done` is `False`, otherwise it will contain the
+ /// epilogue, if any.
+ remaining: BitArray,
+ )
+ MoreRequiredForBody(
+ // The body that has been parsed so far. The full body of the part is this
+ // concatenated with the chunk returned by each `MoreRequiredForBody` return
+ // value, and the final `MultipartBody` return value.
+ chunk: BitArray,
+ /// Call this function to continue parsing the body for this part.
+ continuation: fn(BitArray) -> Result(MultipartBody, Nil),
+ )
+}
+
+/// Parse the headers for part of a multipart message, as defined in RFC 2045.
+///
+/// This function skips any preamble before the boundary. The preamble may be
+/// retrieved using `parse_multipart_body`.
+///
+/// This function will accept input of any size, it is up to the caller to limit
+/// it if needed.
+///
+/// To enable streaming parsing of multipart messages, this function will return
+/// a continuation if there is not enough data to fully parse the headers.
+/// Further information is available in the documentation for `MultipartBody`.
+///
+pub fn parse_multipart_headers(
+ data: BitArray,
+ boundary: String,
+) -> Result(MultipartHeaders, Nil) {
+ let boundary = bit_array.from_string(boundary)
+ // TODO: rewrite this to use a bit pattern once JavaScript supports
+ // the `b:binary-size(bsize)` pattern.
+ let prefix = <<45, 45, boundary:bits>>
+ case bit_array.slice(data, 0, bit_array.byte_size(prefix)) == Ok(prefix) {
+ // There is no preamble, parse the headers.
+ True -> parse_headers_after_prelude(data, boundary)
+ // There is a preamble, skip it before parsing.
+ False -> skip_preamble(data, boundary)
+ }
+}
+
+/// Parse the body for part of a multipart message, as defined in RFC 2045. The
+/// body is everything until the next boundary. This function is generally to be
+/// called after calling `parse_multipart_headers` for a given part.
+///
+/// This function will accept input of any size, it is up to the caller to limit
+/// it if needed.
+///
+/// To enable streaming parsing of multipart messages, this function will return
+/// a continuation if there is not enough data to fully parse the body, along
+/// with the data that has been parsed so far. Further information is available
+/// in the documentation for `MultipartBody`.
+///
+pub fn parse_multipart_body(
+ data: BitArray,
+ boundary: String,
+) -> Result(MultipartBody, Nil) {
+ boundary
+ |> bit_array.from_string
+ |> parse_body_with_bit_array(data, _)
+}
+
+fn parse_body_with_bit_array(
+ data: BitArray,
+ boundary: BitArray,
+) -> Result(MultipartBody, Nil) {
+ let bsize = bit_array.byte_size(boundary)
+ let prefix = bit_array.slice(data, 0, 2 + bsize)
+ case prefix == Ok(<<45, 45, boundary:bits>>) {
+ True -> Ok(MultipartBody(<<>>, done: False, remaining: data))
+ False -> parse_body_loop(data, boundary, <<>>)
+ }
+}
+
+fn parse_body_loop(
+ data: BitArray,
+ boundary: BitArray,
+ body: BitArray,
+) -> Result(MultipartBody, Nil) {
+ let dsize = bit_array.byte_size(data)
+ let bsize = bit_array.byte_size(boundary)
+ let required = 6 + bsize
+ case data {
+ _ if dsize < required -> {
+ more_please_body(parse_body_loop(_, boundary, <<>>), body, data)
+ }
+
+ // TODO: flatten this into a single case expression once JavaScript supports
+ // the `b:binary-size(bsize)` pattern.
+ //
+ // \r\n
+ <<13, 10, data:bytes>> -> {
+ let desired = <<45, 45, boundary:bits>>
+ let size = bit_array.byte_size(desired)
+ let dsize = bit_array.byte_size(data)
+ let prefix = bit_array.slice(data, 0, size)
+ let rest = bit_array.slice(data, size, dsize - size)
+ case prefix == Ok(desired), rest {
+ // --boundary\r\n
+ True, Ok(<<13, 10, _:bytes>>) ->
+ Ok(MultipartBody(body, done: False, remaining: data))
+
+ // --boundary--
+ True, Ok(<<45, 45, data:bytes>>) ->
+ Ok(MultipartBody(body, done: True, remaining: data))
+
+ False, _ -> parse_body_loop(data, boundary, <<body:bits, 13, 10>>)
+ _, _ -> Error(Nil)
+ }
+ }
+
+ <<char, data:bytes>> -> {
+ parse_body_loop(data, boundary, <<body:bits, char>>)
+ }
+ }
+}
+
+fn parse_headers_after_prelude(
+ data: BitArray,
+ boundary: BitArray,
+) -> Result(MultipartHeaders, Nil) {
+ let dsize = bit_array.byte_size(data)
+ let bsize = bit_array.byte_size(boundary)
+ let required_size = bsize + 4
+
+ // TODO: this could be written as a single case expression if JavaScript had
+ // support for the `b:binary-size(bsize)` pattern. Rewrite this once the
+ // compiler support this.
+
+ use <- bool.guard(
+ when: dsize < required_size,
+ return: more_please_headers(parse_headers_after_prelude(_, boundary), data),
+ )
+
+ use prefix <- result.try(bit_array.slice(data, 0, required_size - 2))
+ use second <- result.try(bit_array.slice(data, 2 + bsize, 2))
+ let desired = <<45, 45, boundary:bits>>
+
+ use <- bool.guard(prefix != desired, return: Error(Nil))
+
+ case second == <<45, 45>> {
+ // --boundary--
+ // The last boundary. Return the epilogue.
+ True -> {
+ let rest_size = dsize - required_size
+ use data <- result.map(bit_array.slice(data, required_size, rest_size))
+ MultipartHeaders([], remaining: data)
+ }
+
+ // --boundary
+ False -> {
+ let start = required_size - 2
+ let rest_size = dsize - required_size + 2
+ use data <- result.try(bit_array.slice(data, start, rest_size))
+ do_parse_headers(data)
+ }
+ }
+}
+
+fn skip_preamble(
+ data: BitArray,
+ boundary: BitArray,
+) -> Result(MultipartHeaders, Nil) {
+ let data_size = bit_array.byte_size(data)
+ let boundary_size = bit_array.byte_size(boundary)
+ let required = boundary_size + 4
+ case data {
+ _ if data_size < required ->
+ more_please_headers(skip_preamble(_, boundary), data)
+
+ // TODO: change this to use one non-nested case expression once the compiler
+ // supports the `b:binary-size(bsize)` pattern on JS.
+ // \r\n--
+ <<13, 10, 45, 45, data:bytes>> -> {
+ case bit_array.slice(data, 0, boundary_size) {
+ // --boundary
+ Ok(prefix) if prefix == boundary -> {
+ let start = boundary_size
+ let length = bit_array.byte_size(data) - boundary_size
+ use rest <- result.try(bit_array.slice(data, start, length))
+ do_parse_headers(rest)
+ }
+ Ok(_) -> skip_preamble(data, boundary)
+ Error(_) -> Error(Nil)
+ }
+ }
+
+ <<_, data:bytes>> -> skip_preamble(data, boundary)
+ }
+}
+
+fn skip_whitespace(data: BitArray) -> BitArray {
+ case data {
+ // Space or tab.
+ <<32, data:bytes>> | <<9, data:bytes>> -> skip_whitespace(data)
+ _ -> data
+ }
+}
+
+fn do_parse_headers(data: BitArray) -> Result(MultipartHeaders, Nil) {
+ case data {
+ // \r\n\r\n
+ // We've reached the end, there are no headers.
+ <<13, 10, 13, 10, data:bytes>> -> Ok(MultipartHeaders([], remaining: data))
+
+ // \r\n
+ // Skip the line break after the boundary.
+ <<13, 10, data:bytes>> -> parse_header_name(data, [], <<>>)
+
+ <<13>> | <<>> -> more_please_headers(do_parse_headers, data)
+
+ _ -> Error(Nil)
+ }
+}
+
+fn parse_header_name(
+ data: BitArray,
+ headers: List(Header),
+ name: BitArray,
+) -> Result(MultipartHeaders, Nil) {
+ case skip_whitespace(data) {
+ // :
+ <<58, data:bytes>> ->
+ data
+ |> skip_whitespace
+ |> parse_header_value(headers, name, <<>>)
+
+ <<char, data:bytes>> ->
+ parse_header_name(data, headers, <<name:bits, char>>)
+
+ <<>> -> more_please_headers(parse_header_name(_, headers, name), data)
+ }
+}
+
+fn parse_header_value(
+ data: BitArray,
+ headers: List(Header),
+ name: BitArray,
+ value: BitArray,
+) -> Result(MultipartHeaders, Nil) {
+ let size = bit_array.byte_size(data)
+ case data {
+ // We need at least 4 bytes to check for the end of the headers.
+ _ if size < 4 ->
+ fn(data) {
+ data
+ |> skip_whitespace
+ |> parse_header_value(headers, name, value)
+ }
+ |> more_please_headers(data)
+
+ // \r\n\r\n
+ <<13, 10, 13, 10, data:bytes>> -> {
+ use name <- result.try(bit_array.to_string(name))
+ use value <- result.map(bit_array.to_string(value))
+ let headers = list.reverse([#(string.lowercase(name), value), ..headers])
+ MultipartHeaders(headers, data)
+ }
+
+ // \r\n\s
+ // \r\n\t
+ <<13, 10, 32, data:bytes>> | <<13, 10, 9, data:bytes>> ->
+ parse_header_value(data, headers, name, value)
+
+ // \r\n
+ <<13, 10, data:bytes>> -> {
+ use name <- result.try(bit_array.to_string(name))
+ use value <- result.try(bit_array.to_string(value))
+ let headers = [#(string.lowercase(name), value), ..headers]
+ parse_header_name(data, headers, <<>>)
+ }
+
+ <<char, rest:bytes>> -> {
+ let value = <<value:bits, char>>
+ parse_header_value(rest, headers, name, value)
+ }
+
+ _ -> Error(Nil)
+ }
+}
+
+fn more_please_headers(
+ continuation: fn(BitArray) -> Result(MultipartHeaders, Nil),
+ existing: BitArray,
+) -> Result(MultipartHeaders, Nil) {
+ Ok(MoreRequiredForHeaders(fn(more) {
+ use <- bool.guard(more == <<>>, return: Error(Nil))
+ continuation(<<existing:bits, more:bits>>)
+ }))
+}
+
+pub type ContentDisposition {
+ ContentDisposition(String, parameters: List(#(String, String)))
+}
+
+pub fn parse_content_disposition(
+ header: String,
+) -> Result(ContentDisposition, Nil) {
+ parse_content_disposition_type(header, "")
+}
+
+fn parse_content_disposition_type(
+ header: String,
+ name: String,
+) -> Result(ContentDisposition, Nil) {
+ case string.pop_grapheme(header) {
+ Error(Nil) -> Ok(ContentDisposition(name, []))
+
+ Ok(#(" ", rest)) | Ok(#("\t", rest)) | Ok(#(";", rest)) -> {
+ let result = parse_rfc_2045_parameters(rest, [])
+ use parameters <- result.map(result)
+ ContentDisposition(name, parameters)
+ }
+
+ Ok(#(grapheme, rest)) ->
+ parse_content_disposition_type(rest, name <> string.lowercase(grapheme))
+ }
+}
+
+fn parse_rfc_2045_parameters(
+ header: String,
+ parameters: List(#(String, String)),
+) -> Result(List(#(String, String)), Nil) {
+ case string.pop_grapheme(header) {
+ Error(Nil) -> Ok(list.reverse(parameters))
+
+ Ok(#(";", rest)) | Ok(#(" ", rest)) | Ok(#("\t", rest)) ->
+ parse_rfc_2045_parameters(rest, parameters)
+
+ Ok(#(grapheme, rest)) -> {
+ let acc = string.lowercase(grapheme)
+ use #(parameter, rest) <- result.try(parse_rfc_2045_parameter(rest, acc))
+ parse_rfc_2045_parameters(rest, [parameter, ..parameters])
+ }
+ }
+}
+
+fn parse_rfc_2045_parameter(
+ header: String,
+ name: String,
+) -> Result(#(#(String, String), String), Nil) {
+ use #(grapheme, rest) <- result.try(string.pop_grapheme(header))
+ case grapheme {
+ "=" -> parse_rfc_2045_parameter_value(rest, name)
+ _ -> parse_rfc_2045_parameter(rest, name <> string.lowercase(grapheme))
+ }
+}
+
+fn parse_rfc_2045_parameter_value(
+ header: String,
+ name: String,
+) -> Result(#(#(String, String), String), Nil) {
+ case string.pop_grapheme(header) {
+ Error(Nil) -> Error(Nil)
+ Ok(#("\"", rest)) -> parse_rfc_2045_parameter_quoted_value(rest, name, "")
+ Ok(#(grapheme, rest)) ->
+ Ok(parse_rfc_2045_parameter_unquoted_value(rest, name, grapheme))
+ }
+}
+
+fn parse_rfc_2045_parameter_quoted_value(
+ header: String,
+ name: String,
+ value: String,
+) -> Result(#(#(String, String), String), Nil) {
+ case string.pop_grapheme(header) {
+ Error(Nil) -> Error(Nil)
+ Ok(#("\"", rest)) -> Ok(#(#(name, value), rest))
+ Ok(#("\\", rest)) -> {
+ use #(grapheme, rest) <- result.try(string.pop_grapheme(rest))
+ parse_rfc_2045_parameter_quoted_value(rest, name, value <> grapheme)
+ }
+ Ok(#(grapheme, rest)) ->
+ parse_rfc_2045_parameter_quoted_value(rest, name, value <> grapheme)
+ }
+}
+
+fn parse_rfc_2045_parameter_unquoted_value(
+ header: String,
+ name: String,
+ value: String,
+) -> #(#(String, String), String) {
+ case string.pop_grapheme(header) {
+ Error(Nil) -> #(#(name, value), header)
+
+ Ok(#(";", rest)) | Ok(#(" ", rest)) | Ok(#("\t", rest)) -> #(
+ #(name, value),
+ rest,
+ )
+
+ Ok(#(grapheme, rest)) ->
+ parse_rfc_2045_parameter_unquoted_value(rest, name, value <> grapheme)
+ }
+}
+
+fn more_please_body(
+ continuation: fn(BitArray) -> Result(MultipartBody, Nil),
+ chunk: BitArray,
+ existing: BitArray,
+) -> Result(MultipartBody, Nil) {
+ fn(more) {
+ use <- bool.guard(more == <<>>, return: Error(Nil))
+ continuation(<<existing:bits, more:bits>>)
+ }
+ |> MoreRequiredForBody(chunk, _)
+ |> Ok
+}
+
+@target(erlang)
+@external(erlang, "gleam_http_native", "decode_method")
+fn do_method_from_dynamic(a: Dynamic) -> Result(Method, nil)
+
+@target(javascript)
+@external(javascript, "../gleam_http_native.mjs", "decode_method")
+fn do_method_from_dynamic(a: Dynamic) -> Result(Method, Nil)
+
+/// A HTTP header is a key-value pair. Header keys should be all lowercase
+/// characters.
+pub type Header =
+ #(String, String)
diff --git a/aoc2023/build/packages/gleam_http/src/gleam/http/cookie.gleam b/aoc2023/build/packages/gleam_http/src/gleam/http/cookie.gleam
new file mode 100644
index 0000000..e9ccb55
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam/http/cookie.gleam
@@ -0,0 +1,128 @@
+import gleam/result
+import gleam/int
+import gleam/list
+import gleam/regex
+import gleam/string
+import gleam/option.{type Option, Some}
+import gleam/http.{type Scheme}
+
+/// Policy options for the SameSite cookie attribute
+///
+/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
+pub type SameSitePolicy {
+ Lax
+ Strict
+ None
+}
+
+fn same_site_to_string(policy) {
+ case policy {
+ Lax -> "Lax"
+ Strict -> "Strict"
+ None -> "None"
+ }
+}
+
+/// Attributes of a cookie when sent to a client in the `set-cookie` header.
+pub type Attributes {
+ Attributes(
+ max_age: Option(Int),
+ domain: Option(String),
+ path: Option(String),
+ secure: Bool,
+ http_only: Bool,
+ same_site: Option(SameSitePolicy),
+ )
+}
+
+/// Helper to create sensible default attributes for a set cookie.
+///
+/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Attributes
+pub fn defaults(scheme: Scheme) {
+ Attributes(
+ max_age: option.None,
+ domain: option.None,
+ path: option.Some("/"),
+ secure: scheme == http.Https,
+ http_only: True,
+ same_site: Some(Lax),
+ )
+}
+
+const epoch = "Expires=Thu, 01 Jan 1970 00:00:00 GMT"
+
+fn cookie_attributes_to_list(attributes) {
+ let Attributes(
+ max_age: max_age,
+ domain: domain,
+ path: path,
+ secure: secure,
+ http_only: http_only,
+ same_site: same_site,
+ ) = attributes
+ [
+ // Expires is a deprecated attribute for cookies, it has been replaced with MaxAge
+ // MaxAge is widely supported and so Expires values are not set.
+ // Only when deleting cookies is the exception made to use the old format,
+ // to ensure complete clearup of cookies if required by an application.
+ case max_age {
+ option.Some(0) -> option.Some([epoch])
+ _ -> option.None
+ },
+ option.map(max_age, fn(max_age) { ["Max-Age=", int.to_string(max_age)] }),
+ option.map(domain, fn(domain) { ["Domain=", domain] }),
+ option.map(path, fn(path) { ["Path=", path] }),
+ case secure {
+ True -> option.Some(["Secure"])
+ False -> option.None
+ },
+ case http_only {
+ True -> option.Some(["HttpOnly"])
+ False -> option.None
+ },
+ option.map(
+ same_site,
+ fn(same_site) { ["SameSite=", same_site_to_string(same_site)] },
+ ),
+ ]
+ |> list.filter_map(option.to_result(_, Nil))
+}
+
+pub fn set_header(name: String, value: String, attributes: Attributes) -> String {
+ [[name, "=", value], ..cookie_attributes_to_list(attributes)]
+ |> list.map(string.join(_, ""))
+ |> string.join("; ")
+}
+
+/// Parse a list of cookies from a header string. Any malformed cookies will be
+/// discarded.
+///
+pub fn parse(cookie_string: String) -> List(#(String, String)) {
+ let assert Ok(re) = regex.from_string("[,;]")
+ regex.split(re, cookie_string)
+ |> list.filter_map(fn(pair) {
+ case string.split_once(string.trim(pair), "=") {
+ Ok(#("", _)) -> Error(Nil)
+ Ok(#(key, value)) -> {
+ let key = string.trim(key)
+ let value = string.trim(value)
+ use _ <- result.then(check_token(key))
+ use _ <- result.then(check_token(value))
+ Ok(#(key, value))
+ }
+ Error(Nil) -> Error(Nil)
+ }
+ })
+}
+
+fn check_token(token: String) -> Result(Nil, Nil) {
+ case string.pop_grapheme(token) {
+ Error(Nil) -> Ok(Nil)
+ Ok(#(" ", _)) -> Error(Nil)
+ Ok(#("\t", _)) -> Error(Nil)
+ Ok(#("\r", _)) -> Error(Nil)
+ Ok(#("\n", _)) -> Error(Nil)
+ Ok(#("\f", _)) -> Error(Nil)
+ Ok(#(_, rest)) -> check_token(rest)
+ }
+}
diff --git a/aoc2023/build/packages/gleam_http/src/gleam/http/request.gleam b/aoc2023/build/packages/gleam_http/src/gleam/http/request.gleam
new file mode 100644
index 0000000..0bf9af9
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam/http/request.gleam
@@ -0,0 +1,267 @@
+import gleam/result
+// TODO: validate_req
+import gleam/http.{type Header, type Method, type Scheme, Get}
+import gleam/http/cookie
+import gleam/option.{type Option}
+import gleam/uri.{type Uri, Uri}
+import gleam/list
+import gleam/string
+import gleam/string_builder
+
+// TODO: document
+pub type Request(body) {
+ Request(
+ method: Method,
+ headers: List(Header),
+ body: body,
+ scheme: Scheme,
+ host: String,
+ port: Option(Int),
+ path: String,
+ query: Option(String),
+ )
+}
+
+/// Return the uri that a request was sent to.
+///
+pub fn to_uri(request: Request(a)) -> Uri {
+ Uri(
+ scheme: option.Some(http.scheme_to_string(request.scheme)),
+ userinfo: option.None,
+ host: option.Some(request.host),
+ port: request.port,
+ path: request.path,
+ query: request.query,
+ fragment: option.None,
+ )
+}
+
+/// Construct a request from a URI.
+///
+pub fn from_uri(uri: Uri) -> Result(Request(String), Nil) {
+ use scheme <- result.then(
+ uri.scheme
+ |> option.unwrap("")
+ |> http.scheme_from_string,
+ )
+ use host <- result.then(
+ uri.host
+ |> option.to_result(Nil),
+ )
+ let req =
+ Request(
+ method: Get,
+ headers: [],
+ body: "",
+ scheme: scheme,
+ host: host,
+ port: uri.port,
+ path: uri.path,
+ query: uri.query,
+ )
+ Ok(req)
+}
+
+/// Get the value for a given header.
+///
+/// If the request does not have that header then `Error(Nil)` is returned.
+///
+pub fn get_header(request: Request(body), key: String) -> Result(String, Nil) {
+ list.key_find(request.headers, string.lowercase(key))
+}
+
+/// Set the header with the given value under the given header key.
+///
+/// If already present, it is replaced.
+pub fn set_header(
+ request: Request(body),
+ key: String,
+ value: String,
+) -> Request(body) {
+ let headers = list.key_set(request.headers, string.lowercase(key), value)
+ Request(..request, headers: headers)
+}
+
+/// Prepend the header with the given value under the given header key.
+///
+/// Similar to `set_header` except if the header already exists it prepends
+/// another header with the same key.
+pub fn prepend_header(
+ request: Request(body),
+ key: String,
+ value: String,
+) -> Request(body) {
+ let headers = [#(string.lowercase(key), value), ..request.headers]
+ Request(..request, headers: headers)
+}
+
+// TODO: record update syntax, which can't be done currently as body type changes
+/// Set the body of the request, overwriting any existing body.
+///
+pub fn set_body(req: Request(old_body), body: new_body) -> Request(new_body) {
+ let Request(
+ method: method,
+ headers: headers,
+ scheme: scheme,
+ host: host,
+ port: port,
+ path: path,
+ query: query,
+ ..,
+ ) = req
+ Request(
+ method: method,
+ headers: headers,
+ body: body,
+ scheme: scheme,
+ host: host,
+ port: port,
+ path: path,
+ query: query,
+ )
+}
+
+/// Update the body of a request using a given function.
+///
+pub fn map(
+ request: Request(old_body),
+ transform: fn(old_body) -> new_body,
+) -> Request(new_body) {
+ request.body
+ |> transform
+ |> set_body(request, _)
+}
+
+/// Return the non-empty segments of a request path.
+///
+/// # Examples
+///
+/// ```gleam
+/// > new()
+/// > |> set_path("/one/two/three")
+/// > |> path_segments
+/// ["one", "two", "three"]
+/// ```
+///
+pub fn path_segments(request: Request(body)) -> List(String) {
+ request.path
+ |> uri.path_segments
+}
+
+/// Decode the query of a request.
+pub fn get_query(request: Request(body)) -> Result(List(#(String, String)), Nil) {
+ case request.query {
+ option.Some(query_string) -> uri.parse_query(query_string)
+ option.None -> Ok([])
+ }
+}
+
+// TODO: escape
+/// Set the query of the request.
+///
+pub fn set_query(
+ req: Request(body),
+ query: List(#(String, String)),
+) -> Request(body) {
+ let pair = fn(t: #(String, String)) {
+ string_builder.from_strings([t.0, "=", t.1])
+ }
+ let query =
+ query
+ |> list.map(pair)
+ |> list.intersperse(string_builder.from_string("&"))
+ |> string_builder.concat
+ |> string_builder.to_string
+ |> option.Some
+ Request(..req, query: query)
+}
+
+/// Set the method of the request.
+///
+pub fn set_method(req: Request(body), method: Method) -> Request(body) {
+ Request(..req, method: method)
+}
+
+/// A request with commonly used default values. This request can be used as
+/// an initial value and then update to create the desired request.
+///
+pub fn new() -> Request(String) {
+ Request(
+ method: Get,
+ headers: [],
+ body: "",
+ scheme: http.Https,
+ host: "localhost",
+ port: option.None,
+ path: "",
+ query: option.None,
+ )
+}
+
+/// Construct a request from a URL string
+///
+pub fn to(url: String) -> Result(Request(String), Nil) {
+ url
+ |> uri.parse
+ |> result.then(from_uri)
+}
+
+/// Set the scheme (protocol) of the request.
+///
+pub fn set_scheme(req: Request(body), scheme: Scheme) -> Request(body) {
+ Request(..req, scheme: scheme)
+}
+
+/// Set the method of the request.
+///
+pub fn set_host(req: Request(body), host: String) -> Request(body) {
+ Request(..req, host: host)
+}
+
+/// Set the port of the request.
+///
+pub fn set_port(req: Request(body), port: Int) -> Request(body) {
+ Request(..req, port: option.Some(port))
+}
+
+/// Set the path of the request.
+///
+pub fn set_path(req: Request(body), path: String) -> Request(body) {
+ Request(..req, path: path)
+}
+
+/// Send a cookie with a request
+///
+/// Multiple cookies are added to the same cookie header.
+pub fn set_cookie(req: Request(body), name: String, value: String) {
+ let new_cookie_string = string.join([name, value], "=")
+
+ let #(cookies_string, headers) = case list.key_pop(req.headers, "cookie") {
+ Ok(#(cookies_string, headers)) -> {
+ let cookies_string =
+ string.join([cookies_string, new_cookie_string], "; ")
+ #(cookies_string, headers)
+ }
+ Error(Nil) -> #(new_cookie_string, req.headers)
+ }
+
+ Request(..req, headers: [#("cookie", cookies_string), ..headers])
+}
+
+/// Fetch the cookies sent in a request.
+///
+/// Note badly formed cookie pairs will be ignored.
+/// RFC6265 specifies that invalid cookie names/attributes should be ignored.
+pub fn get_cookies(req) -> List(#(String, String)) {
+ let Request(headers: headers, ..) = req
+
+ headers
+ |> list.filter_map(fn(header) {
+ let #(name, value) = header
+ case name {
+ "cookie" -> Ok(cookie.parse(value))
+ _ -> Error(Nil)
+ }
+ })
+ |> list.flatten()
+}
diff --git a/aoc2023/build/packages/gleam_http/src/gleam/http/response.gleam b/aoc2023/build/packages/gleam_http/src/gleam/http/response.gleam
new file mode 100644
index 0000000..87f9140
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam/http/response.gleam
@@ -0,0 +1,141 @@
+import gleam/result
+import gleam/http.{type Header}
+import gleam/http/cookie
+import gleam/list
+import gleam/string
+import gleam/option
+
+// TODO: document
+pub type Response(body) {
+ Response(status: Int, headers: List(Header), body: body)
+}
+
+/// Update the body of a response using a given result returning function.
+///
+/// If the given function returns an `Ok` value the body is set, if it returns
+/// an `Error` value then the error is returned.
+///
+pub fn try_map(
+ response: Response(old_body),
+ transform: fn(old_body) -> Result(new_body, error),
+) -> Result(Response(new_body), error) {
+ use body <- result.then(transform(response.body))
+ Ok(set_body(response, body))
+}
+
+/// Construct an empty Response.
+///
+/// The body type of the returned response is `String` and could be set with a
+/// call to `set_body`.
+///
+pub fn new(status: Int) -> Response(String) {
+ Response(status: status, headers: [], body: "")
+}
+
+/// Get the value for a given header.
+///
+/// If the response does not have that header then `Error(Nil)` is returned.
+///
+pub fn get_header(response: Response(body), key: String) -> Result(String, Nil) {
+ list.key_find(response.headers, string.lowercase(key))
+}
+
+/// Set the header with the given value under the given header key.
+///
+/// If the response already has that key, it is replaced.
+pub fn set_header(
+ response: Response(body),
+ key: String,
+ value: String,
+) -> Response(body) {
+ let headers = list.key_set(response.headers, string.lowercase(key), value)
+ Response(..response, headers: headers)
+}
+
+/// Prepend the header with the given value under the given header key.
+///
+/// Similar to `set_header` except if the header already exists it prepends
+/// another header with the same key.
+pub fn prepend_header(
+ response: Response(body),
+ key: String,
+ value: String,
+) -> Response(body) {
+ let headers = [#(string.lowercase(key), value), ..response.headers]
+ Response(..response, headers: headers)
+}
+
+/// Set the body of the response, overwriting any existing body.
+///
+pub fn set_body(
+ response: Response(old_body),
+ body: new_body,
+) -> Response(new_body) {
+ let Response(status: status, headers: headers, ..) = response
+ Response(status: status, headers: headers, body: body)
+}
+
+/// Update the body of a response using a given function.
+///
+pub fn map(
+ response: Response(old_body),
+ transform: fn(old_body) -> new_body,
+) -> Response(new_body) {
+ response.body
+ |> transform
+ |> set_body(response, _)
+}
+
+/// Create a response that redirects to the given uri.
+///
+pub fn redirect(uri: String) -> Response(String) {
+ Response(
+ status: 303,
+ headers: [#("location", uri)],
+ body: string.append("You are being redirected to ", uri),
+ )
+}
+
+/// Fetch the cookies sent in a response.
+///
+/// Badly formed cookies will be discarded.
+///
+pub fn get_cookies(resp) -> List(#(String, String)) {
+ let Response(headers: headers, ..) = resp
+ headers
+ |> list.filter_map(fn(header) {
+ let #(name, value) = header
+ case name {
+ "set-cookie" -> Ok(cookie.parse(value))
+ _ -> Error(Nil)
+ }
+ })
+ |> list.flatten()
+}
+
+/// Set a cookie value for a client
+///
+pub fn set_cookie(
+ response: Response(t),
+ name: String,
+ value: String,
+ attributes: cookie.Attributes,
+) -> Response(t) {
+ prepend_header(
+ response,
+ "set-cookie",
+ cookie.set_header(name, value, attributes),
+ )
+}
+
+/// Expire a cookie value for a client
+///
+/// Note: The attributes value should be the same as when the response cookie was set.
+pub fn expire_cookie(
+ response: Response(t),
+ name: String,
+ attributes: cookie.Attributes,
+) -> Response(t) {
+ let attrs = cookie.Attributes(..attributes, max_age: option.Some(0))
+ set_cookie(response, name, "", attrs)
+}
diff --git a/aoc2023/build/packages/gleam_http/src/gleam/http/service.gleam b/aoc2023/build/packages/gleam_http/src/gleam/http/service.gleam
new file mode 100644
index 0000000..3dfac87
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam/http/service.gleam
@@ -0,0 +1,82 @@
+import gleam/http.{Delete, Patch, Post, Put}
+import gleam/http/request.{type Request}
+import gleam/http/response.{type Response}
+import gleam/list
+import gleam/result
+
+// TODO: document
+pub type Service(in, out) =
+ fn(Request(in)) -> Response(out)
+
+pub type Middleware(before_req, before_resp, after_req, after_resp) =
+ fn(Service(before_req, before_resp)) -> Service(after_req, after_resp)
+
+/// A middleware that transform the response body returned by the service using
+/// a given function.
+///
+pub fn map_response_body(
+ service: Service(req, a),
+ with mapper: fn(a) -> b,
+) -> Service(req, b) {
+ fn(req) {
+ req
+ |> service
+ |> response.map(mapper)
+ }
+}
+
+/// A middleware that prepends a header to the request.
+///
+pub fn prepend_response_header(
+ service: Service(req, resp),
+ key: String,
+ value: String,
+) -> Service(req, resp) {
+ fn(req) {
+ req
+ |> service
+ |> response.prepend_header(key, value)
+ }
+}
+
+fn ensure_post(req: Request(a)) {
+ case req.method {
+ Post -> Ok(req)
+ _ -> Error(Nil)
+ }
+}
+
+fn get_override_method(request: Request(t)) -> Result(http.Method, Nil) {
+ use query_params <- result.then(request.get_query(request))
+ use method <- result.then(list.key_find(query_params, "_method"))
+ use method <- result.then(http.parse_method(method))
+ case method {
+ Put | Patch | Delete -> Ok(method)
+ _ -> Error(Nil)
+ }
+}
+
+/// A middleware that overrides an incoming POST request with a method given in
+/// the request's `_method` query paramerter. This is useful as web browsers
+/// typically only support GET and POST requests, but our application may
+/// expect other HTTP methods that are more semantically correct.
+///
+/// The methods PUT, PATCH, and DELETE are accepted for overriding, all others
+/// are ignored.
+///
+/// The `_method` query paramerter can be specified in a HTML form like so:
+///
+/// <form method="POST" action="/item/1?_method=DELETE">
+/// <button type="submit">Delete item</button>
+/// </form>
+///
+pub fn method_override(service: Service(req, resp)) -> Service(req, resp) {
+ fn(request) {
+ request
+ |> ensure_post
+ |> result.then(get_override_method)
+ |> result.map(request.set_method(request, _))
+ |> result.unwrap(request)
+ |> service
+ }
+}
diff --git a/aoc2023/build/packages/gleam_http/src/gleam@http.erl b/aoc2023/build/packages/gleam_http/src/gleam@http.erl
new file mode 100644
index 0000000..91ee6e8
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam@http.erl
@@ -0,0 +1,626 @@
+-module(gleam@http).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]).
+
+-export([parse_method/1, method_to_string/1, scheme_to_string/1, scheme_from_string/1, parse_content_disposition/1, parse_multipart_body/2, method_from_dynamic/1, parse_multipart_headers/2]).
+-export_type([method/0, scheme/0, multipart_headers/0, multipart_body/0, content_disposition/0]).
+
+-type method() :: get |
+ post |
+ head |
+ put |
+ delete |
+ trace |
+ connect |
+ options |
+ patch |
+ {other, binary()}.
+
+-type scheme() :: http | https.
+
+-type multipart_headers() :: {multipart_headers,
+ list({binary(), binary()}),
+ bitstring()} |
+ {more_required_for_headers,
+ fun((bitstring()) -> {ok, multipart_headers()} | {error, nil})}.
+
+-type multipart_body() :: {multipart_body, bitstring(), boolean(), bitstring()} |
+ {more_required_for_body,
+ bitstring(),
+ fun((bitstring()) -> {ok, multipart_body()} | {error, nil})}.
+
+-type content_disposition() :: {content_disposition,
+ binary(),
+ list({binary(), binary()})}.
+
+-spec parse_method(binary()) -> {ok, method()} | {error, nil}.
+parse_method(S) ->
+ case gleam@string:lowercase(S) of
+ <<"connect"/utf8>> ->
+ {ok, connect};
+
+ <<"delete"/utf8>> ->
+ {ok, delete};
+
+ <<"get"/utf8>> ->
+ {ok, get};
+
+ <<"head"/utf8>> ->
+ {ok, head};
+
+ <<"options"/utf8>> ->
+ {ok, options};
+
+ <<"patch"/utf8>> ->
+ {ok, patch};
+
+ <<"post"/utf8>> ->
+ {ok, post};
+
+ <<"put"/utf8>> ->
+ {ok, put};
+
+ <<"trace"/utf8>> ->
+ {ok, trace};
+
+ _ ->
+ {error, nil}
+ end.
+
+-spec method_to_string(method()) -> binary().
+method_to_string(Method) ->
+ case Method of
+ connect ->
+ <<"connect"/utf8>>;
+
+ delete ->
+ <<"delete"/utf8>>;
+
+ get ->
+ <<"get"/utf8>>;
+
+ head ->
+ <<"head"/utf8>>;
+
+ options ->
+ <<"options"/utf8>>;
+
+ patch ->
+ <<"patch"/utf8>>;
+
+ post ->
+ <<"post"/utf8>>;
+
+ put ->
+ <<"put"/utf8>>;
+
+ trace ->
+ <<"trace"/utf8>>;
+
+ {other, S} ->
+ S
+ end.
+
+-spec scheme_to_string(scheme()) -> binary().
+scheme_to_string(Scheme) ->
+ case Scheme of
+ http ->
+ <<"http"/utf8>>;
+
+ https ->
+ <<"https"/utf8>>
+ end.
+
+-spec scheme_from_string(binary()) -> {ok, scheme()} | {error, nil}.
+scheme_from_string(Scheme) ->
+ case gleam@string:lowercase(Scheme) of
+ <<"http"/utf8>> ->
+ {ok, http};
+
+ <<"https"/utf8>> ->
+ {ok, https};
+
+ _ ->
+ {error, nil}
+ end.
+
+-spec skip_whitespace(bitstring()) -> bitstring().
+skip_whitespace(Data) ->
+ case Data of
+ <<32, Data@1/binary>> ->
+ skip_whitespace(Data@1);
+
+ <<9, Data@1/binary>> ->
+ skip_whitespace(Data@1);
+
+ _ ->
+ Data
+ end.
+
+-spec more_please_headers(
+ fun((bitstring()) -> {ok, multipart_headers()} | {error, nil}),
+ bitstring()
+) -> {ok, multipart_headers()} | {error, nil}.
+more_please_headers(Continuation, Existing) ->
+ {ok,
+ {more_required_for_headers,
+ fun(More) ->
+ gleam@bool:guard(
+ More =:= <<>>,
+ {error, nil},
+ fun() ->
+ Continuation(<<Existing/bitstring, More/bitstring>>)
+ end
+ )
+ end}}.
+
+-spec parse_rfc_2045_parameter_quoted_value(binary(), binary(), binary()) -> {ok,
+ {{binary(), binary()}, binary()}} |
+ {error, nil}.
+parse_rfc_2045_parameter_quoted_value(Header, Name, Value) ->
+ case gleam@string:pop_grapheme(Header) of
+ {error, nil} ->
+ {error, nil};
+
+ {ok, {<<"\""/utf8>>, Rest}} ->
+ {ok, {{Name, Value}, Rest}};
+
+ {ok, {<<"\\"/utf8>>, Rest@1}} ->
+ gleam@result:'try'(
+ gleam@string:pop_grapheme(Rest@1),
+ fun(_use0) ->
+ {Grapheme, Rest@2} = _use0,
+ parse_rfc_2045_parameter_quoted_value(
+ Rest@2,
+ Name,
+ <<Value/binary, Grapheme/binary>>
+ )
+ end
+ );
+
+ {ok, {Grapheme@1, Rest@3}} ->
+ parse_rfc_2045_parameter_quoted_value(
+ Rest@3,
+ Name,
+ <<Value/binary, Grapheme@1/binary>>
+ )
+ end.
+
+-spec parse_rfc_2045_parameter_unquoted_value(binary(), binary(), binary()) -> {{binary(),
+ binary()},
+ binary()}.
+parse_rfc_2045_parameter_unquoted_value(Header, Name, Value) ->
+ case gleam@string:pop_grapheme(Header) of
+ {error, nil} ->
+ {{Name, Value}, Header};
+
+ {ok, {<<";"/utf8>>, Rest}} ->
+ {{Name, Value}, Rest};
+
+ {ok, {<<" "/utf8>>, Rest}} ->
+ {{Name, Value}, Rest};
+
+ {ok, {<<"\t"/utf8>>, Rest}} ->
+ {{Name, Value}, Rest};
+
+ {ok, {Grapheme, Rest@1}} ->
+ parse_rfc_2045_parameter_unquoted_value(
+ Rest@1,
+ Name,
+ <<Value/binary, Grapheme/binary>>
+ )
+ end.
+
+-spec parse_rfc_2045_parameter_value(binary(), binary()) -> {ok,
+ {{binary(), binary()}, binary()}} |
+ {error, nil}.
+parse_rfc_2045_parameter_value(Header, Name) ->
+ case gleam@string:pop_grapheme(Header) of
+ {error, nil} ->
+ {error, nil};
+
+ {ok, {<<"\""/utf8>>, Rest}} ->
+ parse_rfc_2045_parameter_quoted_value(Rest, Name, <<""/utf8>>);
+
+ {ok, {Grapheme, Rest@1}} ->
+ {ok,
+ parse_rfc_2045_parameter_unquoted_value(Rest@1, Name, Grapheme)}
+ end.
+
+-spec parse_rfc_2045_parameter(binary(), binary()) -> {ok,
+ {{binary(), binary()}, binary()}} |
+ {error, nil}.
+parse_rfc_2045_parameter(Header, Name) ->
+ gleam@result:'try'(
+ gleam@string:pop_grapheme(Header),
+ fun(_use0) ->
+ {Grapheme, Rest} = _use0,
+ case Grapheme of
+ <<"="/utf8>> ->
+ parse_rfc_2045_parameter_value(Rest, Name);
+
+ _ ->
+ parse_rfc_2045_parameter(
+ Rest,
+ <<Name/binary,
+ (gleam@string:lowercase(Grapheme))/binary>>
+ )
+ end
+ end
+ ).
+
+-spec parse_rfc_2045_parameters(binary(), list({binary(), binary()})) -> {ok,
+ list({binary(), binary()})} |
+ {error, nil}.
+parse_rfc_2045_parameters(Header, Parameters) ->
+ case gleam@string:pop_grapheme(Header) of
+ {error, nil} ->
+ {ok, gleam@list:reverse(Parameters)};
+
+ {ok, {<<";"/utf8>>, Rest}} ->
+ parse_rfc_2045_parameters(Rest, Parameters);
+
+ {ok, {<<" "/utf8>>, Rest}} ->
+ parse_rfc_2045_parameters(Rest, Parameters);
+
+ {ok, {<<"\t"/utf8>>, Rest}} ->
+ parse_rfc_2045_parameters(Rest, Parameters);
+
+ {ok, {Grapheme, Rest@1}} ->
+ Acc = gleam@string:lowercase(Grapheme),
+ gleam@result:'try'(
+ parse_rfc_2045_parameter(Rest@1, Acc),
+ fun(_use0) ->
+ {Parameter, Rest@2} = _use0,
+ parse_rfc_2045_parameters(Rest@2, [Parameter | Parameters])
+ end
+ )
+ end.
+
+-spec parse_content_disposition_type(binary(), binary()) -> {ok,
+ content_disposition()} |
+ {error, nil}.
+parse_content_disposition_type(Header, Name) ->
+ case gleam@string:pop_grapheme(Header) of
+ {error, nil} ->
+ {ok, {content_disposition, Name, []}};
+
+ {ok, {<<" "/utf8>>, Rest}} ->
+ Result = parse_rfc_2045_parameters(Rest, []),
+ gleam@result:map(
+ Result,
+ fun(Parameters) -> {content_disposition, Name, Parameters} end
+ );
+
+ {ok, {<<"\t"/utf8>>, Rest}} ->
+ Result = parse_rfc_2045_parameters(Rest, []),
+ gleam@result:map(
+ Result,
+ fun(Parameters) -> {content_disposition, Name, Parameters} end
+ );
+
+ {ok, {<<";"/utf8>>, Rest}} ->
+ Result = parse_rfc_2045_parameters(Rest, []),
+ gleam@result:map(
+ Result,
+ fun(Parameters) -> {content_disposition, Name, Parameters} end
+ );
+
+ {ok, {Grapheme, Rest@1}} ->
+ parse_content_disposition_type(
+ Rest@1,
+ <<Name/binary, (gleam@string:lowercase(Grapheme))/binary>>
+ )
+ end.
+
+-spec parse_content_disposition(binary()) -> {ok, content_disposition()} |
+ {error, nil}.
+parse_content_disposition(Header) ->
+ parse_content_disposition_type(Header, <<""/utf8>>).
+
+-spec more_please_body(
+ fun((bitstring()) -> {ok, multipart_body()} | {error, nil}),
+ bitstring(),
+ bitstring()
+) -> {ok, multipart_body()} | {error, nil}.
+more_please_body(Continuation, Chunk, Existing) ->
+ _pipe = fun(More) ->
+ gleam@bool:guard(
+ More =:= <<>>,
+ {error, nil},
+ fun() -> Continuation(<<Existing/bitstring, More/bitstring>>) end
+ )
+ end,
+ _pipe@1 = {more_required_for_body, Chunk, _pipe},
+ {ok, _pipe@1}.
+
+-spec parse_body_loop(bitstring(), bitstring(), bitstring()) -> {ok,
+ multipart_body()} |
+ {error, nil}.
+parse_body_loop(Data, Boundary, Body) ->
+ Dsize = erlang:byte_size(Data),
+ Bsize = erlang:byte_size(Boundary),
+ Required = 6 + Bsize,
+ case Data of
+ _ when Dsize < Required ->
+ more_please_body(
+ fun(_capture) -> parse_body_loop(_capture, Boundary, <<>>) end,
+ Body,
+ Data
+ );
+
+ <<13, 10, Data@1/binary>> ->
+ Desired = <<45, 45, Boundary/bitstring>>,
+ Size = erlang:byte_size(Desired),
+ Dsize@1 = erlang:byte_size(Data@1),
+ Prefix = gleam_stdlib:bit_array_slice(Data@1, 0, Size),
+ Rest = gleam_stdlib:bit_array_slice(Data@1, Size, Dsize@1 - Size),
+ case {Prefix =:= {ok, Desired}, Rest} of
+ {true, {ok, <<13, 10, _/binary>>}} ->
+ {ok, {multipart_body, Body, false, Data@1}};
+
+ {true, {ok, <<45, 45, Data@2/binary>>}} ->
+ {ok, {multipart_body, Body, true, Data@2}};
+
+ {false, _} ->
+ parse_body_loop(
+ Data@1,
+ Boundary,
+ <<Body/bitstring, 13, 10>>
+ );
+
+ {_, _} ->
+ {error, nil}
+ end;
+
+ <<Char, Data@3/binary>> ->
+ parse_body_loop(Data@3, Boundary, <<Body/bitstring, Char>>)
+ end.
+
+-spec parse_body_with_bit_array(bitstring(), bitstring()) -> {ok,
+ multipart_body()} |
+ {error, nil}.
+parse_body_with_bit_array(Data, Boundary) ->
+ Bsize = erlang:byte_size(Boundary),
+ Prefix = gleam_stdlib:bit_array_slice(Data, 0, 2 + Bsize),
+ case Prefix =:= {ok, <<45, 45, Boundary/bitstring>>} of
+ true ->
+ {ok, {multipart_body, <<>>, false, Data}};
+
+ false ->
+ parse_body_loop(Data, Boundary, <<>>)
+ end.
+
+-spec parse_multipart_body(bitstring(), binary()) -> {ok, multipart_body()} |
+ {error, nil}.
+parse_multipart_body(Data, Boundary) ->
+ _pipe = Boundary,
+ _pipe@1 = gleam_stdlib:identity(_pipe),
+ parse_body_with_bit_array(Data, _pipe@1).
+
+-spec method_from_dynamic(gleam@dynamic:dynamic_()) -> {ok, method()} |
+ {error, list(gleam@dynamic:decode_error())}.
+method_from_dynamic(Value) ->
+ case gleam_http_native:decode_method(Value) of
+ {ok, Method} ->
+ {ok, Method};
+
+ {error, _} ->
+ {error,
+ [{decode_error,
+ <<"HTTP method"/utf8>>,
+ gleam@dynamic:classify(Value),
+ []}]}
+ end.
+
+-spec parse_header_value(
+ bitstring(),
+ list({binary(), binary()}),
+ bitstring(),
+ bitstring()
+) -> {ok, multipart_headers()} | {error, nil}.
+parse_header_value(Data, Headers, Name, Value) ->
+ Size = erlang:byte_size(Data),
+ case Data of
+ _ when Size < 4 ->
+ _pipe@2 = fun(Data@1) -> _pipe = Data@1,
+ _pipe@1 = skip_whitespace(_pipe),
+ parse_header_value(_pipe@1, Headers, Name, Value) end,
+ more_please_headers(_pipe@2, Data);
+
+ <<13, 10, 13, 10, Data@2/binary>> ->
+ gleam@result:'try'(
+ gleam@bit_array:to_string(Name),
+ fun(Name@1) ->
+ gleam@result:map(
+ gleam@bit_array:to_string(Value),
+ fun(Value@1) ->
+ Headers@1 = gleam@list:reverse(
+ [{gleam@string:lowercase(Name@1), Value@1} |
+ Headers]
+ ),
+ {multipart_headers, Headers@1, Data@2}
+ end
+ )
+ end
+ );
+
+ <<13, 10, 32, Data@3/binary>> ->
+ parse_header_value(Data@3, Headers, Name, Value);
+
+ <<13, 10, 9, Data@3/binary>> ->
+ parse_header_value(Data@3, Headers, Name, Value);
+
+ <<13, 10, Data@4/binary>> ->
+ gleam@result:'try'(
+ gleam@bit_array:to_string(Name),
+ fun(Name@2) ->
+ gleam@result:'try'(
+ gleam@bit_array:to_string(Value),
+ fun(Value@2) ->
+ Headers@2 = [{gleam@string:lowercase(Name@2),
+ Value@2} |
+ Headers],
+ parse_header_name(Data@4, Headers@2, <<>>)
+ end
+ )
+ end
+ );
+
+ <<Char, Rest/binary>> ->
+ Value@3 = <<Value/bitstring, Char>>,
+ parse_header_value(Rest, Headers, Name, Value@3);
+
+ _ ->
+ {error, nil}
+ end.
+
+-spec parse_header_name(bitstring(), list({binary(), binary()}), bitstring()) -> {ok,
+ multipart_headers()} |
+ {error, nil}.
+parse_header_name(Data, Headers, Name) ->
+ case skip_whitespace(Data) of
+ <<58, Data@1/binary>> ->
+ _pipe = Data@1,
+ _pipe@1 = skip_whitespace(_pipe),
+ parse_header_value(_pipe@1, Headers, Name, <<>>);
+
+ <<Char, Data@2/binary>> ->
+ parse_header_name(Data@2, Headers, <<Name/bitstring, Char>>);
+
+ <<>> ->
+ more_please_headers(
+ fun(_capture) -> parse_header_name(_capture, Headers, Name) end,
+ Data
+ )
+ end.
+
+-spec do_parse_headers(bitstring()) -> {ok, multipart_headers()} | {error, nil}.
+do_parse_headers(Data) ->
+ case Data of
+ <<13, 10, 13, 10, Data@1/binary>> ->
+ {ok, {multipart_headers, [], Data@1}};
+
+ <<13, 10, Data@2/binary>> ->
+ parse_header_name(Data@2, [], <<>>);
+
+ <<13>> ->
+ more_please_headers(fun do_parse_headers/1, Data);
+
+ <<>> ->
+ more_please_headers(fun do_parse_headers/1, Data);
+
+ _ ->
+ {error, nil}
+ end.
+
+-spec parse_headers_after_prelude(bitstring(), bitstring()) -> {ok,
+ multipart_headers()} |
+ {error, nil}.
+parse_headers_after_prelude(Data, Boundary) ->
+ Dsize = erlang:byte_size(Data),
+ Bsize = erlang:byte_size(Boundary),
+ Required_size = Bsize + 4,
+ gleam@bool:guard(
+ Dsize < Required_size,
+ more_please_headers(
+ fun(_capture) -> parse_headers_after_prelude(_capture, Boundary) end,
+ Data
+ ),
+ fun() ->
+ gleam@result:'try'(
+ gleam_stdlib:bit_array_slice(Data, 0, Required_size - 2),
+ fun(Prefix) ->
+ gleam@result:'try'(
+ gleam_stdlib:bit_array_slice(Data, 2 + Bsize, 2),
+ fun(Second) ->
+ Desired = <<45, 45, Boundary/bitstring>>,
+ gleam@bool:guard(
+ Prefix /= Desired,
+ {error, nil},
+ fun() -> case Second =:= <<45, 45>> of
+ true ->
+ Rest_size = Dsize - Required_size,
+ gleam@result:map(
+ gleam_stdlib:bit_array_slice(
+ Data,
+ Required_size,
+ Rest_size
+ ),
+ fun(Data@1) ->
+ {multipart_headers,
+ [],
+ Data@1}
+ end
+ );
+
+ false ->
+ Start = Required_size - 2,
+ Rest_size@1 = (Dsize - Required_size)
+ + 2,
+ gleam@result:'try'(
+ gleam_stdlib:bit_array_slice(
+ Data,
+ Start,
+ Rest_size@1
+ ),
+ fun(Data@2) ->
+ do_parse_headers(Data@2)
+ end
+ )
+ end end
+ )
+ end
+ )
+ end
+ )
+ end
+ ).
+
+-spec skip_preamble(bitstring(), bitstring()) -> {ok, multipart_headers()} |
+ {error, nil}.
+skip_preamble(Data, Boundary) ->
+ Data_size = erlang:byte_size(Data),
+ Boundary_size = erlang:byte_size(Boundary),
+ Required = Boundary_size + 4,
+ case Data of
+ _ when Data_size < Required ->
+ more_please_headers(
+ fun(_capture) -> skip_preamble(_capture, Boundary) end,
+ Data
+ );
+
+ <<13, 10, 45, 45, Data@1/binary>> ->
+ case gleam_stdlib:bit_array_slice(Data@1, 0, Boundary_size) of
+ {ok, Prefix} when Prefix =:= Boundary ->
+ Start = Boundary_size,
+ Length = erlang:byte_size(Data@1) - Boundary_size,
+ gleam@result:'try'(
+ gleam_stdlib:bit_array_slice(Data@1, Start, Length),
+ fun(Rest) -> do_parse_headers(Rest) end
+ );
+
+ {ok, _} ->
+ skip_preamble(Data@1, Boundary);
+
+ {error, _} ->
+ {error, nil}
+ end;
+
+ <<_, Data@2/binary>> ->
+ skip_preamble(Data@2, Boundary)
+ end.
+
+-spec parse_multipart_headers(bitstring(), binary()) -> {ok,
+ multipart_headers()} |
+ {error, nil}.
+parse_multipart_headers(Data, Boundary) ->
+ Boundary@1 = gleam_stdlib:identity(Boundary),
+ Prefix = <<45, 45, Boundary@1/bitstring>>,
+ case gleam_stdlib:bit_array_slice(Data, 0, erlang:byte_size(Prefix)) =:= {ok,
+ Prefix} of
+ true ->
+ parse_headers_after_prelude(Data, Boundary@1);
+
+ false ->
+ skip_preamble(Data, Boundary@1)
+ end.
diff --git a/aoc2023/build/packages/gleam_http/src/gleam@http@cookie.erl b/aoc2023/build/packages/gleam_http/src/gleam@http@cookie.erl
new file mode 100644
index 0000000..9d6d13e
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam@http@cookie.erl
@@ -0,0 +1,153 @@
+-module(gleam@http@cookie).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]).
+
+-export([defaults/1, set_header/3, parse/1]).
+-export_type([same_site_policy/0, attributes/0]).
+
+-type same_site_policy() :: lax | strict | none.
+
+-type attributes() :: {attributes,
+ gleam@option:option(integer()),
+ gleam@option:option(binary()),
+ gleam@option:option(binary()),
+ boolean(),
+ boolean(),
+ gleam@option:option(same_site_policy())}.
+
+-spec same_site_to_string(same_site_policy()) -> binary().
+same_site_to_string(Policy) ->
+ case Policy of
+ lax ->
+ <<"Lax"/utf8>>;
+
+ strict ->
+ <<"Strict"/utf8>>;
+
+ none ->
+ <<"None"/utf8>>
+ end.
+
+-spec defaults(gleam@http:scheme()) -> attributes().
+defaults(Scheme) ->
+ {attributes,
+ none,
+ none,
+ {some, <<"/"/utf8>>},
+ Scheme =:= https,
+ true,
+ {some, lax}}.
+
+-spec cookie_attributes_to_list(attributes()) -> list(list(binary())).
+cookie_attributes_to_list(Attributes) ->
+ {attributes, Max_age, Domain, Path, Secure, Http_only, Same_site} = Attributes,
+ _pipe = [case Max_age of
+ {some, 0} ->
+ {some, [<<"Expires=Thu, 01 Jan 1970 00:00:00 GMT"/utf8>>]};
+
+ _ ->
+ none
+ end, gleam@option:map(
+ Max_age,
+ fun(Max_age@1) ->
+ [<<"Max-Age="/utf8>>, gleam@int:to_string(Max_age@1)]
+ end
+ ), gleam@option:map(
+ Domain,
+ fun(Domain@1) -> [<<"Domain="/utf8>>, Domain@1] end
+ ), gleam@option:map(Path, fun(Path@1) -> [<<"Path="/utf8>>, Path@1] end), case Secure of
+ true ->
+ {some, [<<"Secure"/utf8>>]};
+
+ false ->
+ none
+ end, case Http_only of
+ true ->
+ {some, [<<"HttpOnly"/utf8>>]};
+
+ false ->
+ none
+ end, gleam@option:map(
+ Same_site,
+ fun(Same_site@1) ->
+ [<<"SameSite="/utf8>>, same_site_to_string(Same_site@1)]
+ end
+ )],
+ gleam@list:filter_map(
+ _pipe,
+ fun(_capture) -> gleam@option:to_result(_capture, nil) end
+ ).
+
+-spec set_header(binary(), binary(), attributes()) -> binary().
+set_header(Name, Value, Attributes) ->
+ _pipe = [[Name, <<"="/utf8>>, Value] |
+ cookie_attributes_to_list(Attributes)],
+ _pipe@1 = gleam@list:map(
+ _pipe,
+ fun(_capture) -> gleam@string:join(_capture, <<""/utf8>>) end
+ ),
+ gleam@string:join(_pipe@1, <<"; "/utf8>>).
+
+-spec check_token(binary()) -> {ok, nil} | {error, nil}.
+check_token(Token) ->
+ case gleam@string:pop_grapheme(Token) of
+ {error, nil} ->
+ {ok, nil};
+
+ {ok, {<<" "/utf8>>, _}} ->
+ {error, nil};
+
+ {ok, {<<"\t"/utf8>>, _}} ->
+ {error, nil};
+
+ {ok, {<<"\r"/utf8>>, _}} ->
+ {error, nil};
+
+ {ok, {<<"\n"/utf8>>, _}} ->
+ {error, nil};
+
+ {ok, {<<"\f"/utf8>>, _}} ->
+ {error, nil};
+
+ {ok, {_, Rest}} ->
+ check_token(Rest)
+ end.
+
+-spec parse(binary()) -> list({binary(), binary()}).
+parse(Cookie_string) ->
+ _assert_subject = gleam@regex:from_string(<<"[,;]"/utf8>>),
+ {ok, Re} = case _assert_subject of
+ {ok, _} -> _assert_subject;
+ _assert_fail ->
+ erlang:error(#{gleam_error => let_assert,
+ message => <<"Assertion pattern match failed"/utf8>>,
+ value => _assert_fail,
+ module => <<"gleam/http/cookie"/utf8>>,
+ function => <<"parse"/utf8>>,
+ line => 101})
+ end,
+ _pipe = gleam@regex:split(Re, Cookie_string),
+ gleam@list:filter_map(
+ _pipe,
+ fun(Pair) ->
+ case gleam@string:split_once(gleam@string:trim(Pair), <<"="/utf8>>) of
+ {ok, {<<""/utf8>>, _}} ->
+ {error, nil};
+
+ {ok, {Key, Value}} ->
+ Key@1 = gleam@string:trim(Key),
+ Value@1 = gleam@string:trim(Value),
+ gleam@result:then(
+ check_token(Key@1),
+ fun(_) ->
+ gleam@result:then(
+ check_token(Value@1),
+ fun(_) -> {ok, {Key@1, Value@1}} end
+ )
+ end
+ );
+
+ {error, nil} ->
+ {error, nil}
+ end
+ end
+ ).
diff --git a/aoc2023/build/packages/gleam_http/src/gleam@http@request.erl b/aoc2023/build/packages/gleam_http/src/gleam@http@request.erl
new file mode 100644
index 0000000..630788d
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam@http@request.erl
@@ -0,0 +1,202 @@
+-module(gleam@http@request).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]).
+
+-export([to_uri/1, from_uri/1, get_header/2, set_header/3, prepend_header/3, set_body/2, map/2, path_segments/1, get_query/1, set_query/2, set_method/2, new/0, to/1, set_scheme/2, set_host/2, set_port/2, set_path/2, set_cookie/3, get_cookies/1]).
+-export_type([request/1]).
+
+-type request(FYJ) :: {request,
+ gleam@http:method(),
+ list({binary(), binary()}),
+ FYJ,
+ gleam@http:scheme(),
+ binary(),
+ gleam@option:option(integer()),
+ binary(),
+ gleam@option:option(binary())}.
+
+-spec to_uri(request(any())) -> gleam@uri:uri().
+to_uri(Request) ->
+ {uri,
+ {some, gleam@http:scheme_to_string(erlang:element(5, Request))},
+ none,
+ {some, erlang:element(6, Request)},
+ erlang:element(7, Request),
+ erlang:element(8, Request),
+ erlang:element(9, Request),
+ none}.
+
+-spec from_uri(gleam@uri:uri()) -> {ok, request(binary())} | {error, nil}.
+from_uri(Uri) ->
+ gleam@result:then(
+ begin
+ _pipe = erlang:element(2, Uri),
+ _pipe@1 = gleam@option:unwrap(_pipe, <<""/utf8>>),
+ gleam@http:scheme_from_string(_pipe@1)
+ end,
+ fun(Scheme) ->
+ gleam@result:then(
+ begin
+ _pipe@2 = erlang:element(4, Uri),
+ gleam@option:to_result(_pipe@2, nil)
+ end,
+ fun(Host) ->
+ Req = {request,
+ get,
+ [],
+ <<""/utf8>>,
+ Scheme,
+ Host,
+ erlang:element(5, Uri),
+ erlang:element(6, Uri),
+ erlang:element(7, Uri)},
+ {ok, Req}
+ end
+ )
+ end
+ ).
+
+-spec get_header(request(any()), binary()) -> {ok, binary()} | {error, nil}.
+get_header(Request, Key) ->
+ gleam@list:key_find(erlang:element(3, Request), gleam@string:lowercase(Key)).
+
+-spec set_header(request(FYT), binary(), binary()) -> request(FYT).
+set_header(Request, Key, Value) ->
+ Headers = gleam@list:key_set(
+ erlang:element(3, Request),
+ gleam@string:lowercase(Key),
+ Value
+ ),
+ erlang:setelement(3, Request, Headers).
+
+-spec prepend_header(request(FYW), binary(), binary()) -> request(FYW).
+prepend_header(Request, Key, Value) ->
+ Headers = [{gleam@string:lowercase(Key), Value} |
+ erlang:element(3, Request)],
+ erlang:setelement(3, Request, Headers).
+
+-spec set_body(request(any()), FZB) -> request(FZB).
+set_body(Req, Body) ->
+ {request, Method, Headers, _, Scheme, Host, Port, Path, Query} = Req,
+ {request, Method, Headers, Body, Scheme, Host, Port, Path, Query}.
+
+-spec map(request(FZD), fun((FZD) -> FZF)) -> request(FZF).
+map(Request, Transform) ->
+ _pipe = erlang:element(4, Request),
+ _pipe@1 = Transform(_pipe),
+ set_body(Request, _pipe@1).
+
+-spec path_segments(request(any())) -> list(binary()).
+path_segments(Request) ->
+ _pipe = erlang:element(8, Request),
+ gleam@uri:path_segments(_pipe).
+
+-spec get_query(request(any())) -> {ok, list({binary(), binary()})} |
+ {error, nil}.
+get_query(Request) ->
+ case erlang:element(9, Request) of
+ {some, Query_string} ->
+ gleam@uri:parse_query(Query_string);
+
+ none ->
+ {ok, []}
+ end.
+
+-spec set_query(request(FZP), list({binary(), binary()})) -> request(FZP).
+set_query(Req, Query) ->
+ Pair = fun(T) ->
+ gleam@string_builder:from_strings(
+ [erlang:element(1, T), <<"="/utf8>>, erlang:element(2, T)]
+ )
+ end,
+ Query@1 = begin
+ _pipe = Query,
+ _pipe@1 = gleam@list:map(_pipe, Pair),
+ _pipe@2 = gleam@list:intersperse(
+ _pipe@1,
+ gleam@string_builder:from_string(<<"&"/utf8>>)
+ ),
+ _pipe@3 = gleam@string_builder:concat(_pipe@2),
+ _pipe@4 = gleam@string_builder:to_string(_pipe@3),
+ {some, _pipe@4}
+ end,
+ erlang:setelement(9, Req, Query@1).
+
+-spec set_method(request(FZT), gleam@http:method()) -> request(FZT).
+set_method(Req, Method) ->
+ erlang:setelement(2, Req, Method).
+
+-spec new() -> request(binary()).
+new() ->
+ {request,
+ get,
+ [],
+ <<""/utf8>>,
+ https,
+ <<"localhost"/utf8>>,
+ none,
+ <<""/utf8>>,
+ none}.
+
+-spec to(binary()) -> {ok, request(binary())} | {error, nil}.
+to(Url) ->
+ _pipe = Url,
+ _pipe@1 = gleam@uri:parse(_pipe),
+ gleam@result:then(_pipe@1, fun from_uri/1).
+
+-spec set_scheme(request(GAA), gleam@http:scheme()) -> request(GAA).
+set_scheme(Req, Scheme) ->
+ erlang:setelement(5, Req, Scheme).
+
+-spec set_host(request(GAD), binary()) -> request(GAD).
+set_host(Req, Host) ->
+ erlang:setelement(6, Req, Host).
+
+-spec set_port(request(GAG), integer()) -> request(GAG).
+set_port(Req, Port) ->
+ erlang:setelement(7, Req, {some, Port}).
+
+-spec set_path(request(GAJ), binary()) -> request(GAJ).
+set_path(Req, Path) ->
+ erlang:setelement(8, Req, Path).
+
+-spec set_cookie(request(GAM), binary(), binary()) -> request(GAM).
+set_cookie(Req, Name, Value) ->
+ New_cookie_string = gleam@string:join([Name, Value], <<"="/utf8>>),
+ {Cookies_string@2, Headers@1} = case gleam@list:key_pop(
+ erlang:element(3, Req),
+ <<"cookie"/utf8>>
+ ) of
+ {ok, {Cookies_string, Headers}} ->
+ Cookies_string@1 = gleam@string:join(
+ [Cookies_string, New_cookie_string],
+ <<"; "/utf8>>
+ ),
+ {Cookies_string@1, Headers};
+
+ {error, nil} ->
+ {New_cookie_string, erlang:element(3, Req)}
+ end,
+ erlang:setelement(
+ 3,
+ Req,
+ [{<<"cookie"/utf8>>, Cookies_string@2} | Headers@1]
+ ).
+
+-spec get_cookies(request(any())) -> list({binary(), binary()}).
+get_cookies(Req) ->
+ {request, _, Headers, _, _, _, _, _, _} = Req,
+ _pipe = Headers,
+ _pipe@1 = gleam@list:filter_map(
+ _pipe,
+ fun(Header) ->
+ {Name, Value} = Header,
+ case Name of
+ <<"cookie"/utf8>> ->
+ {ok, gleam@http@cookie:parse(Value)};
+
+ _ ->
+ {error, nil}
+ end
+ end
+ ),
+ gleam@list:flatten(_pipe@1).
diff --git a/aoc2023/build/packages/gleam_http/src/gleam@http@response.erl b/aoc2023/build/packages/gleam_http/src/gleam@http@response.erl
new file mode 100644
index 0000000..b073c1d
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam@http@response.erl
@@ -0,0 +1,97 @@
+-module(gleam@http@response).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]).
+
+-export([new/1, get_header/2, set_header/3, prepend_header/3, set_body/2, try_map/2, map/2, redirect/1, get_cookies/1, set_cookie/4, expire_cookie/3]).
+-export_type([response/1]).
+
+-type response(GFN) :: {response, integer(), list({binary(), binary()}), GFN}.
+
+-spec new(integer()) -> response(binary()).
+new(Status) ->
+ {response, Status, [], <<""/utf8>>}.
+
+-spec get_header(response(any()), binary()) -> {ok, binary()} | {error, nil}.
+get_header(Response, Key) ->
+ gleam@list:key_find(
+ erlang:element(3, Response),
+ gleam@string:lowercase(Key)
+ ).
+
+-spec set_header(response(GGC), binary(), binary()) -> response(GGC).
+set_header(Response, Key, Value) ->
+ Headers = gleam@list:key_set(
+ erlang:element(3, Response),
+ gleam@string:lowercase(Key),
+ Value
+ ),
+ erlang:setelement(3, Response, Headers).
+
+-spec prepend_header(response(GGF), binary(), binary()) -> response(GGF).
+prepend_header(Response, Key, Value) ->
+ Headers = [{gleam@string:lowercase(Key), Value} |
+ erlang:element(3, Response)],
+ erlang:setelement(3, Response, Headers).
+
+-spec set_body(response(any()), GGK) -> response(GGK).
+set_body(Response, Body) ->
+ {response, Status, Headers, _} = Response,
+ {response, Status, Headers, Body}.
+
+-spec try_map(response(GFO), fun((GFO) -> {ok, GFQ} | {error, GFR})) -> {ok,
+ response(GFQ)} |
+ {error, GFR}.
+try_map(Response, Transform) ->
+ gleam@result:then(
+ Transform(erlang:element(4, Response)),
+ fun(Body) -> {ok, set_body(Response, Body)} end
+ ).
+
+-spec map(response(GGM), fun((GGM) -> GGO)) -> response(GGO).
+map(Response, Transform) ->
+ _pipe = erlang:element(4, Response),
+ _pipe@1 = Transform(_pipe),
+ set_body(Response, _pipe@1).
+
+-spec redirect(binary()) -> response(binary()).
+redirect(Uri) ->
+ {response,
+ 303,
+ [{<<"location"/utf8>>, Uri}],
+ gleam@string:append(<<"You are being redirected to "/utf8>>, Uri)}.
+
+-spec get_cookies(response(any())) -> list({binary(), binary()}).
+get_cookies(Resp) ->
+ {response, _, Headers, _} = Resp,
+ _pipe = Headers,
+ _pipe@1 = gleam@list:filter_map(
+ _pipe,
+ fun(Header) ->
+ {Name, Value} = Header,
+ case Name of
+ <<"set-cookie"/utf8>> ->
+ {ok, gleam@http@cookie:parse(Value)};
+
+ _ ->
+ {error, nil}
+ end
+ end
+ ),
+ gleam@list:flatten(_pipe@1).
+
+-spec set_cookie(
+ response(GGT),
+ binary(),
+ binary(),
+ gleam@http@cookie:attributes()
+) -> response(GGT).
+set_cookie(Response, Name, Value, Attributes) ->
+ prepend_header(
+ Response,
+ <<"set-cookie"/utf8>>,
+ gleam@http@cookie:set_header(Name, Value, Attributes)
+ ).
+
+-spec expire_cookie(response(GGW), binary(), gleam@http@cookie:attributes()) -> response(GGW).
+expire_cookie(Response, Name, Attributes) ->
+ Attrs = erlang:setelement(2, Attributes, {some, 0}),
+ set_cookie(Response, Name, <<""/utf8>>, Attrs).
diff --git a/aoc2023/build/packages/gleam_http/src/gleam@http@service.erl b/aoc2023/build/packages/gleam_http/src/gleam@http@service.erl
new file mode 100644
index 0000000..d07b31f
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam@http@service.erl
@@ -0,0 +1,82 @@
+-module(gleam@http@service).
+-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function]).
+
+-export([map_response_body/2, prepend_response_header/3, method_override/1]).
+
+-spec map_response_body(
+ fun((gleam@http@request:request(GJL)) -> gleam@http@response:response(GJM)),
+ fun((GJM) -> GJP)
+) -> fun((gleam@http@request:request(GJL)) -> gleam@http@response:response(GJP)).
+map_response_body(Service, Mapper) ->
+ fun(Req) -> _pipe = Req,
+ _pipe@1 = Service(_pipe),
+ gleam@http@response:map(_pipe@1, Mapper) end.
+
+-spec prepend_response_header(
+ fun((gleam@http@request:request(GJS)) -> gleam@http@response:response(GJT)),
+ binary(),
+ binary()
+) -> fun((gleam@http@request:request(GJS)) -> gleam@http@response:response(GJT)).
+prepend_response_header(Service, Key, Value) ->
+ fun(Req) -> _pipe = Req,
+ _pipe@1 = Service(_pipe),
+ gleam@http@response:prepend_header(_pipe@1, Key, Value) end.
+
+-spec ensure_post(gleam@http@request:request(GJY)) -> {ok,
+ gleam@http@request:request(GJY)} |
+ {error, nil}.
+ensure_post(Req) ->
+ case erlang:element(2, Req) of
+ post ->
+ {ok, Req};
+
+ _ ->
+ {error, nil}
+ end.
+
+-spec get_override_method(gleam@http@request:request(any())) -> {ok,
+ gleam@http:method()} |
+ {error, nil}.
+get_override_method(Request) ->
+ gleam@result:then(
+ gleam@http@request:get_query(Request),
+ fun(Query_params) ->
+ gleam@result:then(
+ gleam@list:key_find(Query_params, <<"_method"/utf8>>),
+ fun(Method) ->
+ gleam@result:then(
+ gleam@http:parse_method(Method),
+ fun(Method@1) -> case Method@1 of
+ put ->
+ {ok, Method@1};
+
+ patch ->
+ {ok, Method@1};
+
+ delete ->
+ {ok, Method@1};
+
+ _ ->
+ {error, nil}
+ end end
+ )
+ end
+ )
+ end
+ ).
+
+-spec method_override(
+ fun((gleam@http@request:request(GKF)) -> gleam@http@response:response(GKG))
+) -> fun((gleam@http@request:request(GKF)) -> gleam@http@response:response(GKG)).
+method_override(Service) ->
+ fun(Request) -> _pipe = Request,
+ _pipe@1 = ensure_post(_pipe),
+ _pipe@2 = gleam@result:then(_pipe@1, fun get_override_method/1),
+ _pipe@3 = gleam@result:map(
+ _pipe@2,
+ fun(_capture) ->
+ gleam@http@request:set_method(Request, _capture)
+ end
+ ),
+ _pipe@4 = gleam@result:unwrap(_pipe@3, Request),
+ Service(_pipe@4) end.
diff --git a/aoc2023/build/packages/gleam_http/src/gleam_http.app.src b/aoc2023/build/packages/gleam_http/src/gleam_http.app.src
new file mode 100644
index 0000000..c37ad54
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam_http.app.src
@@ -0,0 +1,12 @@
+{application, gleam_http, [
+ {vsn, "3.5.2"},
+ {applications, [gleam_stdlib,
+ gleeunit]},
+ {description, "Types and functions for Gleam HTTP clients and servers"},
+ {modules, [gleam@http,
+ gleam@http@cookie,
+ gleam@http@request,
+ gleam@http@response,
+ gleam@http@service]},
+ {registered, []}
+]}.
diff --git a/aoc2023/build/packages/gleam_http/src/gleam_http_native.erl b/aoc2023/build/packages/gleam_http/src/gleam_http_native.erl
new file mode 100644
index 0000000..bb499bb
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam_http_native.erl
@@ -0,0 +1,88 @@
+-module(gleam_http_native).
+-export([decode_method/1]).
+
+decode_method(Term) ->
+ case Term of
+ "connect" -> {ok, connect};
+ "delete" -> {ok, delete};
+ "get" -> {ok, get};
+ "head" -> {ok, head};
+ "options" -> {ok, options};
+ "patch" -> {ok, patch};
+ "post" -> {ok, post};
+ "put" -> {ok, put};
+ "trace" -> {ok, trace};
+ "CONNECT" -> {ok, connect};
+ "DELETE" -> {ok, delete};
+ "GET" -> {ok, get};
+ "HEAD" -> {ok, head};
+ "OPTIONS" -> {ok, options};
+ "PATCH" -> {ok, patch};
+ "POST" -> {ok, post};
+ "PUT" -> {ok, put};
+ "TRACE" -> {ok, trace};
+ "Connect" -> {ok, connect};
+ "Delete" -> {ok, delete};
+ "Get" -> {ok, get};
+ "Head" -> {ok, head};
+ "Options" -> {ok, options};
+ "Patch" -> {ok, patch};
+ "Post" -> {ok, post};
+ "Put" -> {ok, put};
+ "Trace" -> {ok, trace};
+ 'connect' -> {ok, connect};
+ 'delete' -> {ok, delete};
+ 'get' -> {ok, get};
+ 'head' -> {ok, head};
+ 'options' -> {ok, options};
+ 'patch' -> {ok, patch};
+ 'post' -> {ok, post};
+ 'put' -> {ok, put};
+ 'trace' -> {ok, trace};
+ 'CONNECT' -> {ok, connect};
+ 'DELETE' -> {ok, delete};
+ 'GET' -> {ok, get};
+ 'HEAD' -> {ok, head};
+ 'OPTIONS' -> {ok, options};
+ 'PATCH' -> {ok, patch};
+ 'POST' -> {ok, post};
+ 'PUT' -> {ok, put};
+ 'TRACE' -> {ok, trace};
+ 'Connect' -> {ok, connect};
+ 'Delete' -> {ok, delete};
+ 'Get' -> {ok, get};
+ 'Head' -> {ok, head};
+ 'Options' -> {ok, options};
+ 'Patch' -> {ok, patch};
+ 'Post' -> {ok, post};
+ 'Put' -> {ok, put};
+ 'Trace' -> {ok, trace};
+ <<"connect">> -> {ok, connect};
+ <<"delete">> -> {ok, delete};
+ <<"get">> -> {ok, get};
+ <<"head">> -> {ok, head};
+ <<"options">> -> {ok, options};
+ <<"patch">> -> {ok, patch};
+ <<"post">> -> {ok, post};
+ <<"put">> -> {ok, put};
+ <<"trace">> -> {ok, trace};
+ <<"CONNECT">> -> {ok, connect};
+ <<"DELETE">> -> {ok, delete};
+ <<"GET">> -> {ok, get};
+ <<"HEAD">> -> {ok, head};
+ <<"OPTIONS">> -> {ok, options};
+ <<"PATCH">> -> {ok, patch};
+ <<"POST">> -> {ok, post};
+ <<"PUT">> -> {ok, put};
+ <<"TRACE">> -> {ok, trace};
+ <<"Connect">> -> {ok, connect};
+ <<"Delete">> -> {ok, delete};
+ <<"Get">> -> {ok, get};
+ <<"Head">> -> {ok, head};
+ <<"Options">> -> {ok, options};
+ <<"Patch">> -> {ok, patch};
+ <<"Post">> -> {ok, post};
+ <<"Put">> -> {ok, put};
+ <<"Trace">> -> {ok, trace};
+ _ -> {error, nil}
+ end.
diff --git a/aoc2023/build/packages/gleam_http/src/gleam_http_native.mjs b/aoc2023/build/packages/gleam_http/src/gleam_http_native.mjs
new file mode 100644
index 0000000..c871a8b
--- /dev/null
+++ b/aoc2023/build/packages/gleam_http/src/gleam_http_native.mjs
@@ -0,0 +1,38 @@
+import { Ok, Error } from "./gleam.mjs";
+import {
+ Get,
+ Post,
+ Head,
+ Put,
+ Delete,
+ Trace,
+ Connect,
+ Options,
+ Patch,
+} from "./gleam/http.mjs";
+
+export function decode_method(value) {
+ try {
+ switch (value.toLowerCase()) {
+ case "get":
+ return new Ok(new Get());
+ case "post":
+ return new Ok(new Post());
+ case "head":
+ return new Ok(new Head());
+ case "put":
+ return new Ok(new Put());
+ case "delete":
+ return new Ok(new Delete());
+ case "trace":
+ return new Ok(new Trace());
+ case "connect":
+ return new Ok(new Connect());
+ case "options":
+ return new Ok(new Options());
+ case "patch":
+ return new Ok(new Patch());
+ }
+ } catch {}
+ return new Error(undefined);
+}