src/gleeps/stdlib/uri.gleam

//// Utilities for working with URIs
////
//// This module provides functions for working with URIs (for example, parsing
//// URIs or encoding query strings). The functions in this module are implemented
//// according to [RFC 3986](https://tools.ietf.org/html/rfc3986).
////
//// Query encoding (Form encoding) is defined in the
//// [W3C specification](https://www.w3.org/TR/html52/sec-forms.html#urlencoded-form-data).

import gleeps/stdlib/int
import gleeps/stdlib/list
import gleeps/stdlib/option.{type Option, None, Some}
import gleeps/stdlib/string
import gleeps/stdlib/string_tree.{type StringTree}

/// Type representing the parsed components of a URI.
/// All components of a URI are optional, except the path.
///
pub type Uri {
  Uri(
    scheme: Option(String),
    userinfo: Option(String),
    host: Option(String),
    port: Option(Int),
    path: String,
    query: Option(String),
    fragment: Option(String),
  )
}

/// Constant representing an empty URI, equivalent to "".
///
/// ## Examples
///
/// ```gleam
/// assert Uri(..empty, scheme: Some("https"), host: Some("example.com"))
///   == Uri(
///     scheme: Some("https"),
///     userinfo: None,
///     host: Some("example.com"),
///     port: None,
///     path: "",
///     query: None,
///     fragment: None,
///   )
/// ```
///
pub const empty = Uri(
  scheme: None,
  userinfo: None,
  host: None,
  port: None,
  path: "",
  query: None,
  fragment: None,
)

/// Parses a compliant URI string into the `Uri` type.
/// If the string is not a valid URI string then an error is returned.
///
/// The opposite operation is `uri.to_string`.
///
/// ## Examples
///
/// ```gleam
/// assert parse("https://example.com:1234/a/b?query=true#fragment")
///   == Ok(
///     Uri(
///       scheme: Some("https"),
///       userinfo: None,
///       host: Some("example.com"),
///       port: Some(1234),
///       path: "/a/b",
///       query: Some("query=true"),
///       fragment: Some("fragment")
///     )
///   )
/// ```
///
@external(erlang, "gleam_stdlib", "uri_parse")
pub fn parse(uri_string: String) -> Result(Uri, Nil) {
  // This parses a uri_string following the regex defined in
  // https://tools.ietf.org/html/rfc3986#appendix-B
  //
  // TODO: This is not perfect and will be more permissive than its Erlang
  // counterpart, ideally we want to replicate Erlang's implementation on the js
  // target as well.
  parse_scheme_loop(uri_string, uri_string, empty, 0)
}

fn parse_scheme_loop(
  original: String,
  uri_string: String,
  pieces: Uri,
  size: Int,
) -> Result(Uri, Nil) {
  case uri_string {
    // `/` is not allowed to appear in a scheme so we know it's over and we can
    // start parsing the authority with slashes.
    "/" <> _ if size == 0 -> parse_authority_with_slashes(uri_string, pieces)
    "/" <> _ -> {
      let scheme = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, scheme: Some(string.lowercase(scheme)))
      parse_authority_with_slashes(uri_string, pieces)
    }

    // `?` is not allowed to appear in a scheme, in an authority, or in a path;
    // so if we see it we know it marks the beginning of the query part.
    "?" <> rest if size == 0 -> parse_query_with_question_mark(rest, pieces)
    "?" <> rest -> {
      let scheme = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, scheme: Some(string.lowercase(scheme)))
      parse_query_with_question_mark(rest, pieces)
    }

    // `#` is not allowed to appear in a scheme, in an authority, in a path or
    // in a query; so if we see it we know it marks the beginning of the final
    // fragment.
    "#" <> rest if size == 0 -> parse_fragment(rest, pieces)
    "#" <> rest -> {
      let scheme = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, scheme: Some(string.lowercase(scheme)))
      parse_fragment(rest, pieces)
    }

    // A colon marks the end of a uri scheme, but if it is not preceded by any
    // character then it's not a valid URI.
    ":" <> _ if size == 0 -> Error(Nil)
    ":" <> rest -> {
      let scheme = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, scheme: Some(string.lowercase(scheme)))
      parse_authority_with_slashes(rest, pieces)
    }

    // If we could get to the end of the string and we've met no special
    // chars whatsoever, that means the entire string is just a long path.
    "" -> Ok(Uri(..pieces, path: original))

    // In all other cases the first character is just a valid URI scheme
    // character and we just keep munching characters until we reach the end of
    // the uri scheme (or the end of the string and that would mean this is not
    // a valid uri scheme since we found no `:`).
    _ -> {
      let #(_, rest) = pop_codeunit(uri_string)
      parse_scheme_loop(original, rest, pieces, size + 1)
    }
  }
}

fn parse_authority_with_slashes(
  uri_string: String,
  pieces: Uri,
) -> Result(Uri, Nil) {
  case uri_string {
    // To be a valid authority the string must start with a `//`, otherwise
    // there's no authority and we just skip ahead to parsing the path.
    "//" -> Ok(Uri(..pieces, host: Some("")))
    "//" <> rest -> parse_authority_pieces(rest, pieces)
    _ -> parse_path(uri_string, pieces)
  }
}

fn parse_authority_pieces(string: String, pieces: Uri) -> Result(Uri, Nil) {
  parse_userinfo_loop(string, string, pieces, 0)
}

fn parse_userinfo_loop(
  original: String,
  uri_string: String,
  pieces: Uri,
  size: Int,
) -> Result(Uri, Nil) {
  case uri_string {
    // `@` marks the end of the userinfo and the start of the host part in the
    // authority string.
    "@" <> rest if size == 0 -> parse_host(rest, pieces)
    "@" <> rest -> {
      let userinfo = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, userinfo: Some(userinfo))
      parse_host(rest, pieces)
    }

    // If we reach the end of the authority string without finding an `@`
    // special character, then we know that the authority doesn't actually
    // contain the userinfo part.
    // The entire string we just went through was a host! So we parse it as
    // such.
    "" | "/" <> _ | "?" <> _ | "#" <> _ -> parse_host(original, pieces)

    // In all other cases we just keep munching characters increasing the size
    // of the userinfo bit.
    _ -> {
      let #(_, rest) = pop_codeunit(uri_string)
      parse_userinfo_loop(original, rest, pieces, size + 1)
    }
  }
}

fn parse_host(uri_string: String, pieces: Uri) -> Result(Uri, Nil) {
  // A host string can be in two formats:
  // - \[[:.a-zA-Z0-9]*\]
  // - [^:]
  case uri_string {
    // If we find an opening bracket we know it's the first format.
    "[" <> _ -> parse_host_within_brackets(uri_string, pieces)

    // A `:` marks the beginning of the port part of the authority string.
    ":" <> _ -> {
      let pieces = Uri(..pieces, host: Some(""))
      parse_port(uri_string, pieces)
    }

    // If the string is empty then there's no need to keep going. The host is
    // empty.
    "" -> Ok(Uri(..pieces, host: Some("")))

    // Otherwise it's the second format
    _ -> parse_host_outside_of_brackets(uri_string, pieces)
  }
}

fn parse_host_within_brackets(
  uri_string: String,
  pieces: Uri,
) -> Result(Uri, Nil) {
  parse_host_within_brackets_loop(uri_string, uri_string, pieces, 0)
}

fn parse_host_within_brackets_loop(
  original: String,
  uri_string: String,
  pieces: Uri,
  size: Int,
) -> Result(Uri, Nil) {
  case uri_string {
    // If the string is over the entire string we were iterating through is the
    // host part.
    "" -> Ok(Uri(..pieces, host: Some(uri_string)))

    // A `]` marks the end of the host and the start of the port part.
    "]" <> rest if size == 0 -> parse_port(rest, pieces)
    "]" <> rest -> {
      let host = codeunit_slice(original, at_index: 0, length: size + 1)
      let pieces = Uri(..pieces, host: Some(host))
      parse_port(rest, pieces)
    }

    // `/` marks the beginning of a path.
    "/" <> _ if size == 0 -> parse_path(uri_string, pieces)
    "/" <> _ -> {
      let host = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, host: Some(host))
      parse_path(uri_string, pieces)
    }

    // `?` marks the beginning of the query with question mark.
    "?" <> rest if size == 0 -> parse_query_with_question_mark(rest, pieces)
    "?" <> rest -> {
      let host = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, host: Some(host))
      parse_query_with_question_mark(rest, pieces)
    }

    // `#` marks the beginning of the fragment part.
    "#" <> rest if size == 0 -> parse_fragment(rest, pieces)
    "#" <> rest -> {
      let host = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, host: Some(host))
      parse_fragment(rest, pieces)
    }

    // In all other cases we just keep iterating.
    _ -> {
      let #(char, rest) = pop_codeunit(uri_string)
      // Inside `[...]` there can only be some characters, if we find a special
      // one then we know that we're actually parsing the other format for the
      // host and we switch to that!
      case is_valid_host_within_brackets_char(char) {
        True ->
          parse_host_within_brackets_loop(original, rest, pieces, size + 1)

        False ->
          parse_host_outside_of_brackets_loop(original, original, pieces, 0)
      }
    }
  }
}

fn is_valid_host_within_brackets_char(char: Int) -> Bool {
  // [0-9]
  { 48 >= char && char <= 57 }
  // [A-Z]
  || { 65 >= char && char <= 90 }
  // [a-z]
  || { 97 >= char && char <= 122 }
  // :
  || char == 58
  // .
  || char == 46
}

fn parse_host_outside_of_brackets(
  uri_string: String,
  pieces: Uri,
) -> Result(Uri, Nil) {
  parse_host_outside_of_brackets_loop(uri_string, uri_string, pieces, 0)
}

fn parse_host_outside_of_brackets_loop(
  original: String,
  uri_string: String,
  pieces: Uri,
  size: Int,
) -> Result(Uri, Nil) {
  case uri_string {
    "" -> Ok(Uri(..pieces, host: Some(original)))

    // `:` marks the beginning of the port.
    ":" <> _ -> {
      let host = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, host: Some(host))
      parse_port(uri_string, pieces)
    }

    // `/` marks the beginning of a path.
    "/" <> _ -> {
      let host = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, host: Some(host))
      parse_path(uri_string, pieces)
    }

    // `?` marks the beginning of the query with question mark.
    "?" <> rest -> {
      let host = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, host: Some(host))
      parse_query_with_question_mark(rest, pieces)
    }

    // `#` marks the beginning of the fragment part.
    "#" <> rest -> {
      let host = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, host: Some(host))
      parse_fragment(rest, pieces)
    }

    _ -> {
      let #(_, rest) = pop_codeunit(uri_string)
      parse_host_outside_of_brackets_loop(original, rest, pieces, size + 1)
    }
  }
}

fn parse_port(uri_string: String, pieces: Uri) -> Result(Uri, Nil) {
  case uri_string {
    ":0" <> rest -> parse_port_loop(rest, pieces, 0)
    ":1" <> rest -> parse_port_loop(rest, pieces, 1)
    ":2" <> rest -> parse_port_loop(rest, pieces, 2)
    ":3" <> rest -> parse_port_loop(rest, pieces, 3)
    ":4" <> rest -> parse_port_loop(rest, pieces, 4)
    ":5" <> rest -> parse_port_loop(rest, pieces, 5)
    ":6" <> rest -> parse_port_loop(rest, pieces, 6)
    ":7" <> rest -> parse_port_loop(rest, pieces, 7)
    ":8" <> rest -> parse_port_loop(rest, pieces, 8)
    ":9" <> rest -> parse_port_loop(rest, pieces, 9)

    // The port could be empty and be followed by any of the next delimiters.
    // Like `:#`, `:?` or `:/`
    ":" | "" -> Ok(pieces)

    // `?` marks the beginning of the query with question mark.
    "?" <> rest | ":?" <> rest -> parse_query_with_question_mark(rest, pieces)

    // `#` marks the beginning of the fragment part.
    "#" <> rest | ":#" <> rest -> parse_fragment(rest, pieces)

    // `/` marks the beginning of a path.
    "/" <> _ -> parse_path(uri_string, pieces)
    ":" <> rest ->
      case rest {
        "/" <> _ -> parse_path(rest, pieces)
        _ -> Error(Nil)
      }

    _ -> Error(Nil)
  }
}

fn parse_port_loop(
  uri_string: String,
  pieces: Uri,
  port: Int,
) -> Result(Uri, Nil) {
  case uri_string {
    // As long as we find port numbers we keep accumulating those.
    "0" <> rest -> parse_port_loop(rest, pieces, port * 10)
    "1" <> rest -> parse_port_loop(rest, pieces, port * 10 + 1)
    "2" <> rest -> parse_port_loop(rest, pieces, port * 10 + 2)
    "3" <> rest -> parse_port_loop(rest, pieces, port * 10 + 3)
    "4" <> rest -> parse_port_loop(rest, pieces, port * 10 + 4)
    "5" <> rest -> parse_port_loop(rest, pieces, port * 10 + 5)
    "6" <> rest -> parse_port_loop(rest, pieces, port * 10 + 6)
    "7" <> rest -> parse_port_loop(rest, pieces, port * 10 + 7)
    "8" <> rest -> parse_port_loop(rest, pieces, port * 10 + 8)
    "9" <> rest -> parse_port_loop(rest, pieces, port * 10 + 9)

    // `?` marks the beginning of the query with question mark.
    "?" <> rest -> {
      let pieces = Uri(..pieces, port: Some(port))
      parse_query_with_question_mark(rest, pieces)
    }

    // `#` marks the beginning of the fragment part.
    "#" <> rest -> {
      let pieces = Uri(..pieces, port: Some(port))
      parse_fragment(rest, pieces)
    }

    // `/` marks the beginning of a path.
    "/" <> _ -> {
      let pieces = Uri(..pieces, port: Some(port))
      parse_path(uri_string, pieces)
    }

    // The string (and so the port) is over, we return what we parsed so far.
    "" -> Ok(Uri(..pieces, port: Some(port)))

    // In all other cases we've ran into some invalid character inside the port
    // so the uri is invalid!
    _ -> Error(Nil)
  }
}

fn parse_path(uri_string: String, pieces: Uri) -> Result(Uri, Nil) {
  parse_path_loop(uri_string, uri_string, pieces, 0)
}

fn parse_path_loop(
  original: String,
  uri_string: String,
  pieces: Uri,
  size: Int,
) -> Result(Uri, Nil) {
  case uri_string {
    // `?` marks the beginning of the query with question mark.
    "?" <> rest -> {
      let path = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, path: path)
      parse_query_with_question_mark(rest, pieces)
    }

    // `#` marks the beginning of the fragment part.
    "#" <> rest -> {
      let path = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, path: path)
      parse_fragment(rest, pieces)
    }

    // If the string is over that means the entirety of the string was the path
    // and it has an empty query and fragment.
    "" -> Ok(Uri(..pieces, path: original))

    // In all other cases the character is allowed to be part of the path so we
    // just keep munching until we reach to its end.
    _ -> {
      let #(_, rest) = pop_codeunit(uri_string)
      parse_path_loop(original, rest, pieces, size + 1)
    }
  }
}

fn parse_query_with_question_mark(
  uri_string: String,
  pieces: Uri,
) -> Result(Uri, Nil) {
  parse_query_with_question_mark_loop(uri_string, uri_string, pieces, 0)
}

fn parse_query_with_question_mark_loop(
  original: String,
  uri_string: String,
  pieces: Uri,
  size: Int,
) -> Result(Uri, Nil) {
  case uri_string {
    // `#` marks the beginning of the fragment part.
    "#" <> rest if size == 0 -> parse_fragment(rest, pieces)
    "#" <> rest -> {
      let query = codeunit_slice(original, at_index: 0, length: size)
      let pieces = Uri(..pieces, query: Some(query))
      parse_fragment(rest, pieces)
    }

    // If the string is over that means the entirety of the string was the query
    // and it has an empty fragment.
    "" -> Ok(Uri(..pieces, query: Some(original)))

    // In all other cases the character is allowed to be part of the query so we
    // just keep munching until we reach to its end.
    _ -> {
      let #(_, rest) = pop_codeunit(uri_string)
      parse_query_with_question_mark_loop(original, rest, pieces, size + 1)
    }
  }
}

fn parse_fragment(rest: String, pieces: Uri) -> Result(Uri, Nil) {
  Ok(Uri(..pieces, fragment: Some(rest)))
}

// WARN: this function returns invalid strings!
// We need to return a String anyways to have this as the representation on the
// JavaScript target.
// Alternatively, we could rewrite the entire code to use a single
// `fold_codeunits`-style loop and a state machine.
@external(erlang, "gleam_stdlib", "string_pop_codeunit")
@external(javascript, "../gleam_stdlib.mjs", "pop_codeunit")
fn pop_codeunit(str: String) -> #(Int, String)

@external(erlang, "binary", "part")
@external(javascript, "../gleam_stdlib.mjs", "string_codeunit_slice")
fn codeunit_slice(str: String, at_index from: Int, length length: Int) -> String

/// Parses an URL-encoded query string into a list of key value pairs.
/// Returns an error for invalid encoding.
///
/// The opposite operation is `uri.query_to_string`.
///
/// ## Examples
///
/// ```gleam
/// assert parse_query("a=1&b=2") == Ok([#("a", "1"), #("b", "2")])
/// ```
///
@external(erlang, "gleam_stdlib", "parse_query")
@external(javascript, "../gleam_stdlib.mjs", "parse_query")
pub fn parse_query(query: String) -> Result(List(#(String, String)), Nil)

/// Encodes a list of key value pairs as a URI query string.
///
/// The opposite operation is `uri.parse_query`.
///
/// ## Examples
///
/// ```gleam
/// assert query_to_string([#("a", "1"), #("b", "2")]) == "a=1&b=2"
/// ```
///
pub fn query_to_string(query: List(#(String, String))) -> String {
  query
  |> list.map(query_pair)
  |> list.intersperse(string_tree.from_string("&"))
  |> string_tree.concat
  |> string_tree.to_string
}

fn query_pair(pair: #(String, String)) -> StringTree {
  [percent_encode_query(pair.0), "=", percent_encode_query(pair.1)]
  |> string_tree.from_strings
}

fn percent_encode_query(part: String) -> String {
  percent_encode(part)
  |> string.replace(each: "+", with: "%2B")
}

/// Encodes a string into a percent encoded representation.
///
/// ## Examples
///
/// ```gleam
/// assert percent_encode("100% great") == "100%25%20great"
/// ```
///
@external(erlang, "gleam_stdlib", "percent_encode")
@external(javascript, "../gleam_stdlib.mjs", "percent_encode")
pub fn percent_encode(value: String) -> String

/// Decodes a percent encoded string.
///
/// ## Examples
///
/// ```gleam
/// assert percent_decode("100%25%20great+fun") == Ok("100% great+fun")
/// ```
///
@external(erlang, "gleam_stdlib", "percent_decode")
@external(javascript, "../gleam_stdlib.mjs", "percent_decode")
pub fn percent_decode(value: String) -> Result(String, Nil)

/// Splits the path section of a URI into its constituent segments.
///
/// Removes empty segments and resolves dot-segments as specified in
/// [section 5.2](https://www.ietf.org/rfc/rfc3986.html#section-5.2) of the RFC.
///
/// ## Examples
///
/// ```gleam
/// assert path_segments("/users/1") == ["users" ,"1"]
/// ```
///
pub fn path_segments(path: String) -> List(String) {
  remove_dot_segments(string.split(path, "/"))
}

fn remove_dot_segments(input: List(String)) -> List(String) {
  remove_dot_segments_loop(input, [])
}

fn remove_dot_segments_loop(
  input: List(String),
  accumulator: List(String),
) -> List(String) {
  case input {
    [] -> list.reverse(accumulator)
    [segment, ..rest] -> {
      let accumulator = case segment, accumulator {
        "", accumulator -> accumulator
        ".", accumulator -> accumulator
        "..", [] -> []
        "..", [_, ..accumulator] -> accumulator
        segment, accumulator -> [segment, ..accumulator]
      }
      remove_dot_segments_loop(rest, accumulator)
    }
  }
}

/// Encodes a `Uri` value as a URI string.
///
/// The opposite operation is `uri.parse`.
///
/// ## Examples
///
/// ```gleam
/// let uri = Uri(..empty, scheme: Some("https"), host: Some("example.com"))
/// assert to_string(uri) == "https://example.com"
/// ```
///
pub fn to_string(uri: Uri) -> String {
  let parts = case uri.fragment {
    Some(fragment) -> ["#", fragment]
    None -> []
  }
  let parts = case uri.query {
    Some(query) -> ["?", query, ..parts]
    None -> parts
  }
  let parts = [uri.path, ..parts]
  let parts = case uri.host, string.starts_with(uri.path, "/") {
    Some(host), False if host != "" -> ["/", ..parts]
    _, _ -> parts
  }
  let parts = case uri.host, uri.port {
    Some(_), Some(port) -> [":", int.to_string(port), ..parts]
    _, _ -> parts
  }
  let parts = case uri.scheme, uri.userinfo, uri.host {
    Some(s), Some(u), Some(h) -> [s, "://", u, "@", h, ..parts]
    Some(s), None, Some(h) -> [s, "://", h, ..parts]
    Some(s), Some(_), None | Some(s), None, None -> [s, ":", ..parts]
    None, None, Some(h) -> ["//", h, ..parts]
    _, _, _ -> parts
  }
  string.concat(parts)
}

/// Fetches the origin of a URI.
///
/// Returns the origin of a uri as defined in
/// [RFC 6454](https://tools.ietf.org/html/rfc6454)
///
/// The supported URI schemes are `http` and `https`.
/// URLs without a scheme will return `Error`.
///
/// ## Examples
///
/// ```gleam
/// let assert Ok(uri) = parse("https://example.com/path?foo#bar")
/// assert origin(uri) == Ok("https://example.com")
/// ```
///
pub fn origin(uri: Uri) -> Result(String, Nil) {
  let Uri(scheme: scheme, host: host, port: port, ..) = uri
  case host, scheme {
    Some(h), Some("https") if port == Some(443) ->
      Ok(string.concat(["https://", h]))
    Some(h), Some("http") if port == Some(80) ->
      Ok(string.concat(["http://", h]))
    Some(h), Some(s) if s == "http" || s == "https" -> {
      case port {
        Some(p) -> Ok(string.concat([s, "://", h, ":", int.to_string(p)]))
        None -> Ok(string.concat([s, "://", h]))
      }
    }
    _, _ -> Error(Nil)
  }
}

/// Resolves a URI with respect to the given base URI.
///
/// The base URI must be an absolute URI or this function will return an error.
/// The algorithm for merging URIs is described in
/// [RFC 3986](https://tools.ietf.org/html/rfc3986#section-5.2).
///
pub fn merge(base: Uri, relative: Uri) -> Result(Uri, Nil) {
  case base {
    Uri(scheme: Some(_), host: Some(_), ..) ->
      case relative {
        Uri(host: Some(_), ..) -> {
          let path =
            relative.path
            |> string.split("/")
            |> remove_dot_segments()
            |> join_segments()
          let resolved =
            Uri(
              option.or(relative.scheme, base.scheme),
              None,
              relative.host,
              option.or(relative.port, base.port),
              path,
              relative.query,
              relative.fragment,
            )
          Ok(resolved)
        }
        _ -> {
          let #(new_path, new_query) = case relative.path {
            "" -> #(base.path, option.or(relative.query, base.query))
            _ -> {
              let path_segments = case string.starts_with(relative.path, "/") {
                True -> string.split(relative.path, "/")
                False ->
                  base.path
                  |> string.split("/")
                  |> drop_last()
                  |> list.append(string.split(relative.path, "/"))
              }
              let path =
                path_segments
                |> remove_dot_segments()
                |> join_segments()
              #(path, relative.query)
            }
          }
          let resolved =
            Uri(
              base.scheme,
              None,
              base.host,
              base.port,
              new_path,
              new_query,
              relative.fragment,
            )
          Ok(resolved)
        }
      }
    _ -> Error(Nil)
  }
}

fn drop_last(elements: List(a)) -> List(a) {
  list.take(from: elements, up_to: list.length(elements) - 1)
}

fn join_segments(segments: List(String)) -> String {
  string.join(["", ..segments], "/")
}