Packages
gleam_stdlib
0.12.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/list
import gleam/result
import gleam/option.{None, Option, Some}
import gleam/string
import gleam/dynamic.{Dynamic}
import gleam/map.{Map}
import gleam/function
import gleam/pair
/// 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),
)
}
pub external fn erl_parse(String) -> Dynamic =
"uri_string" "parse"
type UriKey {
Scheme
Userinfo
Host
Port
Path
Query
Fragment
}
/// 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`
///
pub fn parse(string: String) -> Result(Uri, Nil) {
try uri_map =
dynamic.map(erl_parse(string))
|> result.nil_error
let get = fn(k: UriKey, decode_type: dynamic.Decoder(t)) -> Option(t) {
uri_map
|> map.get(dynamic.from(k))
|> result.then(function.compose(decode_type, result.nil_error))
|> option.from_result
}
let uri =
Uri(
scheme: get(Scheme, dynamic.string),
userinfo: get(Userinfo, dynamic.string),
host: get(Host, dynamic.string),
port: get(Port, dynamic.int),
path: option.unwrap(get(Path, dynamic.string), ""),
query: get(Query, dynamic.string),
fragment: get(Fragment, dynamic.string),
)
Ok(uri)
}
external fn erl_parse_query(String) -> Dynamic =
"uri_string" "dissect_query"
/// 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`.
///
pub fn parse_query(query: String) -> Result(List(tuple(String, String)), Nil) {
let bool_value = fn(x) { result.map(dynamic.bool(x), fn(_) { "" }) }
let query_param = dynamic.typed_tuple2(
_,
dynamic.string,
dynamic.any(_, of: [dynamic.string, bool_value]),
)
query
|> erl_parse_query
|> dynamic.typed_list(of: query_param)
|> result.nil_error
}
type Encoding {
Utf8
}
type ErlQueryToStringOption {
Encoding(Encoding)
}
external fn erl_query_to_string(
List(tuple(String, String)),
List(ErlQueryToStringOption),
) -> Dynamic =
"uri_string" "compose_query"
/// Encode a list of key value pairs as a URI query string.
///
/// The opposite operation is `uri.parse_query`.
///
pub fn query_to_string(query: List(tuple(String, String))) -> String {
query
|> erl_query_to_string([Encoding(Utf8)])
|> dynamic.string
|> result.unwrap("")
}
/// Encode a string into a percent encoded representation.
/// Note that this encodes space as +.
///
/// ## Example
///
/// percent_encode("100% great")
/// > "100%25+great"
///
pub fn percent_encode(value: String) -> String {
query_to_string([tuple("k", value)])
|> string.replace(each: "k=", with: "")
}
/// Decode a percent encoded string.
///
/// ## Example
///
/// percent_decode("100%25+great")
/// > Ok("100% great")
///
pub fn percent_decode(value: String) -> Result(String, Nil) {
string.concat(["k=", value])
|> parse_query
|> result.then(list.head)
|> result.map(pair.second)
}
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, [])
}
/// Split 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.
///
pub fn path_segments(path: String) -> List(String) {
remove_dot_segments(string.split(path, "/"))
}
external fn erl_to_string(Map(UriKey, Dynamic)) -> Dynamic =
"uri_string" "recompose"
/// Encode a `Uri` value as a URI string.
///
/// The opposite operation is `uri.parse`.
///
pub fn to_string(uri: Uri) -> String {
let field = fn(key: UriKey, value: Option(anything)) -> Result(
tuple(UriKey, Dynamic),
Nil,
) {
case value {
Some(v) -> Ok(tuple(key, dynamic.from(v)))
None -> Error(Nil)
}
}
[
field(Scheme, uri.scheme),
field(Userinfo, uri.userinfo),
field(Host, uri.host),
field(Port, uri.port),
field(Path, option.Some(uri.path)),
field(Query, uri.query),
field(Fragment, uri.fragment),
]
|> list.filter_map(fn(x) { x })
|> map.from_list
|> erl_to_string
|> dynamic.string
|> result.unwrap("")
}
/// Fetch the origin of a uri
///
/// Return the origin of a uri as defined in
/// https://tools.ietf.org/html/rfc6454
///
/// The supported uri schemes are `http` and `https`
/// Urls without a scheme will return Error
pub fn origin(uri: Uri) -> Result(String, Nil) {
let Uri(scheme: scheme, host: host, port: port, ..) = uri
case scheme {
Some("https") | Some("http") -> {
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], "/")
}
/// Resolve 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,
relative.port,
path,
relative.query,
relative.fragment,
)
Ok(resolved)
}
Uri(scheme: None, host: None, ..) -> {
let tuple(new_path, new_query) = case relative.path {
"" -> tuple(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()
tuple(path, relative.query)
}
}
let resolved =
Uri(
base.scheme,
None,
base.host,
base.port,
new_path,
new_query,
relative.fragment,
)
Ok(resolved)
}
}
_ -> Error(Nil)
}
}