aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/gleam/gleam_uri_native.erl53
-rw-r--r--src/gleam/uri.gleam78
-rw-r--r--test/gleam/uri_test.gleam110
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"])
+}