diff options
-rw-r--r-- | src/gleam/gleam_uri_native.erl | 53 | ||||
-rw-r--r-- | src/gleam/uri.gleam | 78 | ||||
-rw-r--r-- | test/gleam/uri_test.gleam | 110 |
3 files changed, 241 insertions, 0 deletions
diff --git a/src/gleam/gleam_uri_native.erl b/src/gleam/gleam_uri_native.erl new file mode 100644 index 0000000..e0f9d2d --- /dev/null +++ b/src/gleam/gleam_uri_native.erl @@ -0,0 +1,53 @@ +-module (gleam_uri_native). +-export ([parse/1, to_string/1, parse_query/1, query_to_string/1]). + +find_key(Key, Map) -> + case maps:find(Key, Map) of + {ok, Value} -> + {ok, Value}; + error -> + {error, nil} + end. + +parse(String) -> + case uri_string:parse(String) of + {error, _Reason, _Term} -> + {error, nil}; + Map -> + {ok, { + uri, + find_key(scheme, Map), + find_key(userinfo, Map), + find_key(host, Map), + find_key(port, Map), + maps:get(path, Map), + find_key(query, Map), + find_key(fragment, Map) + }} + end. + +to_string({uri, MaybeScheme, MaybeUserinfo, MaybeHost, MaybePort, Path, MaybeQuery, MaybeFragment}) -> + Components = [{scheme, MaybeScheme}, {userinfo, MaybeUserinfo}, {host, MaybeHost}, {port, MaybePort}, {path, {ok, Path}}, {query, MaybeQuery}, {fragment, MaybeFragment}], + Map = maps:from_list([{K, V} || {K, {ok, V}} <- Components]), + case uri_string:recompose(Map) of + String when is_binary(String) -> + String; + % Return value when empty + [] -> <<"">> + end. + +parse_query(String) -> + case uri_string:dissect_query(String) of + {error, _Reason, _Term} -> + {error, nil}; + Parts -> + {ok, Parts} + end. + +query_to_string(Parts) -> + case uri_string:compose_query(Parts, [{encoding, utf8}]) of + String when is_binary(String) -> + String; + % Return value when empty + [] -> <<"">> + end. diff --git a/src/gleam/uri.gleam b/src/gleam/uri.gleam new file mode 100644 index 0000000..84b2360 --- /dev/null +++ b/src/gleam/uri.gleam @@ -0,0 +1,78 @@ +//// 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.{Option} +import gleam/string + +/// 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 complient 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 external fn parse(String) -> Result(Uri, Nil) = + "gleam_uri_native" "parse" + +/// 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 external fn parse_query( + String, +) -> Result(List(tuple(String, String)), Nil) = + "gleam_uri_native" "parse_query" + +/// Encode a list of key value pairs as a URI query string. +/// +/// The opposite operation is `uri.parse_query`. +pub external fn query_to_string(List(tuple(String, String))) -> String = + "gleam_uri_native" "query_to_string" + +fn do_path_segments(input, accumulator) { + case input { + [] -> list.reverse(accumulator) + [segment, ..rest] -> { + let accumulator = case segment, accumulator { + "", accumulator -> accumulator + ".", accumulator -> accumulator + "..", [] -> [] + "..", [_, ..accumulator] -> accumulator + segment, accumulator -> [segment, ..accumulator] + } + do_path_segments(rest, accumulator) + } + } +} + +/// 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) { + do_path_segments(string.split(path, "/"), []) +} + +/// Encode a `Uri` value as a URI string. +/// +/// The opposite operation is `uri.parse`. +pub external fn to_string(Uri) -> String = + "gleam_uri_native" "to_string" diff --git a/test/gleam/uri_test.gleam b/test/gleam/uri_test.gleam new file mode 100644 index 0000000..edb7fce --- /dev/null +++ b/test/gleam/uri_test.gleam @@ -0,0 +1,110 @@ +import gleam/uri +import gleam/should + +pub fn full_parse_test() { + let Ok( + parsed, + ) = uri.parse("https://foo:bar@example.com:1234/path?query=true#fragment") + should.equal(parsed.scheme, Ok("https")) + should.equal(parsed.userinfo, Ok("foo:bar")) + should.equal(parsed.host, Ok("example.com")) + should.equal(parsed.port, Ok(1234)) + should.equal(parsed.path, "/path") + should.equal(parsed.query, Ok("query=true")) + should.equal(parsed.fragment, Ok("fragment")) +} + +pub fn parse_only_path_test() { + let Ok(parsed) = uri.parse("") + should.equal(parsed.scheme, Error(Nil)) + should.equal(parsed.userinfo, Error(Nil)) + should.equal(parsed.host, Error(Nil)) + should.equal(parsed.port, Error(Nil)) + should.equal(parsed.path, "") + should.equal(parsed.query, Error(Nil)) + should.equal(parsed.fragment, Error(Nil)) +} + +pub fn parse_only_host_test() { + let Ok(parsed) = uri.parse("//") + should.equal(parsed.scheme, Error(Nil)) + should.equal(parsed.userinfo, Error(Nil)) + should.equal(parsed.host, Ok("")) + should.equal(parsed.port, Error(Nil)) + should.equal(parsed.path, "") + should.equal(parsed.query, Error(Nil)) + should.equal(parsed.fragment, Error(Nil)) +} + +pub fn error_parsing_uri_test() { + should.equal(uri.parse("::"), Error(Nil)) +} + +pub fn full_uri_to_string_test() { + let test_uri = uri.Uri( + Ok("https"), + Ok("foo:bar"), + Ok("example.com"), + Ok(1234), + "/path", + Ok("query=true"), + Ok("fragment"), + ) + should.equal( + uri.to_string(test_uri), + "https://foo:bar@example.com:1234/path?query=true#fragment", + ) +} + +pub fn path_only_uri_to_string_test() { + let test_uri = uri.Uri( + Error(Nil), + Error(Nil), + Error(Nil), + Error(Nil), + "/", + Error(Nil), + Error(Nil), + ) + should.equal(uri.to_string(test_uri), "/") +} + +pub fn parse_query_string_test() { + let Ok(parsed) = uri.parse_query("foo+bar=1&city=%C3%B6rebro") + should.equal(parsed, [tuple("foo bar", "1"), tuple("city", "örebro")]) +} + +pub fn parse_empty_query_string_test() { + let Ok(parsed) = uri.parse_query("") + should.equal(parsed, []) +} + +pub fn error_parsing_query_test() { + should.equal(uri.parse_query("%C2"), Error(Nil)) +} + +pub fn query_to_string_test() { + let query_string = uri.query_to_string( + [tuple("foo bar", "1"), tuple("city", "örebro")], + ) + should.equal(query_string, "foo+bar=1&city=%C3%B6rebro") +} + +pub fn empty_query_to_string_test() { + let query_string = uri.query_to_string([]) + should.equal(query_string, "") +} + +pub fn parse_segments_test() { + should.equal(uri.path_segments("/"), []) + should.equal(uri.path_segments("/foo/bar"), ["foo", "bar"]) + should.equal(uri.path_segments("////"), []) + should.equal(uri.path_segments("/foo//bar"), ["foo", "bar"]) + + should.equal(uri.path_segments("/."), []) + should.equal(uri.path_segments("/.foo"), [".foo"]) + + should.equal(uri.path_segments("/../bar"), ["bar"]) + should.equal(uri.path_segments("../bar"), ["bar"]) + should.equal(uri.path_segments("/foo/../bar"), ["bar"]) +} |