diff options
-rw-r--r-- | src/gleam/uri.gleam | 237 | ||||
-rw-r--r-- | src/gleam_stdlib.erl | 38 | ||||
-rw-r--r-- | test/gleam/uri_test.gleam | 25 |
3 files changed, 158 insertions, 142 deletions
diff --git a/src/gleam/uri.gleam b/src/gleam/uri.gleam index caa02f9..86e0c3a 100644 --- a/src/gleam/uri.gleam +++ b/src/gleam/uri.gleam @@ -50,144 +50,135 @@ pub fn parse(uri_string: String) -> Result(Uri, Nil) { do_parse(uri_string) } -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.then(string.pop_grapheme) - |> result.map(pair.second) - |> option.from_result - let port = case port { - None -> default_port(scheme) - _ -> port - } - 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, - )) +if erlang { + external fn do_parse(String) -> Result(Uri, Nil) = + "gleam_stdlib" "uri_parse" } -fn default_port(scheme: Option(String)) -> Option(Int) { - case scheme { - Some("ftp") -> Some(21) - Some("sftp") -> Some(22) - Some("tftp") -> Some(69) - Some("http") -> Some(80) - Some("https") -> Some(443) - Some("ldap") -> Some(389) - _ -> None +if 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.then(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, + )) } -} -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.then(list.head) - |> result.map(fn(m: regex.Match) { m.submatches }) - |> result.unwrap([]) -} + 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.then(list.head) + |> result.map(fn(m: regex.Match) { m.submatches }) + |> result.unwrap([]) + } -fn noneify_query(x: Option(String)) -> Option(String) { - case x { - None -> None - Some(x) -> - case string.pop_grapheme(x) { - Ok(#("?", query)) -> Some(query) - _ -> None - } + fn noneify_query(x: Option(String)) -> Option(String) { + case x { + None -> None + Some(x) -> + case string.pop_grapheme(x) { + Ok(#("?", query)) -> Some(query) + _ -> None + } + } } -} -fn noneify_empty_string(x: Option(String)) -> Option(String) { - case x { - Some("") | None -> None - Some(_) -> x + 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. -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) + // Split an authority into its userinfo, host and port parts. + 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) } - _ -> #(None, None, None) } } } -} -fn pad_list(list: List(Option(a)), size: Int) -> List(Option(a)) { - list - |> list.append(list.repeat(None, extra_required(list, size))) -} + fn pad_list(list: List(Option(a)), size: Int) -> List(Option(a)) { + list + |> list.append(list.repeat(None, extra_required(list, size))) + } -fn extra_required(list: List(a), remaining: Int) -> Int { - case list { - _ if remaining == 0 -> 0 - [] -> remaining - [_, ..xs] -> extra_required(xs, remaining - 1) + fn extra_required(list: List(a), remaining: Int) -> Int { + case list { + _ if remaining == 0 -> 0 + [] -> remaining + [_, ..xs] -> extra_required(xs, remaining - 1) + } } } diff --git a/src/gleam_stdlib.erl b/src/gleam_stdlib.erl index f607497..9a27c18 100644 --- a/src/gleam_stdlib.erl +++ b/src/gleam_stdlib.erl @@ -6,7 +6,7 @@ decode_float/1, decode_thunk/1, decode_list/1, decode_option/2, decode_field/2, parse_int/1, parse_float/1, less_than/2, string_pop_grapheme/1, string_starts_with/2, wrap_list/1, - string_ends_with/2, string_pad/4, decode_map/1, + string_ends_with/2, string_pad/4, decode_map/1, uri_parse/1, bit_string_int_to_u32/1, bit_string_int_from_u32/1, decode_result/1, bit_string_slice/3, decode_bit_string/1, compile_regex/2, regex_scan/2, percent_encode/1, percent_decode/1, regex_check/2, regex_split/2, @@ -298,3 +298,39 @@ check_utf8(Cs) -> {error, _, _} -> {error, nil}; _ -> {ok, Cs} end. + +uri_parse(String) -> + case uri_string:parse(String) of + {error, _, _} -> {error, nil}; + Uri -> + % #{ + % host := Host, path := Path, port := Port, query := Query, + % scheme := Scheme, userinfo := Userinfo + % } -> + % scheme: Option(String), + % userinfo: Option(String), + % host: Option(String), + % port: Option(Int), + % path: String, + % query: Option(String), + % fragment: Option(String), + {ok, {uri, + maps_get_optional(Uri, scheme), + maps_get_optional(Uri, userinfo), + maps_get_optional(Uri, host), + maps_get_optional(Uri, port), + maps_get_or(Uri, path, <<>>), + maps_get_optional(Uri, query), + maps_get_optional(Uri, fragment) + }} + end. + +maps_get_optional(Map, Key) -> + try {some, maps:get(Key, Map)} + catch _:_ -> none + end. + +maps_get_or(Map, Key, Default) -> + try maps:get(Key, Map) + catch _:_ -> Default + end. diff --git a/test/gleam/uri_test.gleam b/test/gleam/uri_test.gleam index 78038c1..bcf0d18 100644 --- a/test/gleam/uri_test.gleam +++ b/test/gleam/uri_test.gleam @@ -41,17 +41,6 @@ pub fn parse_only_host_test() { should.equal(parsed.fragment, None) } -pub fn colon_uri_test() { - assert Ok(parsed) = uri.parse("::") - should.equal(parsed.scheme, None) - should.equal(parsed.userinfo, None) - should.equal(parsed.host, None) - should.equal(parsed.port, None) - should.equal(parsed.path, "::") - should.equal(parsed.query, None) - should.equal(parsed.fragment, None) -} - pub fn parse_scheme_test() { uri.parse("http://one.com/path/to/something?one=two&two=one#fragment") |> should.equal(Ok(uri.Uri( @@ -60,7 +49,7 @@ pub fn parse_scheme_test() { path: "/path/to/something", query: Some("one=two&two=one"), fragment: Some("fragment"), - port: Some(80), + port: None, userinfo: None, ))) } @@ -73,7 +62,7 @@ pub fn parse_https_scheme_test() { path: "", query: None, fragment: None, - port: Some(443), + port: None, userinfo: None, ))) } @@ -101,7 +90,7 @@ pub fn parse_ftp_scheme_test() { path: "/my_directory/my_file.txt", query: None, fragment: None, - port: Some(21), + port: None, ))) } @@ -115,7 +104,7 @@ pub fn parse_sftp_scheme_test() { path: "/my_directory/my_file.txt", query: None, fragment: None, - port: Some(22), + port: None, ))) } @@ -129,7 +118,7 @@ pub fn parse_tftp_scheme_test() { path: "/my_directory/my_file.txt", query: None, fragment: None, - port: Some(69), + port: None, ))) } @@ -143,7 +132,7 @@ pub fn parse_ldap_scheme_test() { path: "/dc=example,dc=com", query: Some("?sub?(givenName=John)"), fragment: None, - port: Some(389), + port: None, ))) } @@ -157,7 +146,7 @@ pub fn parse_ldap_2_scheme_test() { path: "/cn=John%20Doe,dc=foo,dc=com", query: None, fragment: None, - port: Some(389), + port: None, ))) } |