diff options
author | J.J <thechairman@thechairman.info> | 2024-05-30 21:50:02 -0400 |
---|---|---|
committer | J.J <thechairman@thechairman.info> | 2024-05-30 21:50:02 -0400 |
commit | 612fd986ab1e00b6d34dc1937136250e08e89325 (patch) | |
tree | a3c93952040c6afdf348b5831619a45db7ba0a2e /aoc2023/build/packages/gleam_http/src | |
parent | 231c2b688d1e6cf0846d46e883da30e042a9c6cf (diff) | |
download | gleam_aoc-612fd986ab1e00b6d34dc1937136250e08e89325.tar.gz gleam_aoc-612fd986ab1e00b6d34dc1937136250e08e89325.zip |
cleanup
Diffstat (limited to 'aoc2023/build/packages/gleam_http/src')
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); +} |