Packages
gleam_stdlib
0.67.0
1.0.3
1.0.2
1.0.1
1.0.0
0.71.0
0.70.0
0.69.0
0.68.1
0.68.0
0.67.1
0.67.0
0.65.0
0.64.0
0.63.2
0.63.1
0.63.0
0.62.1
0.62.0
0.61.0
0.60.0
0.59.0
0.58.0
0.57.0
0.56.0
0.55.0
0.54.0
0.53.0
0.52.0
0.51.0
0.50.0
0.49.0
0.48.0
0.47.0
0.46.0
0.45.0
0.44.0
0.43.0
0.42.0
0.41.0
0.40.0
0.39.0
0.38.0
0.37.0
0.36.0
0.35.1
0.35.0
0.34.0
0.33.1
0.33.0
0.32.1
0.32.0
0.31.0
0.30.2
0.30.1
0.30.0
0.29.2
0.29.1
0.29.0
0.28.2
0.28.1
0.28.0
0.27.0
0.26.1
0.26.0
0.25.0
0.24.0
0.23.0
0.22.3
0.22.2
0.22.1
0.22.0
0.21.0
0.20.0
0.19.3
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.18.0-rc1
0.17.1
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.1
0.10.0
0.9.0
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.4.0-rc1
0.3.1
0.3.0
0.2.0
retired
A standard library for the Gleam programming language
Current section
Files
Jump to
Current section
Files
src/gleam/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 gleam/int
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string
import gleam/string_tree.{type StringTree}
/// Type representing holding the parsed components of an 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
/// let uri = 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
/// 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 schemem, 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 urlencoded 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
/// 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
/// 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 {
string_tree.from_strings([percent_encode(pair.0), "=", percent_encode(pair.1)])
}
/// Encodes a string into a percent encoded representation.
///
/// ## Examples
///
/// ```gleam
/// 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
/// 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 it's 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
/// 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"))
/// 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")
/// 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], "/")
}