diff --git a/README.md b/README.md index c4a2d34..c07a326 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,10 @@ gleam add uri@1 ```gleam import uri -pub fn main() -> Nil { - // TODO: An example of the project in use +pub fn main() { + let uri = uri.parse("http://example.com:8080/path?q=1") + |> result.unwrap(types.empty_uri) + uri.normalise(uri) |> uri.to_string |> echo } ``` diff --git a/gleam.toml b/gleam.toml index afba55e..56bfda3 100644 --- a/gleam.toml +++ b/gleam.toml @@ -11,6 +11,7 @@ version = "1.0.0" # # For a full reference of all the available options, you can have a look at # https://gleam.run/writing-gleam/gleam-toml/. +# [dependencies] gleam_stdlib = ">= 0.44.0 and < 2.0.0" diff --git a/src/types.gleam b/src/types.gleam index 364b251..9336238 100644 --- a/src/types.gleam +++ b/src/types.gleam @@ -12,4 +12,5 @@ pub type Uri { ) } +/// Blank Uri value pub const empty_uri = Uri(None, None, None, None, "", None, None) diff --git a/src/uri.gleam b/src/uri.gleam index 8ce4558..ffd9ec5 100644 --- a/src/uri.gleam +++ b/src/uri.gleam @@ -4,14 +4,53 @@ import gleam/list import gleam/option.{Some} import gleam/string import gleam/uri -import internal/parser -import internal/utils import types.{type Uri, Uri} +import uri/internal/parser +import uri/internal/utils +/// Parses a string to the RFC3986 standard. +/// `Error` is returned if it fails parsing. +/// +/// ## Examples +/// +/// ```gleam +/// parse("https://me@host.com:9999/path?q=1#fragment") +/// // -> Ok( +/// // Uri( +/// // scheme: Some("https"), +/// // userinfo: Some("me"), +/// // host: Some("host.com"), +/// // port: Some(9999), +/// // path: "/path", +/// // query: Some("q=1"), +/// // fragment: Some("fragment") +/// // ) +/// // ) +/// ``` +/// pub fn parse(uri: String) -> Result(Uri, Nil) { parser.parse(uri) } +/// Encodes a `Uri` value as a URI string. +/// +/// +/// ## Examples +/// +/// ```gleam +/// let uri = Uri( +/// scheme: Some("https"), +/// userinfo: Some("me"), +/// host: Some("host.com"), +/// port: Some(9999), +/// path: "/path", +/// query: Some("q=1"), +/// fragment: Some("fragment") +/// ) +/// to_string(uri) +/// // -> "https://me@host.com:9999/path?q=1#fragment" +/// ``` +/// pub fn to_string(uri: Uri) -> String { let uri_string = case uri.scheme { Some(scheme) -> scheme <> ":" @@ -52,19 +91,58 @@ pub fn to_string(uri: Uri) -> String { uri_string } +/// 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 as described in +/// [RFC 3986](https://tools.ietf.org/html/rfc3986#section-5.2). +/// pub fn merge(base: Uri, relative: Uri) -> Result(Uri, Nil) { utils.merge(base, relative) } -pub fn normalize(uri: Uri) -> Uri { - normalise(uri) -} - +/// Normalises the `Uri` +/// +/// This follows the normalisation process in RFC3986 +/// - Case normalisation (scheme/host -> lowercase, percent-encoding -> uppercase) +/// - Percent-encoding normalisation (removal of non-necessary encoding) +/// - Path segement normalisation (processing of /, .. and .) +/// - Scheme based normalisation (removal of default ports for http/https/ftp/ws/wss, setting empty path to / for valid http(s) uri) +/// +/// ## Examples +/// +/// ```gleam +/// let uri = Uri( +/// scheme: Some("Https"), +/// userinfo: None, +/// host: Some("host.com"), +/// port: Some(443), +/// path: "", +/// query: Some("q=1"), +/// fragment: Some("fragment") +/// ) +/// normalise(uri) +/// // -> "https://host.com/?q=1#fragment" +/// ``` +/// pub fn normalise(uri: Uri) -> Uri { utils.normalise(uri) } -pub fn are_equivalent(uri1: Uri, uri2: Uri) { +/// Determines whether 2 Uris are equivalent, i.e. denote the same endpoint +/// +/// This will perform normalisation if the Uris are not exactly the same +/// +/// ## Examples +/// +/// ```gleam +/// let uri = parse("Https://host.com:443?q=1#fragment") +/// let uri2 = parse("https://HOST.com/?q=1#fragment") +/// are_equivalent(uri, uri2) +/// // -> True +/// ``` +/// +pub fn are_equivalent(uri1: Uri, uri2: Uri) -> Bool { use <- bool.guard(when: uri1 == uri2, return: True) let uri1 = normalise(uri1) @@ -73,6 +151,7 @@ pub fn are_equivalent(uri1: Uri, uri2: Uri) { uri1 == uri2 } +/// Converts a uri library Uri value to the Gleam stdlib Uri value pub fn to_uri(uri: Uri) -> uri.Uri { uri.Uri( uri.scheme, @@ -85,6 +164,7 @@ pub fn to_uri(uri: Uri) -> uri.Uri { ) } +/// Converts a Gleam stdlib Uri value to a uri library Uri value pub fn from_uri(uri: uri.Uri) -> Uri { Uri( uri.scheme, @@ -97,14 +177,46 @@ pub fn from_uri(uri: uri.Uri) -> Uri { ) } +/// Decodes a percent encoded string. +/// +/// Will return an `Error` if the encoding is not valid +/// +/// ## Examples +/// +/// ```gleam +/// percent_decode("This%20is%20worth%20%E2%82%AC1+") +/// // -> Ok("This is worth €1+") +/// ``` +/// pub fn percent_decode(value: String) -> Result(String, Nil) { utils.percent_decode(value) } +/// Encodes a string into a percent encoded string. +/// +/// ## Examples +/// +/// ```gleam +/// percent_encode("This is worth €1+") +/// // -> "This%20is%20worth%20%E2%82%AC1+" +/// ``` +/// pub fn percent_encode(value: String) -> String { utils.do_percent_encode(value) } +/// Encodes a list of key/value pairs into a URI query string +/// +/// Empty keys/values are encoded so would need to be filtered before +/// passing into this function if required +/// +/// ## Examples +/// +/// ```gleam +/// query_to_string([#("first", "1"), #("", ""), #("last", "2")]) +/// // -> "first=1&=&last=2" +/// ``` +/// pub fn query_to_string(query: List(#(String, String))) -> String { list.map(query, fn(q) { [utils.do_percent_encode(q.0), "=", utils.do_percent_encode(q.1)] @@ -114,6 +226,22 @@ pub fn query_to_string(query: List(#(String, String))) -> String { |> string.concat } +/// Takes a query string and returns a list of key/value pairs +/// +/// As this decodes the keys & values an `Error` may be returned if the +/// encoding is invalid +/// +/// As in query_to_string entries with blank key and values are returned +/// Empty entries (i.e. without a = separator) are omitted as they cannot +/// be generated using query_to_string +/// +/// ## Examples +/// +/// ```gleam +/// parse_query("first=1&=&last=2") +/// // -> Ok([#("first", "1"), #("", ""), #("last", "2")]) +/// ``` +/// pub fn parse_query(query: String) -> Result(List(#(String, String)), Nil) { parser.parse_query_parts(query) } diff --git a/src/internal/parser.gleam b/src/uri/internal/parser.gleam similarity index 99% rename from src/internal/parser.gleam rename to src/uri/internal/parser.gleam index 718e52c..f39dc27 100644 --- a/src/internal/parser.gleam +++ b/src/uri/internal/parser.gleam @@ -4,8 +4,8 @@ import gleam/list.{Continue, Stop} import gleam/option.{None, Some} import gleam/result import gleam/string -import internal/utils import splitter +import uri/internal/utils import types.{type Uri, Uri, empty_uri} diff --git a/src/internal/utils.gleam b/src/uri/internal/utils.gleam similarity index 97% rename from src/internal/utils.gleam rename to src/uri/internal/utils.gleam index 74031b1..3ebf15d 100644 --- a/src/internal/utils.gleam +++ b/src/uri/internal/utils.gleam @@ -90,13 +90,34 @@ pub fn normalise(uri: Uri) -> Uri { let port = uri.port |> scheme_normalisation(scheme) let host = uri.host |> option.map(string.lowercase) |> option.map(percent_normaliser) - let path = uri.path |> percent_normaliser |> remove_dot_segments + let path = + uri.path + |> percent_normaliser + |> remove_dot_segments + |> path_normalise(scheme, host) let query = uri.query |> option.map(percent_normaliser) let fragment = uri.fragment |> option.map(percent_normaliser) Uri(scheme, userinfo, host, port, path, query, fragment) } +pub fn path_normalise(str: String, scheme: Option(String), host: Option(String)) { + case str { + "" -> { + case scheme { + Some("http") | Some("https") -> { + case host { + Some(_) -> "/" + _ -> "" + } + } + _ -> "" + } + } + _ -> str + } +} + pub fn scheme_normalisation( port: Option(Int), scheme: Option(String), diff --git a/test/scratch.gleam b/test/scratch.gleam new file mode 100644 index 0000000..e75b3d7 --- /dev/null +++ b/test/scratch.gleam @@ -0,0 +1,12 @@ +// import gleam/result + +// import gleam/uri as uri2 +// import splitter +// import types.{Uri} + +// import uri + +pub fn main() { + // uri.parse_query("a+c=1%23&b=2") |> echo + Nil +}