Packages
gleam_stdlib
0.30.2
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.{None, Option, Some}
import gleam/string
import gleam/string_builder.{StringBuilder}
@target(javascript)
import gleam/pair
@target(javascript)
import gleam/regex
@target(javascript)
import gleam/result
/// 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),
)
}
/// 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")
/// )
/// )
/// ```
///
pub fn parse(uri_string: String) -> Result(Uri, Nil) {
do_parse(uri_string)
}
@target(erlang)
@external(erlang, "gleam_stdlib", "uri_parse")
fn do_parse(a: String) -> Result(Uri, Nil)
@target(javascript)
fn do_parse(uri_string: String) -> Result(Uri, Nil) {
// From https://tools.ietf.org/html/rfc3986#appendix-B
let pattern =
// 12 3 4 5 6 7 8
"^(([a-z][a-z0-9\\+\\-\\.]*):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#.*)?"
let matches =
pattern
|> regex_submatches(uri_string)
|> pad_list(8)
let #(scheme, authority, path, query, fragment) = case matches {
[
_scheme_with_colon,
scheme,
authority_with_slashes,
_authority,
path,
query_with_question_mark,
_query,
fragment,
] -> #(
scheme,
authority_with_slashes,
path,
query_with_question_mark,
fragment,
)
_ -> #(None, None, None, None, None)
}
let scheme = noneify_empty_string(scheme)
let path = option.unwrap(path, "")
let query = noneify_query(query)
let #(userinfo, host, port) = split_authority(authority)
let fragment =
fragment
|> option.to_result(Nil)
|> result.try(string.pop_grapheme)
|> result.map(pair.second)
|> option.from_result
let scheme =
scheme
|> noneify_empty_string
|> option.map(string.lowercase)
Ok(Uri(
scheme: scheme,
userinfo: userinfo,
host: host,
port: port,
path: path,
query: query,
fragment: fragment,
))
}
@target(javascript)
fn regex_submatches(pattern: String, string: String) -> List(Option(String)) {
pattern
|> regex.compile(regex.Options(case_insensitive: True, multi_line: False))
|> result.nil_error
|> result.map(regex.scan(_, string))
|> result.try(list.first)
|> result.map(fn(m: regex.Match) { m.submatches })
|> result.unwrap([])
}
@target(javascript)
fn noneify_query(x: Option(String)) -> Option(String) {
case x {
None -> None
Some(x) ->
case string.pop_grapheme(x) {
Ok(#("?", query)) -> Some(query)
_ -> None
}
}
}
@target(javascript)
fn noneify_empty_string(x: Option(String)) -> Option(String) {
case x {
Some("") | None -> None
Some(_) -> x
}
}
// Split an authority into its userinfo, host and port parts.
@target(javascript)
fn split_authority(
authority: Option(String),
) -> #(Option(String), Option(String), Option(Int)) {
case option.unwrap(authority, "") {
"" -> #(None, None, None)
"//" -> #(None, Some(""), None)
authority -> {
let matches =
"^(//)?((.*)@)?(\\[[a-zA-Z0-9:.]*\\]|[^:]*)(:(\\d*))?"
|> regex_submatches(authority)
|> pad_list(6)
case matches {
[_, _, userinfo, host, _, port] -> {
let userinfo = noneify_empty_string(userinfo)
let host = noneify_empty_string(host)
let port =
port
|> option.unwrap("")
|> int.parse
|> option.from_result
#(userinfo, host, port)
}
_ -> #(None, None, None)
}
}
}
}
@target(javascript)
fn pad_list(list: List(Option(a)), size: Int) -> List(Option(a)) {
list
|> list.append(list.repeat(None, extra_required(list, size)))
}
@target(javascript)
fn extra_required(list: List(a), remaining: Int) -> Int {
case list {
_ if remaining == 0 -> 0
[] -> remaining
[_, ..xs] -> extra_required(xs, remaining - 1)
}
}
/// 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")])
/// ```
///
pub fn parse_query(query: String) -> Result(List(#(String, String)), Nil) {
do_parse_query(query)
}
@external(erlang, "gleam_stdlib", "parse_query")
@external(javascript, "../gleam_stdlib.mjs", "parse_query")
fn do_parse_query(a: 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_builder.from_string("&"))
|> string_builder.concat
|> string_builder.to_string
}
fn query_pair(pair: #(String, String)) -> StringBuilder {
string_builder.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"
/// ```
///
pub fn percent_encode(value: String) -> String {
do_percent_encode(value)
}
@external(erlang, "gleam_stdlib", "percent_encode")
@external(javascript, "../gleam_stdlib.mjs", "percent_encode")
fn do_percent_encode(a: String) -> String
/// Decodes a percent encoded string.
///
/// ## Examples
///
/// ```gleam
/// > percent_decode("100%25+great")
/// Ok("100% great")
/// ```
///
pub fn percent_decode(value: String) -> Result(String, Nil) {
do_percent_decode(value)
}
@external(erlang, "gleam_stdlib", "percent_decode")
@external(javascript, "../gleam_stdlib.mjs", "percent_decode")
fn do_percent_decode(a: String) -> Result(String, Nil)
fn do_remove_dot_segments(
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]
}
do_remove_dot_segments(rest, accumulator)
}
}
}
fn remove_dot_segments(input: List(String)) -> List(String) {
do_remove_dot_segments(input, [])
}
/// 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, "/"))
}
/// Encodes a `Uri` value as a URI string.
///
/// The opposite operation is `uri.parse`.
///
/// ## Examples
///
/// ```gleam
/// > let uri = Uri(Some("http"), None, Some("example.com"), ...)
/// > to_string(uri)
/// "http://example.com"
/// ```
///
pub fn to_string(uri: Uri) -> String {
let parts = case uri.fragment {
Some(fragment) -> ["#", fragment]
_ -> []
}
let parts = case uri.query {
Some(query) -> ["?", query, ..parts]
_ -> 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]
None, Some(_), None | None, None, None -> 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("http://example.com/path?foo#bar")
/// > origin(uri)
/// Ok("http://example.com")
/// ```
///
pub fn origin(uri: Uri) -> Result(String, Nil) {
let Uri(scheme: scheme, host: host, port: port, ..) = uri
case scheme {
Some("https") if port == Some(443) -> {
let origin = Uri(scheme, None, host, None, "", None, None)
Ok(to_string(origin))
}
Some("http") if port == Some(80) -> {
let origin = Uri(scheme, None, host, None, "", None, None)
Ok(to_string(origin))
}
Some(s) if s == "http" || s == "https" -> {
let origin = Uri(scheme, None, host, port, "", None, None)
Ok(to_string(origin))
}
_ -> 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], "/")
}
/// 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 =
string.split(relative.path, "/")
|> 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)
}
Uri(scheme: None, host: None, ..) -> {
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 ->
string.split(base.path, "/")
|> 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)
}
}