From b7325de6ecead33fa409c0e4e9d2852665e36edb Mon Sep 17 00:00:00 2001 From: Gareth Pendleton Date: Sun, 7 Sep 2025 16:35:20 +0100 Subject: [PATCH] feat: Refactor and added normali(s/z)e --- gleam.toml | 1 + manifest.toml | 2 + src/internal/parser.gleam | 850 +++++++++++++++++++++++++++++++ src/types.gleam | 15 + src/uri.gleam | 1013 +++++++------------------------------ test/uri_test.gleam | 264 +++++----- 6 files changed, 1197 insertions(+), 948 deletions(-) create mode 100644 src/internal/parser.gleam create mode 100644 src/types.gleam diff --git a/gleam.toml b/gleam.toml index b3fd95b..afba55e 100644 --- a/gleam.toml +++ b/gleam.toml @@ -14,6 +14,7 @@ version = "1.0.0" [dependencies] gleam_stdlib = ">= 0.44.0 and < 2.0.0" +splitter = ">= 1.1.0 and < 2.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index 44a039b..42d98c9 100644 --- a/manifest.toml +++ b/manifest.toml @@ -22,6 +22,7 @@ packages = [ { name = "ranger", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "ranger", source = "hex", outer_checksum = "C8988E8F8CDBD3E7F4D8F2E663EF76490390899C2B2885A6432E942495B3E854" }, { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, { name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" }, + { name = "splitter", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "05564A381580395DCDEFF4F88A64B021E8DAFA6540AE99B4623962F52976AA9D" }, { name = "startest", version = "0.7.0", build_tools = ["gleam"], requirements = ["argv", "bigben", "birl", "exception", "gleam_community_ansi", "gleam_erlang", "gleam_javascript", "gleam_regexp", "gleam_stdlib", "glint", "simplifile", "tom"], otp_app = "startest", source = "hex", outer_checksum = "71B9CB82C4B8779A4BD54C7151DF7D0B0F778D0DDE805B782B44EFA7BA8F50DA" }, { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" }, ] @@ -29,4 +30,5 @@ packages = [ [requirements] gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +splitter = { version = ">= 1.1.0 and < 2.0.0" } startest = { version = ">= 0.7.0 and < 1.0.0" } diff --git a/src/internal/parser.gleam b/src/internal/parser.gleam new file mode 100644 index 0000000..81db22b --- /dev/null +++ b/src/internal/parser.gleam @@ -0,0 +1,850 @@ +import gleam/bool +import gleam/int +import gleam/list.{Continue, Stop} +import gleam/option.{None, Some} +import gleam/result +import gleam/string + +import types.{type Uri, Uri, empty_uri} + +pub fn parse(uri: String) -> Result(Uri, Nil) { + case parse_scheme(uri) { + Ok(#(scheme, rest)) -> { + use #(rel_part, rest) <- result.try(parse_hier_part(rest)) + + use #(query, rest) <- result.try(parse_query(rest)) + + use #(fragment, rest) <- result.try(parse_fragment(rest)) + + case rest { + "" -> Ok(combine_uris([scheme, rel_part, query, fragment])) + _ -> Error(Nil) + } + } + Error(_) -> { + use #(rel_part, rest) <- result.try(parse_relative_part(uri)) + + use #(query, rest) <- result.try(parse_query(rest)) + + use #(fragment, rest) <- result.try(parse_fragment(rest)) + + case rest { + "" -> Ok(combine_uris([rel_part, query, fragment])) + _ -> Error(Nil) + } + } + } +} + +fn parse_query(str: String) -> Result(#(Uri, String), Nil) { + case str { + "?" <> rest -> { + let #(query, rest) = get_multiple_optional(parse_query_fragment, rest) + Ok(#(Uri(..empty_uri, query: Some(query)), rest)) + } + _ -> Ok(#(empty_uri, str)) + } +} + +fn parse_fragment(str: String) -> Result(#(Uri, String), Nil) { + case str { + "#" <> rest -> { + let #(fragment, rest) = get_multiple_optional(parse_query_fragment, rest) + Ok(#(Uri(..empty_uri, fragment: Some(fragment)), rest)) + } + _ -> Ok(#(empty_uri, str)) + } +} + +fn parse_hier_part(str: String) -> Result(#(Uri, String), Nil) { + list.fold_until( + [parse_authority, parse_absolute, parse_rootless, parse_empty], + Error(Nil), + get_parser_fn(str), + ) +} + +fn parse_relative_part(str: String) -> Result(#(Uri, String), Nil) { + list.fold_until( + [parse_authority, parse_absolute, parse_noscheme, parse_empty], + Error(Nil), + get_parser_fn(str), + ) +} + +fn parse_absolute(str: String) -> Result(#(Uri, String), Nil) { + case str { + "/" <> rest -> { + let assert Ok(#(seg, rest)) = + parse_optional(rest, parse_this_then( + [ + do_parse_segment_nz, + get_multiple_optional_result( + fn(str) { + case str { + "/" <> rest -> { + do_parse_segment(rest, do_parse_pchar, "/") + } + _ -> Error(Nil) + } + }, + _, + ), + ], + _, + )) + + Ok(#(Uri(None, None, None, None, "/" <> seg, None, None), rest)) + } + _ -> Error(Nil) + } +} + +fn parse_rootless(str: String) -> Result(#(Uri, String), Nil) { + use #(seg1, rest) <- result.try(do_parse_segment_nz(str)) + + let #(segs, rest) = + get_multiple_optional( + fn(str) { + case str { + "/" <> rest -> { + do_parse_segment(rest, do_parse_pchar, "/") + } + _ -> Error(Nil) + } + }, + rest, + ) + + Ok(#(Uri(None, None, None, None, seg1 <> segs, None, None), rest)) +} + +fn parse_noscheme(str: String) -> Result(#(Uri, String), Nil) { + use #(seg1, rest) <- result.try(do_parse_segment_nz_nc(str)) + + let #(segs, rest) = + get_multiple_optional( + fn(str) { + case str { + "/" <> rest -> { + do_parse_segment(rest, do_parse_pchar, "/") + } + _ -> Error(Nil) + } + }, + rest, + ) + + Ok(#(Uri(None, None, None, None, seg1 <> segs, None, None), rest)) +} + +fn parse_optional(str, opt_fn) { + case opt_fn(str) { + Error(Nil) -> Ok(#("", str)) + Ok(r) -> Ok(r) + } +} + +fn get_multiple_optional_result(opt_fn, str: String) { + get_multiple_optional(opt_fn, str) |> Ok +} + +fn get_multiple_optional(opt_fn, str: String) { + case get_multiple(opt_fn, str) { + Error(_) -> #("", str) + Ok(r) -> r + } +} + +fn parse_empty(str: String) -> Result(#(Uri, String), Nil) { + Ok(#(Uri(None, None, None, None, "", None, None), str)) +} + +fn parse_authority(str: String) -> Result(#(Uri, String), Nil) { + case str { + "//" <> rest -> { + parse_authority_part(rest) |> echo + } + _ -> Error(Nil) + } +} + +fn parse_authority_part(str: String) -> Result(#(Uri, String), Nil) { + let #(ui, rest) = case parse_userinfo(str, "") { + Ok(#(ui, rest)) -> #(Some(ui), rest) + Error(_) -> #(None, str) + } + + use #(host, rest) <- result.try(parse_host(rest)) + + let #(port, rest) = case parse_port(rest) { + Ok(#("", rest)) -> #(None, rest) + Error(_) -> #(None, rest) + Ok(#(port, rest)) -> { + let assert Ok(port) = int.parse(port) + #(Some(port), rest) + } + } + + let #(path, rest) = parse_abs_empty(rest) + + Ok(#(Uri(None, ui, Some(host), port, path, None, None), rest)) +} + +fn parse_port(str: String) { + case str { + ":" <> rest -> { + Ok(parse_digits(rest, "")) + } + _ -> Error(Nil) + } +} + +fn parse_digits(str: String, digits: String) { + case parse_digit(str) { + Ok(#(d, rest)) -> { + parse_digits(rest, digits <> d) + } + Error(_) -> #(digits, str) + } +} + +fn parse_host(str: String) { + list.fold_until( + [parse_ip_literal, parse_ipv4, parse_reg_name], + Error(Nil), + get_parser_fn(str), + ) +} + +fn parse_ip_literal(str: String) { + case str { + "[" <> rest -> { + use #(ip, rest) <- result.try(list.fold_until( + [parse_ipv6, parse_ipfuture], + Error(Nil), + get_parser_fn(rest), + )) + case rest { + "]" <> rest -> Ok(#(ip, rest)) + _ -> Error(Nil) + } + } + _ -> Error(Nil) + } +} + +fn parse_ipv6(str: String) { + list.fold_until( + [ + parse_this_then([parse_min_max(_, 6, 6, parse_h16_colon), parse_ls32], _), + parse_this_then( + [parse_colons, parse_min_max(_, 5, 5, parse_h16_colon), parse_ls32], + _, + ), + parse_this_then( + [ + parse_optional(_, parse_h16), + parse_colons, + parse_min_max(_, 4, 4, parse_h16_colon), + parse_ls32, + ], + _, + ), + parse_this_then( + [ + parse_optional(_, parse_this_then([parse_h16s(_, 1), parse_h16], _)), + parse_colons, + parse_min_max(_, 3, 3, parse_h16_colon), + parse_ls32, + ], + _, + ), + parse_this_then( + [ + parse_optional(_, parse_this_then([parse_h16s(_, 2), parse_h16], _)), + parse_colons, + parse_min_max(_, 2, 2, parse_h16_colon), + parse_ls32, + ], + _, + ), + parse_this_then( + [ + parse_optional(_, parse_this_then([parse_h16s(_, 3), parse_h16], _)), + parse_colons, + parse_min_max(_, 1, 1, parse_h16_colon), + parse_ls32, + ], + _, + ), + parse_this_then( + [ + parse_optional(_, parse_this_then([parse_h16s(_, 4), parse_h16], _)), + parse_colons, + parse_ls32, + ], + _, + ), + parse_this_then( + [ + parse_optional(_, parse_this_then([parse_h16s(_, 5), parse_h16], _)), + parse_colons, + parse_h16, + ], + _, + ), + parse_this_then( + [ + parse_optional(_, parse_this_then([parse_h16s(_, 6), parse_h16], _)), + parse_colons, + ], + _, + ), + ], + Error(Nil), + get_parser_fn(str), + ) +} + +fn parse_h16s(str: String, max) { + parse_min_max(str, 0, max, parse_h16_colon) +} + +fn parse_colons(str: String) { + case str { + "::" <> rest -> Ok(#("::", rest)) + _ -> Error(Nil) + } +} + +fn parse_this_then( + parsers: List(fn(String) -> Result(#(String, String), Nil)), + str: String, +) { + list.fold_until(parsers, Ok(#("", str)), fn(acc, parser) { + let assert Ok(#(res, str)) = acc + case parser(str) { + Ok(#(res2, rest)) -> { + Continue(Ok(#(res <> res2, rest))) + } + Error(Nil) -> Stop(Error(Nil)) + } + }) +} + +fn parse_ls32(str: String) -> Result(#(String, String), Nil) { + list.fold_until([parse_h16_pair, parse_ipv4], Error(Nil), get_parser_fn(str)) +} + +fn parse_h16_pair(str: String) { + use #(h16a, rest) <- result.try(parse_h16(str)) + case rest { + ":" <> rest -> { + use #(h16b, rest) <- result.try(parse_h16(rest)) + Ok(#(h16a <> ":" <> h16b, rest)) + } + _ -> Error(Nil) + } +} + +fn parse_h16(str: String) { + parse_hex_digits(str, 1, 4) +} + +fn parse_h16_colon(str: String) { + use #(h16, rest) <- result.try(parse_h16(str)) + case rest { + ":" <> rest -> Ok(#(h16 <> ":", rest)) + _ -> Error(Nil) + } +} + +fn parse_ipfuture(str: String) { + Error(Nil) +} + +fn get_multiple( + to_run: fn(String) -> Result(#(String, String), Nil), + str: String, +) -> Result(#(String, String), Nil) { + case do_get_multiple(to_run, str, "") { + Ok(#("", _)) | Error(Nil) -> Error(Nil) + Ok(#(r, rest)) -> Ok(#(r, rest)) + } +} + +fn do_get_multiple( + to_run: fn(String) -> Result(#(String, String), Nil), + str: String, + ret: String, +) -> Result(#(String, String), Nil) { + case str { + "" -> Ok(#(ret, str)) + _ -> + case to_run(str) { + Ok(#(r, rest)) -> do_get_multiple(to_run, rest, ret <> r) + Error(_) -> Ok(#(ret, str)) + } + } +} + +fn parse_query_fragment(str: String) { + list.fold_until( + [ + do_parse_pchar, + fn(str: String) { + case str { + "/" as l <> rest | "?" as l <> rest -> Ok(#(l, rest)) + _ -> Error(Nil) + } + }, + ], + Error(Nil), + get_parser_fn(str), + ) +} + +fn parse_abs_empty(str: String) -> #(String, String) { + get_multiple_optional( + fn(str) { + case str { + "/" <> rest -> { + do_parse_segment(rest, do_parse_pchar, "/") + } + _ -> Error(Nil) + } + }, + str, + ) +} + +fn do_parse_segment( + str: String, + char_fn, + segment: String, +) -> Result(#(String, String), Nil) { + case char_fn(str) { + Error(Nil) | Ok(#("", _)) -> Ok(#(segment, str)) + Ok(#(l, rest)) -> do_parse_segment(rest, char_fn, segment <> l) + } +} + +fn do_parse_segment_nz(str: String) { + use #(char1, rest) <- result.try(do_parse_pchar(str)) + + use #(chars, rest) <- result.try(do_parse_segment(rest, do_parse_pchar, char1)) + + Ok(#(chars, rest)) +} + +fn do_parse_segment_nz_nc(str: String) { + use #(char1, rest) <- result.try(do_parse_pchar_nc(str)) + + use #(chars, rest) <- result.try(do_parse_segment( + rest, + do_parse_pchar_nc, + char1, + )) + + Ok(#(chars, rest)) +} + +fn do_parse_pchar(str: String) { + list.fold_until( + [ + parse_unreserved, + parse_pct_encoded, + parse_sub_delim, + fn(str: String) { + case str { + ":" as l <> rest | "@" as l <> rest -> Ok(#(l, rest)) + _ -> Error(Nil) + } + }, + ], + Error(Nil), + get_parser_fn(str), + ) +} + +fn do_parse_pchar_nc(str: String) { + list.fold_until( + [ + parse_unreserved, + parse_pct_encoded, + parse_sub_delim, + fn(str: String) { + case str { + "@" as l <> rest -> Ok(#(l, rest)) + _ -> Error(Nil) + } + }, + ], + Error(Nil), + get_parser_fn(str), + ) +} + +fn parse_reg_name(str: String) { + // can't error + + case do_parse_reg_name(str, "") |> echo { + Error(Nil) -> Ok(#("", str)) + Ok(#(reg_name, rest)) -> Ok(#(reg_name, rest)) + } +} + +fn do_parse_reg_name(str: String, reg_name: String) { + case + list.fold_until( + [parse_unreserved, parse_pct_encoded, parse_sub_delim], + Error(Nil), + get_parser_fn(str), + ) + { + Error(Nil) | Ok(#("", _)) -> Ok(#(reg_name, str)) + Ok(#(l, rest)) -> do_parse_reg_name(rest, reg_name <> l) + } +} + +fn parse_pct_encoded(str: String) { + case str { + "%" <> rest -> { + use #(hex1, rest) <- result.try(parse_hex_digit(rest)) + use #(hex2, rest) <- result.try(parse_hex_digit(rest)) + + Ok(#("%" <> hex1 <> hex2, rest)) + } + _ -> Error(Nil) + } +} + +fn parse_sub_delim(str: String) { + case str { + "!" as l <> rest + | "$" as l <> rest + | "&" as l <> rest + | "'" as l <> rest + | "(" as l <> rest + | ")" as l <> rest + | "*" as l <> rest + | "+" as l <> rest + | "," as l <> rest + | ";" as l <> rest + | "=" as l <> rest -> Ok(#(l, rest)) + _ -> Error(Nil) + } +} + +fn parse_ipv4(str: String) { + use #(oct1, rest) <- result.try(parse_dec_octet(str)) + use rest <- result.try(case rest { + "." <> rest -> Ok(rest) + _ -> Error(Nil) + }) + use #(oct2, rest) <- result.try(parse_dec_octet(rest)) + use rest <- result.try(case rest { + "." <> rest -> Ok(rest) + _ -> Error(Nil) + }) + use #(oct3, rest) <- result.try(parse_dec_octet(rest)) + use rest <- result.try(case rest { + "." <> rest -> Ok(rest) + _ -> Error(Nil) + }) + use #(oct4, rest) <- result.try(parse_dec_octet(rest)) + Ok(#(oct1 <> "." <> oct2 <> "." <> oct3 <> "." <> oct4, rest)) +} + +fn parse_dec_octet(str: String) -> Result(#(String, String), Nil) { + let matches = [ + ["2", "5", "012345"], + ["2", "01234", "0123456789"], + ["1", "0123456789", "0123456789"], + ["123456789", "0123456789"], + ["0123456789"], + ] + + list.fold_until(matches, Error(Nil), fn(_, chars) { + case + list.fold_until(chars, #("", str), fn(acc, charset) { + let #(octet, str) = acc + case string.pop_grapheme(str) { + Error(_) -> Stop(#("", "")) + Ok(#(char, rest)) -> { + case string.contains(charset, char) { + True -> Continue(#(octet <> char, rest)) + False -> Stop(#("", "")) + } + } + } + }) + { + #("", _) -> Continue(Error(Nil)) + #(octet, rest) -> Stop(Ok(#(octet, rest))) + } + }) +} + +fn parse_userinfo( + str: String, + userinfo: String, +) -> Result(#(String, String), Nil) { + case str { + "@" <> rest -> Ok(#(userinfo, rest)) + "" -> Error(Nil) + _ -> { + use #(part, rest) <- result.try(list.fold_until( + [ + parse_unreserved, + parse_pct_encoded, + parse_sub_delim, + fn(str: String) { + case str { + ":" as l <> rest -> Ok(#(l, rest)) + _ -> Error(Nil) + } + }, + ], + Error(Nil), + get_parser_fn(str), + )) + parse_userinfo(rest, userinfo <> part) + } + } +} + +fn parse_scheme(str: String) -> Result(#(Uri, String), Nil) { + case parse_alpha(str) { + Ok(#(first, rest)) -> { + case do_parse_scheme(rest, first) { + Error(_) -> Error(Nil) + Ok(#(scheme, rest)) -> + Ok(#(Uri(Some(scheme), None, None, None, "", None, None), rest)) + } + } + _ -> Error(Nil) + } +} + +fn do_parse_scheme( + str: String, + scheme: String, +) -> Result(#(String, String), Nil) { + case str { + ":" <> rest -> Ok(#(scheme, rest)) + "" -> Error(Nil) + _ -> { + use #(part, rest) <- result.try(list.fold_until( + [ + parse_alpha, + parse_digit, + fn(str) { + case str { + "+" as l <> rest | "-" as l <> rest | "." as l <> rest -> + Ok(#(l, rest)) + _ -> Error(Nil) + } + }, + ], + Error(Nil), + get_parser_fn(str), + )) + do_parse_scheme(rest, scheme <> part) + } + } +} + +fn get_parser_fn( + str: String, +) -> fn(a, fn(String) -> Result(b, c)) -> list.ContinueOrStop(Result(b, Nil)) { + fn(_, parse_fn) { + case parse_fn(str) { + Ok(r) -> Stop(Ok(r)) + Error(_) -> Continue(Error(Nil)) + } + } +} + +fn parse_min_max(str, min, max, parse_fn) { + use <- bool.guard(when: min < 0 || max <= 0 || min > max, return: Error(Nil)) + case + list.repeat("", max) + |> list.fold_until(Ok(#("", str, 0)), fn(acc, _) { + let assert Ok(#(hex, str, i)) = acc + case parse_fn(str) { + Error(_) -> + case i < min { + True -> Stop(Error(Nil)) + False -> Stop(Ok(#(hex, str, i))) + } + Ok(#(l, rest)) -> Continue(Ok(#(hex <> l, rest, i + 1))) + } + }) + { + Error(_) -> Error(Nil) + Ok(#(hex, str, _)) -> Ok(#(hex, str)) + } +} + +fn parse_hex_digits(str, min, max) { + parse_min_max(str, min, max, parse_hex_digit) +} + +pub fn parse_hex_digit(str) { + case str { + "0" as l <> rest + | "1" as l <> rest + | "2" as l <> rest + | "3" as l <> rest + | "4" as l <> rest + | "5" as l <> rest + | "6" as l <> rest + | "7" as l <> rest + | "8" as l <> rest + | "9" as l <> rest + | "a" as l <> rest + | "b" as l <> rest + | "c" as l <> rest + | "d" as l <> rest + | "e" as l <> rest + | "f" as l <> rest + | "A" as l <> rest + | "B" as l <> rest + | "C" as l <> rest + | "D" as l <> rest + | "E" as l <> rest + | "F" as l <> rest -> Ok(#(l, rest)) + _ -> Error(Nil) + } +} + +fn parse_digit(str: String) -> Result(#(String, String), Nil) { + case str { + "0" as l <> rest + | "1" as l <> rest + | "2" as l <> rest + | "3" as l <> rest + | "4" as l <> rest + | "5" as l <> rest + | "6" as l <> rest + | "7" as l <> rest + | "8" as l <> rest + | "9" as l <> rest -> Ok(#(l, rest)) + _ -> Error(Nil) + } +} + +fn parse_alpha(str: String) -> Result(#(String, String), Nil) { + case str { + "a" as l <> rest + | "b" as l <> rest + | "c" as l <> rest + | "d" as l <> rest + | "e" as l <> rest + | "f" as l <> rest + | "g" as l <> rest + | "h" as l <> rest + | "i" as l <> rest + | "j" as l <> rest + | "k" as l <> rest + | "l" as l <> rest + | "m" as l <> rest + | "n" as l <> rest + | "o" as l <> rest + | "p" as l <> rest + | "q" as l <> rest + | "r" as l <> rest + | "s" as l <> rest + | "t" as l <> rest + | "u" as l <> rest + | "v" as l <> rest + | "w" as l <> rest + | "x" as l <> rest + | "y" as l <> rest + | "z" as l <> rest + | "A" as l <> rest + | "B" as l <> rest + | "C" as l <> rest + | "D" as l <> rest + | "E" as l <> rest + | "F" as l <> rest + | "G" as l <> rest + | "H" as l <> rest + | "I" as l <> rest + | "J" as l <> rest + | "K" as l <> rest + | "L" as l <> rest + | "M" as l <> rest + | "N" as l <> rest + | "O" as l <> rest + | "P" as l <> rest + | "Q" as l <> rest + | "R" as l <> rest + | "S" as l <> rest + | "T" as l <> rest + | "U" as l <> rest + | "V" as l <> rest + | "W" as l <> rest + | "X" as l <> rest + | "Y" as l <> rest + | "Z" as l <> rest -> Ok(#(l, rest)) + _ -> Error(Nil) + } +} + +fn parse_unreserved(str: String) -> Result(#(String, String), Nil) { + list.fold_until( + [ + parse_alpha, + parse_digit, + fn(str) { + case str { + "_" as l <> rest + | "-" as l <> rest + | "." as l <> rest + | "~" as l <> rest -> Ok(#(l, rest)) + _ -> Error(Nil) + } + }, + ], + Error(Nil), + get_parser_fn(str), + ) +} + +fn combine_uris(uris: List(Uri)) -> Uri { + list.fold(uris, Uri(None, None, None, None, "", None, None), fn(acc, uri) { + let acc = case uri { + Uri(Some(scheme), _, _, _, _, _, _) -> Uri(..acc, scheme: Some(scheme)) + _ -> acc + } + let acc = case uri { + Uri(_, Some(userinfo), _, _, _, _, _) -> + Uri(..acc, userinfo: Some(userinfo)) + _ -> acc + } + let acc = case uri { + Uri(_, _, Some(host), _, _, _, _) -> Uri(..acc, host: Some(host)) + _ -> acc + } + let acc = case uri { + Uri(_, _, _, Some(port), _, _, _) -> Uri(..acc, port: Some(port)) + _ -> acc + } + let acc = case uri { + Uri(_, _, _, _, path, _, _) if path != "" -> Uri(..acc, path: path) + _ -> acc + } + let acc = case uri { + Uri(_, _, _, _, _, Some(query), _) -> Uri(..acc, query: Some(query)) + _ -> acc + } + case uri { + Uri(_, _, _, _, _, _, Some(fragment)) -> + Uri(..acc, fragment: Some(fragment)) + _ -> acc + } + }) +} diff --git a/src/types.gleam b/src/types.gleam new file mode 100644 index 0000000..364b251 --- /dev/null +++ b/src/types.gleam @@ -0,0 +1,15 @@ +import gleam/option.{type Option, None} + +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 const empty_uri = Uri(None, None, None, None, "", None, None) diff --git a/src/uri.gleam b/src/uri.gleam index 13e9321..9703b4a 100644 --- a/src/uri.gleam +++ b/src/uri.gleam @@ -1,865 +1,224 @@ import gleam/bool import gleam/int -import gleam/list.{Continue, Stop} -import gleam/option.{type Option, None, Some} -import gleam/result +import gleam/option.{None, Some} import gleam/string import gleam/uri - -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 const empty_uri = Uri(None, None, None, None, "", None, None) +import internal/parser +import splitter.{type Splitter} +import types.{type Uri, Uri} pub fn parse(uri: String) -> Result(Uri, Nil) { - case parse_scheme(uri) { - Ok(#(scheme, rest)) -> { - use #(rel_part, rest) <- result.try(parse_hier_part(rest)) + parser.parse(uri) +} - use #(query, rest) <- result.try(parse_query(rest)) +pub fn to_string(uri: Uri) -> String { + let parts = case uri.fragment { + Some(fragment) -> ["#", fragment] + None -> [] + } + let parts = case uri.query { + Some(query) -> ["?", query, ..parts] + None -> parts + } + let parts = [uri.path, ..parts] + let parts = case uri.host, string.starts_with(uri.path, "/") { + Some(host), False if host != "" -> ["/", ..parts] + _, _ -> parts + } + let parts = case uri.host, uri.port { + Some(_), Some(port) -> [":", int.to_string(port), ..parts] + _, _ -> parts + } + let parts = case uri.scheme, uri.userinfo, uri.host { + Some(s), Some(u), Some(h) -> [s, "://", u, "@", h, ..parts] + Some(s), None, Some(h) -> [s, "://", h, ..parts] + Some(s), Some(_), None | Some(s), None, None -> [s, ":", ..parts] + None, None, Some(h) -> ["//", h, ..parts] + _, _, _ -> parts + } + string.concat(parts) +} - use #(fragment, rest) <- result.try(parse_fragment(rest)) +pub fn normalize(uri: Uri) -> Uri { + normalise(uri) +} - case rest { - "" -> Ok(combine_uris([scheme, rel_part, query, fragment])) - _ -> Error(Nil) - } - } - Error(_) -> { - use #(rel_part, rest) <- result.try(parse_relative_part(uri)) +pub fn normalise(uri: Uri) -> Uri { + let percent_splitter = splitter.new(["%"]) + let percent_normaliser = normalise_percent(percent_splitter, _) + let scheme = uri.scheme |> option.map(string.lowercase) + let userinfo = uri.userinfo |> option.map(percent_normaliser) + let port = uri.port + let host = + uri.host |> option.map(string.lowercase) |> option.map(percent_normaliser) + let path = uri.path |> percent_normaliser |> normalise_path + let query = uri.query |> option.map(percent_normaliser) + let fragment = uri.fragment |> option.map(percent_normaliser) - use #(query, rest) <- result.try(parse_query(rest)) + Uri(scheme, userinfo, host, port, path, query, fragment) +} - use #(fragment, rest) <- result.try(parse_fragment(rest)) +fn normalise_path(path: String) -> String { + do_normalise_path(path, "") +} - case rest { - "" -> Ok(combine_uris([rel_part, query, fragment])) - _ -> Error(Nil) - } +fn do_normalise_path(path: String, acc: String) -> String { + case path { + "../" <> rest | "./" <> rest -> do_normalise_path(rest, acc) + "/./" <> rest -> do_normalise_path("/" <> rest, acc) + "/." -> do_normalise_path("/", acc) + "/../" <> rest -> do_normalise_path("/" <> rest, remove_segment(acc)) + "/.." -> do_normalise_path("/", remove_segment(acc)) + "." | ".." | "" -> acc + _ -> { + let assert Ok(#(char, rest)) = string.pop_grapheme(path) + do_normalise_path(rest, acc <> char) } } } -fn parse_query(str: String) -> Result(#(Uri, String), Nil) { - case str { - "?" <> rest -> { - let #(query, rest) = get_multiple_optional(parse_query_fragment, rest) - Ok(#(Uri(..empty_uri, query: Some(query)), rest)) +fn remove_segment(path: String) -> String { + path |> echo |> string.reverse |> do_remove_segment |> string.reverse +} + +fn do_remove_segment(path: String) -> String { + case path { + "/" <> rest -> rest + "" -> "" + _ -> { + do_remove_segment(path |> string.drop_start(1)) } - _ -> Ok(#(empty_uri, str)) } } -fn parse_fragment(str: String) -> Result(#(Uri, String), Nil) { - case str { - "#" <> rest -> { - let #(fragment, rest) = get_multiple_optional(parse_query_fragment, rest) - Ok(#(Uri(..empty_uri, fragment: Some(fragment)), rest)) - } - _ -> Ok(#(empty_uri, str)) - } +fn normalise_percent(percent_splitter: Splitter, str: String) -> String { + do_normalise_percent(percent_splitter, str, "") } -fn parse_hier_part(str: String) -> Result(#(Uri, String), Nil) { - list.fold_until( - [parse_authority, parse_absolute, parse_rootless, parse_empty], - Error(Nil), - get_parser_fn(str), - ) -} - -fn parse_relative_part(str: String) -> Result(#(Uri, String), Nil) { - list.fold_until( - [parse_authority, parse_absolute, parse_noscheme, parse_empty], - Error(Nil), - get_parser_fn(str), - ) -} - -fn parse_absolute(str: String) -> Result(#(Uri, String), Nil) { - case str { - "/" <> rest -> { - let assert Ok(#(seg, rest)) = - parse_optional(rest, parse_this_then( - [ - do_parse_segment_nz, - get_multiple_optional_result( - fn(str) { - case str { - "/" <> rest -> { - do_parse_segment(rest, do_parse_pchar, "/") +fn do_normalise_percent( + percent_splitter: Splitter, + str: String, + res: String, +) -> String { + let #(before, pc, after) = splitter.split(percent_splitter, str) + case pc { + "" -> res <> before + _ -> { + case after { + "" -> res <> before + _ -> { + let #(pc_val, rest) = case parser.parse_hex_digit(after) { + Ok(#(pc1, rest)) -> { + case parser.parse_hex_digit(rest) { + Ok(#(pc2, rest)) -> { + let hex = pc1 <> pc2 + let v = unescape_percent(hex) + case v == hex { + True -> #("%" <> string.uppercase(v), rest) + False -> #(v, rest) } - _ -> Error(Nil) } - }, - _, - ), - ], - _, - )) - - Ok(#(Uri(None, None, None, None, "/" <> seg, None, None), rest)) - } - _ -> Error(Nil) - } -} - -fn parse_rootless(str: String) -> Result(#(Uri, String), Nil) { - use #(seg1, rest) <- result.try(do_parse_segment_nz(str)) - - let #(segs, rest) = - get_multiple_optional( - fn(str) { - case str { - "/" <> rest -> { - do_parse_segment(rest, do_parse_pchar, "/") - } - _ -> Error(Nil) - } - }, - rest, - ) - - Ok(#(Uri(None, None, None, None, seg1 <> segs, None, None), rest)) -} - -fn parse_noscheme(str: String) -> Result(#(Uri, String), Nil) { - use #(seg1, rest) <- result.try(do_parse_segment_nz_nc(str)) - - let #(segs, rest) = - get_multiple_optional( - fn(str) { - case str { - "/" <> rest -> { - do_parse_segment(rest, do_parse_pchar, "/") - } - _ -> Error(Nil) - } - }, - rest, - ) - - Ok(#(Uri(None, None, None, None, seg1 <> segs, None, None), rest)) -} - -fn parse_optional(str, opt_fn) { - case opt_fn(str) { - Error(Nil) -> Ok(#("", str)) - Ok(r) -> Ok(r) - } -} - -fn get_multiple_optional_result(opt_fn, str: String) { - get_multiple_optional(opt_fn, str) |> Ok -} - -fn get_multiple_optional(opt_fn, str: String) { - case get_multiple(opt_fn, str) { - Error(_) -> #("", str) - Ok(r) -> r - } -} - -fn parse_empty(str: String) -> Result(#(Uri, String), Nil) { - Ok(#(Uri(None, None, None, None, "", None, None), str)) -} - -fn parse_authority(str: String) -> Result(#(Uri, String), Nil) { - case str { - "//" <> rest -> { - parse_authority_part(rest) - } - _ -> Error(Nil) - } -} - -fn parse_authority_part(str: String) -> Result(#(Uri, String), Nil) { - let #(ui, rest) = case parse_userinfo(str, "") { - Ok(#(ui, rest)) -> #(Some(ui), rest) - Error(_) -> #(None, str) - } - - use #(host, rest) <- result.try(parse_host(rest)) - - let #(port, rest) = case parse_port(rest) { - Ok(#("", rest)) -> #(None, rest) - Error(_) -> #(None, rest) - Ok(#(port, rest)) -> { - let assert Ok(port) = int.parse(port) - #(Some(port), rest) - } - } - - let #(path, rest) = parse_abs_empty(rest) - - Ok(#(Uri(None, ui, Some(host), port, path, None, None), rest)) -} - -fn parse_port(str: String) { - case str { - ":" <> rest -> { - Ok(parse_digits(rest, "")) - } - _ -> Error(Nil) - } -} - -fn parse_digits(str: String, digits: String) { - case parse_digit(str) { - Ok(#(d, rest)) -> { - parse_digits(rest, digits <> d) - } - Error(_) -> #(digits, str) - } -} - -fn parse_host(str: String) { - list.fold_until( - [parse_ip_literal, parse_ipv4, parse_reg_name], - Error(Nil), - get_parser_fn(str), - ) -} - -fn parse_ip_literal(str: String) { - case str { - "[" <> rest -> { - use #(ip, rest) <- result.try(list.fold_until( - [parse_ipv6, parse_ipfuture], - Error(Nil), - get_parser_fn(rest), - )) - case rest { - "]" <> rest -> Ok(#(ip, rest)) - _ -> Error(Nil) - } - } - _ -> Error(Nil) - } -} - -fn parse_ipv6(str: String) { - list.fold_until( - [ - parse_this_then([parse_min_max(_, 6, 6, parse_h16_colon), parse_ls32], _), - parse_this_then( - [parse_colons, parse_min_max(_, 5, 5, parse_h16_colon), parse_ls32], - _, - ), - parse_this_then( - [ - parse_optional(_, parse_h16), - parse_colons, - parse_min_max(_, 4, 4, parse_h16_colon), - parse_ls32, - ], - _, - ), - parse_this_then( - [ - parse_optional(_, parse_this_then([parse_h16s(_, 1), parse_h16], _)), - parse_colons, - parse_min_max(_, 3, 3, parse_h16_colon), - parse_ls32, - ], - _, - ), - parse_this_then( - [ - parse_optional(_, parse_this_then([parse_h16s(_, 2), parse_h16], _)), - parse_colons, - parse_min_max(_, 2, 2, parse_h16_colon), - parse_ls32, - ], - _, - ), - parse_this_then( - [ - parse_optional(_, parse_this_then([parse_h16s(_, 3), parse_h16], _)), - parse_colons, - parse_min_max(_, 1, 1, parse_h16_colon), - parse_ls32, - ], - _, - ), - parse_this_then( - [ - parse_optional(_, parse_this_then([parse_h16s(_, 4), parse_h16], _)), - parse_colons, - parse_ls32, - ], - _, - ), - parse_this_then( - [ - parse_optional(_, parse_this_then([parse_h16s(_, 5), parse_h16], _)), - parse_colons, - parse_h16, - ], - _, - ), - parse_this_then( - [ - parse_optional(_, parse_this_then([parse_h16s(_, 6), parse_h16], _)), - parse_colons, - ], - _, - ), - ], - Error(Nil), - get_parser_fn(str), - ) -} - -fn parse_h16s(str: String, max) { - parse_min_max(str, 0, max, parse_h16_colon) -} - -fn parse_colons(str: String) { - case str { - "::" <> rest -> Ok(#("::", rest)) - _ -> Error(Nil) - } -} - -fn parse_this_then( - parsers: List(fn(String) -> Result(#(String, String), Nil)), - str: String, -) { - list.fold_until(parsers, Ok(#("", str)), fn(acc, parser) { - let assert Ok(#(res, str)) = acc - case parser(str) { - Ok(#(res2, rest)) -> { - Continue(Ok(#(res <> res2, rest))) - } - Error(Nil) -> Stop(Error(Nil)) - } - }) -} - -fn parse_ls32(str: String) -> Result(#(String, String), Nil) { - list.fold_until([parse_h16_pair, parse_ipv4], Error(Nil), get_parser_fn(str)) -} - -fn parse_h16_pair(str: String) { - use #(h16a, rest) <- result.try(parse_h16(str)) - case rest { - ":" <> rest -> { - use #(h16b, rest) <- result.try(parse_h16(rest)) - Ok(#(h16a <> ":" <> h16b, rest)) - } - _ -> Error(Nil) - } -} - -fn parse_h16(str: String) { - parse_hex_digits(str, 1, 4) -} - -fn parse_h16_colon(str: String) { - use #(h16, rest) <- result.try(parse_h16(str)) - case rest { - ":" <> rest -> Ok(#(h16 <> ":", rest)) - _ -> Error(Nil) - } -} - -fn parse_ipfuture(str: String) { - Error(Nil) -} - -fn get_multiple( - to_run: fn(String) -> Result(#(String, String), Nil), - str: String, -) -> Result(#(String, String), Nil) { - case do_get_multiple(to_run, str, "") { - Ok(#("", _)) | Error(Nil) -> Error(Nil) - Ok(#(r, rest)) -> Ok(#(r, rest)) - } -} - -fn do_get_multiple( - to_run: fn(String) -> Result(#(String, String), Nil), - str: String, - ret: String, -) -> Result(#(String, String), Nil) { - case str { - "" -> Ok(#(ret, str)) - _ -> - case to_run(str) { - Ok(#(r, rest)) -> do_get_multiple(to_run, rest, ret <> r) - Error(_) -> Ok(#(ret, str)) - } - } -} - -fn parse_query_fragment(str: String) { - list.fold_until( - [ - do_parse_pchar, - fn(str: String) { - case str { - "/" as l <> rest | "?" as l <> rest -> Ok(#(l, rest)) - _ -> Error(Nil) - } - }, - ], - Error(Nil), - get_parser_fn(str), - ) -} - -fn parse_abs_empty(str: String) -> #(String, String) { - get_multiple_optional( - fn(str) { - case str { - "/" <> rest -> { - do_parse_segment(rest, do_parse_pchar, "/") - } - _ -> Error(Nil) - } - }, - str, - ) -} - -fn do_parse_segment( - str: String, - char_fn, - segment: String, -) -> Result(#(String, String), Nil) { - case char_fn(str) { - Error(Nil) | Ok(#("", _)) -> Ok(#(segment, str)) - Ok(#(l, rest)) -> do_parse_segment(rest, char_fn, segment <> l) - } -} - -fn do_parse_segment_nz(str: String) { - use #(char1, rest) <- result.try(do_parse_pchar(str)) - - use #(chars, rest) <- result.try(do_parse_segment(rest, do_parse_pchar, char1)) - - Ok(#(chars, rest)) -} - -fn do_parse_segment_nz_nc(str: String) { - use #(char1, rest) <- result.try(do_parse_pchar_nc(str)) - - use #(chars, rest) <- result.try(do_parse_segment( - rest, - do_parse_pchar_nc, - char1, - )) - - Ok(#(chars, rest)) -} - -fn do_parse_pchar(str: String) { - list.fold_until( - [ - parse_unreserved, - parse_pct_encoded, - parse_sub_delim, - fn(str: String) { - case str { - ":" as l <> rest | "@" as l <> rest -> Ok(#(l, rest)) - _ -> Error(Nil) - } - }, - ], - Error(Nil), - get_parser_fn(str), - ) -} - -fn do_parse_pchar_nc(str: String) { - list.fold_until( - [ - parse_unreserved, - parse_pct_encoded, - parse_sub_delim, - fn(str: String) { - case str { - "@" as l <> rest -> Ok(#(l, rest)) - _ -> Error(Nil) - } - }, - ], - Error(Nil), - get_parser_fn(str), - ) -} - -fn parse_reg_name(str: String) { - // can't error - - case do_parse_reg_name(str, "") { - Error(Nil) -> Ok(#("", str)) - Ok(#(reg_name, rest)) -> Ok(#(reg_name, rest)) - } -} - -fn do_parse_reg_name(str: String, reg_name: String) { - case - list.fold_until( - [parse_unreserved, parse_pct_encoded, parse_sub_delim], - Error(Nil), - get_parser_fn(str), - ) - { - Error(Nil) | Ok(#("", _)) -> Ok(#(reg_name, str)) - Ok(#(l, rest)) -> do_parse_reg_name(rest, reg_name <> l) - } -} - -fn parse_pct_encoded(str: String) { - case str { - "%" <> rest -> { - use #(hex1, rest) <- result.try(parse_hex_digit(rest)) - use #(hex2, rest) <- result.try(parse_hex_digit(rest)) - - Ok(#("%" <> hex1 <> hex2, rest)) - } - _ -> Error(Nil) - } -} - -fn parse_sub_delim(str: String) { - case str { - "!" as l <> rest - | "$" as l <> rest - | "&" as l <> rest - | "'" as l <> rest - | "(" as l <> rest - | ")" as l <> rest - | "*" as l <> rest - | "+" as l <> rest - | "," as l <> rest - | ";" as l <> rest - | "=" as l <> rest -> Ok(#(l, rest)) - _ -> Error(Nil) - } -} - -fn parse_ipv4(str: String) { - use #(oct1, rest) <- result.try(parse_dec_octet(str)) - use rest <- result.try(case rest { - "." <> rest -> Ok(rest) - _ -> Error(Nil) - }) - use #(oct2, rest) <- result.try(parse_dec_octet(rest)) - use rest <- result.try(case rest { - "." <> rest -> Ok(rest) - _ -> Error(Nil) - }) - use #(oct3, rest) <- result.try(parse_dec_octet(rest)) - use rest <- result.try(case rest { - "." <> rest -> Ok(rest) - _ -> Error(Nil) - }) - use #(oct4, rest) <- result.try(parse_dec_octet(rest)) - Ok(#(oct1 <> "." <> oct2 <> "." <> oct3 <> "." <> oct4, rest)) -} - -fn parse_dec_octet(str: String) -> Result(#(String, String), Nil) { - let matches = [ - ["2", "5", "012345"], - ["2", "01234", "0123456789"], - ["1", "0123456789", "0123456789"], - ["123456789", "0123456789"], - ["0123456789"], - ] - - list.fold_until(matches, Error(Nil), fn(_, chars) { - case - list.fold_until(chars, #("", str), fn(acc, charset) { - let #(octet, str) = acc - case string.pop_grapheme(str) { - Error(_) -> Stop(#("", "")) - Ok(#(char, rest)) -> { - case string.contains(charset, char) { - True -> Continue(#(octet <> char, rest)) - False -> Stop(#("", "")) + Error(_) -> #("", after) + } } + Error(_) -> #("", after) } + do_normalise_percent(percent_splitter, rest, res <> before <> pc_val) } - }) - { - #("", _) -> Continue(Error(Nil)) - #(octet, rest) -> Stop(Ok(#(octet, rest))) - } - }) -} - -fn parse_userinfo( - str: String, - userinfo: String, -) -> Result(#(String, String), Nil) { - case str { - "@" <> rest -> Ok(#(userinfo, rest)) - "" -> Error(Nil) - _ -> { - use #(part, rest) <- result.try(list.fold_until( - [ - parse_unreserved, - parse_pct_encoded, - parse_sub_delim, - fn(str: String) { - case str { - ":" as l <> rest -> Ok(#(l, rest)) - _ -> Error(Nil) - } - }, - ], - Error(Nil), - get_parser_fn(str), - )) - parse_userinfo(rest, userinfo <> part) - } - } -} - -fn parse_scheme(str: String) -> Result(#(Uri, String), Nil) { - case parse_alpha(str) { - Ok(#(first, rest)) -> { - case do_parse_scheme(rest, first) { - Error(_) -> Error(Nil) - Ok(#(scheme, rest)) -> - Ok(#(Uri(Some(scheme), None, None, None, "", None, None), rest)) } } - _ -> Error(Nil) } } -fn do_parse_scheme( - str: String, - scheme: String, -) -> Result(#(String, String), Nil) { - case str { - ":" <> rest -> Ok(#(scheme, rest)) - "" -> Error(Nil) - _ -> { - use #(part, rest) <- result.try(list.fold_until( - [ - parse_alpha, - parse_digit, - fn(str) { - case str { - "+" as l <> rest | "-" as l <> rest | "." as l <> rest -> - Ok(#(l, rest)) - _ -> Error(Nil) - } - }, - ], - Error(Nil), - get_parser_fn(str), - )) - do_parse_scheme(rest, scheme <> part) - } - } -} - -fn get_parser_fn( - str: String, -) -> fn(a, fn(String) -> Result(b, c)) -> list.ContinueOrStop(Result(b, Nil)) { - fn(_, parse_fn) { - case parse_fn(str) { - Ok(r) -> Stop(Ok(r)) - Error(_) -> Continue(Error(Nil)) - } - } -} - -fn parse_min_max(str, min, max, parse_fn) { - use <- bool.guard(when: min < 0 || max <= 0 || min > max, return: Error(Nil)) - case - list.repeat("", max) - |> list.fold_until(Ok(#("", str, 0)), fn(acc, _) { - let assert Ok(#(hex, str, i)) = acc - case parse_fn(str) { - Error(_) -> - case i < min { - True -> Stop(Error(Nil)) - False -> Stop(Ok(#(hex, str, i))) - } - Ok(#(l, rest)) -> Continue(Ok(#(hex <> l, rest, i + 1))) - } - }) - { - Error(_) -> Error(Nil) - Ok(#(hex, str, _)) -> Ok(#(hex, str)) - } -} - -fn parse_hex_digits(str, min, max) { - parse_min_max(str, min, max, parse_hex_digit) -} - -fn parse_hex_digit(str) { - case str { - "0" as l <> rest - | "1" as l <> rest - | "2" as l <> rest - | "3" as l <> rest - | "4" as l <> rest - | "5" as l <> rest - | "6" as l <> rest - | "7" as l <> rest - | "8" as l <> rest - | "9" as l <> rest - | "a" as l <> rest - | "b" as l <> rest - | "c" as l <> rest - | "d" as l <> rest - | "e" as l <> rest - | "f" as l <> rest - | "A" as l <> rest - | "B" as l <> rest - | "C" as l <> rest - | "D" as l <> rest - | "E" as l <> rest - | "F" as l <> rest -> Ok(#(l, rest)) - _ -> Error(Nil) - } -} - -fn parse_digit(str: String) -> Result(#(String, String), Nil) { - case str { - "0" as l <> rest - | "1" as l <> rest - | "2" as l <> rest - | "3" as l <> rest - | "4" as l <> rest - | "5" as l <> rest - | "6" as l <> rest - | "7" as l <> rest - | "8" as l <> rest - | "9" as l <> rest -> Ok(#(l, rest)) - _ -> Error(Nil) - } -} - -fn parse_alpha(str: String) -> Result(#(String, String), Nil) { - case str { - "a" as l <> rest - | "b" as l <> rest - | "c" as l <> rest - | "d" as l <> rest - | "e" as l <> rest - | "f" as l <> rest - | "g" as l <> rest - | "h" as l <> rest - | "i" as l <> rest - | "j" as l <> rest - | "k" as l <> rest - | "l" as l <> rest - | "m" as l <> rest - | "n" as l <> rest - | "o" as l <> rest - | "p" as l <> rest - | "q" as l <> rest - | "r" as l <> rest - | "s" as l <> rest - | "t" as l <> rest - | "u" as l <> rest - | "v" as l <> rest - | "w" as l <> rest - | "x" as l <> rest - | "y" as l <> rest - | "z" as l <> rest - | "A" as l <> rest - | "B" as l <> rest - | "C" as l <> rest - | "D" as l <> rest - | "E" as l <> rest - | "F" as l <> rest - | "G" as l <> rest - | "H" as l <> rest - | "I" as l <> rest - | "J" as l <> rest - | "K" as l <> rest - | "L" as l <> rest - | "M" as l <> rest - | "N" as l <> rest - | "O" as l <> rest - | "P" as l <> rest - | "Q" as l <> rest - | "R" as l <> rest - | "S" as l <> rest - | "T" as l <> rest - | "U" as l <> rest - | "V" as l <> rest - | "W" as l <> rest - | "X" as l <> rest - | "Y" as l <> rest - | "Z" as l <> rest -> Ok(#(l, rest)) - _ -> Error(Nil) - } -} - -fn parse_unreserved(str: String) -> Result(#(String, String), Nil) { - list.fold_until( - [ - parse_alpha, - parse_digit, - fn(str) { - case str { - "_" as l <> rest - | "-" as l <> rest - | "." as l <> rest - | "~" as l <> rest -> Ok(#(l, rest)) - _ -> Error(Nil) +fn unescape_percent(str: String) -> String { + case int.base_parse(str, 16) { + Error(_) -> str + Ok(ascii) -> { + case ascii { + 45 + | 46 + | 95 + | 126 + | 48 + | 49 + | 50 + | 51 + | 52 + | 53 + | 54 + | 55 + | 56 + | 57 + | 65 + | 66 + | 67 + | 68 + | 69 + | 70 + | 71 + | 72 + | 73 + | 74 + | 75 + | 76 + | 77 + | 78 + | 79 + | 80 + | 81 + | 82 + | 83 + | 84 + | 85 + | 86 + | 87 + | 88 + | 89 + | 90 + | 97 + | 98 + | 99 + | 100 + | 101 + | 102 + | 103 + | 104 + | 105 + | 106 + | 107 + | 108 + | 109 + | 110 + | 111 + | 112 + | 113 + | 114 + | 115 + | 116 + | 117 + | 118 + | 119 + | 120 + | 121 + | 122 -> { + let assert Ok(cpnt) = string.utf_codepoint(ascii) + string.from_utf_codepoints([cpnt]) } - }, - ], - Error(Nil), - get_parser_fn(str), - ) + _ -> str + } + } + } } -fn combine_uris(uris: List(Uri)) -> Uri { - list.fold(uris, Uri(None, None, None, None, "", None, None), fn(acc, uri) { - let acc = case uri { - Uri(Some(scheme), _, _, _, _, _, _) -> Uri(..acc, scheme: Some(scheme)) - _ -> acc - } - let acc = case uri { - Uri(_, Some(userinfo), _, _, _, _, _) -> - Uri(..acc, userinfo: Some(userinfo)) - _ -> acc - } - let acc = case uri { - Uri(_, _, Some(host), _, _, _, _) -> Uri(..acc, host: Some(host)) - _ -> acc - } - let acc = case uri { - Uri(_, _, _, Some(port), _, _, _) -> Uri(..acc, port: Some(port)) - _ -> acc - } - let acc = case uri { - Uri(_, _, _, _, path, _, _) if path != "" -> Uri(..acc, path: path) - _ -> acc - } - let acc = case uri { - Uri(_, _, _, _, _, Some(query), _) -> Uri(..acc, query: Some(query)) - _ -> acc - } - case uri { - Uri(_, _, _, _, _, _, Some(fragment)) -> - Uri(..acc, fragment: Some(fragment)) - _ -> acc - } - }) +pub fn are_equivalent(uri1: Uri, uri2: Uri) { + use <- bool.guard(when: uri1 == uri2, return: True) + + let uri1 = normalise(uri1) + let uri2 = normalise(uri2) + + use <- bool.guard(when: uri1 == uri2, return: True) + + False } pub fn to_uri(uri: Uri) -> uri.Uri { diff --git a/test/uri_test.gleam b/test/uri_test.gleam index 7e38d0e..2506a5f 100644 --- a/test/uri_test.gleam +++ b/test/uri_test.gleam @@ -1,7 +1,8 @@ import gleam/option.{None, Some} import gleeunit/should import startest.{describe, it} -import uri.{Uri} +import types.{Uri} +import uri pub fn main() { startest.run(startest.default_config()) @@ -10,26 +11,26 @@ pub fn main() { pub fn parse_scheme_tests() { describe("scheme parsing", [ it("simple parse", fn() { - uri.parse("") |> should.equal(Ok(uri.empty_uri)) + uri.parse("") |> should.equal(Ok(types.empty_uri)) uri.parse("foo") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "foo"))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "foo"))) uri.parse("foo:") - |> should.equal(Ok(Uri(..uri.empty_uri, scheme: Some("foo")))) + |> should.equal(Ok(Uri(..types.empty_uri, scheme: Some("foo")))) uri.parse("foo:bar:nisse") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("foo"), path: "bar:nisse"), + Uri(..types.empty_uri, scheme: Some("foo"), path: "bar:nisse"), )) uri.parse("foo://") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("foo"), host: Some("")), + Uri(..types.empty_uri, scheme: Some("foo"), host: Some("")), )) uri.parse("foo:///") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("foo"), host: Some(""), path: "/"), + Uri(..types.empty_uri, scheme: Some("foo"), host: Some(""), path: "/"), )) uri.parse("foo:////") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("foo"), host: Some(""), path: "//"), + Uri(..types.empty_uri, scheme: Some("foo"), host: Some(""), path: "//"), )) }), ]) @@ -40,34 +41,34 @@ pub fn parse_userinfo_tests() { it("simple parse", fn() { uri.parse("user:password@localhost") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("user"), path: "password@localhost"), + Uri(..types.empty_uri, scheme: Some("user"), path: "password@localhost"), )) uri.parse("user@") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "user@"))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "user@"))) uri.parse("/user@") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "/user@"))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "/user@"))) uri.parse("user@localhost") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "user@localhost"))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "user@localhost"))) uri.parse("//user@localhost") |> should.equal(Ok( - Uri(..uri.empty_uri, userinfo: Some("user"), host: Some("localhost")), + Uri(..types.empty_uri, userinfo: Some("user"), host: Some("localhost")), )) uri.parse("//user:password@localhost") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, userinfo: Some("user:password"), host: Some("localhost"), ), )) uri.parse("foo:/user@") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("foo"), path: "/user@"), + Uri(..types.empty_uri, scheme: Some("foo"), path: "/user@"), )) uri.parse("foo://user@localhost") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), userinfo: Some("user"), host: Some("localhost"), @@ -76,7 +77,7 @@ pub fn parse_userinfo_tests() { uri.parse("foo://user:password@localhost") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), userinfo: Some("user:password"), host: Some("localhost"), @@ -87,27 +88,27 @@ pub fn parse_userinfo_tests() { uri.parse("user:%E5%90%88@%E6%B0%97%E9%81%93") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("user"), path: "%E5%90%88@%E6%B0%97%E9%81%93", ), )) uri.parse("%E5%90%88%E6%B0%97%E9%81%93@") |> should.equal(Ok( - Uri(..uri.empty_uri, path: "%E5%90%88%E6%B0%97%E9%81%93@"), + Uri(..types.empty_uri, path: "%E5%90%88%E6%B0%97%E9%81%93@"), )) uri.parse("/%E5%90%88%E6%B0%97%E9%81%93@") |> should.equal(Ok( - Uri(..uri.empty_uri, path: "/%E5%90%88%E6%B0%97%E9%81%93@"), + Uri(..types.empty_uri, path: "/%E5%90%88%E6%B0%97%E9%81%93@"), )) uri.parse("%E5%90%88@%E6%B0%97%E9%81%93") |> should.equal(Ok( - Uri(..uri.empty_uri, path: "%E5%90%88@%E6%B0%97%E9%81%93"), + Uri(..types.empty_uri, path: "%E5%90%88@%E6%B0%97%E9%81%93"), )) uri.parse("//%E5%90%88@%E6%B0%97%E9%81%93") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some("%E6%B0%97%E9%81%93"), userinfo: Some("%E5%90%88"), ), @@ -115,7 +116,7 @@ pub fn parse_userinfo_tests() { uri.parse("//%E5%90%88:%E6%B0%97@%E9%81%93") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some("%E9%81%93"), userinfo: Some("%E5%90%88:%E6%B0%97"), ), @@ -123,7 +124,7 @@ pub fn parse_userinfo_tests() { uri.parse("foo:/%E5%90%88%E6%B0%97%E9%81%93@") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), path: "/%E5%90%88%E6%B0%97%E9%81%93@", ), @@ -131,7 +132,7 @@ pub fn parse_userinfo_tests() { uri.parse("foo://%E5%90%88@%E6%B0%97%E9%81%93") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), userinfo: Some("%E5%90%88"), host: Some("%E6%B0%97%E9%81%93"), @@ -140,7 +141,7 @@ pub fn parse_userinfo_tests() { uri.parse("foo://%E5%90%88:%E6%B0%97@%E9%81%93") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), userinfo: Some("%E5%90%88:%E6%B0%97"), host: Some("%E9%81%93"), @@ -156,15 +157,15 @@ pub fn parse_host_tests() { describe("host parsing", [ it("simple parse", fn() { uri.parse("//hostname") - |> should.equal(Ok(Uri(..uri.empty_uri, host: Some("hostname")))) + |> should.equal(Ok(Uri(..types.empty_uri, host: Some("hostname")))) uri.parse("foo://hostname") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("foo"), host: Some("hostname")), + Uri(..types.empty_uri, scheme: Some("foo"), host: Some("hostname")), )) uri.parse("foo://user@hostname") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), userinfo: Some("user"), host: Some("hostname"), @@ -173,54 +174,58 @@ pub fn parse_host_tests() { }), it("ipv4 parse", fn() { uri.parse("//127.0.0.1") - |> should.equal(Ok(Uri(..uri.empty_uri, host: Some("127.0.0.1")))) + |> should.equal(Ok(Uri(..types.empty_uri, host: Some("127.0.0.1")))) uri.parse("//127.0.0.1/over/there") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some("127.0.0.1"), path: "/over/there"), + Uri(..types.empty_uri, host: Some("127.0.0.1"), path: "/over/there"), )) uri.parse("//127.0.0.1?name=ferret") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some("127.0.0.1"), query: Some("name=ferret"), ), )) uri.parse("//127.0.0.1#nose") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some("127.0.0.1"), fragment: Some("nose")), + Uri(..types.empty_uri, host: Some("127.0.0.1"), fragment: Some("nose")), )) uri.parse("//127.0.0.x") - |> should.equal(Ok(Uri(..uri.empty_uri, host: Some("127.0.0.x")))) + |> should.equal(Ok(Uri(..types.empty_uri, host: Some("127.0.0.x")))) uri.parse("//1227.0.0.1") - |> should.equal(Ok(Uri(..uri.empty_uri, host: Some("1227.0.0.1")))) + |> should.equal(Ok(Uri(..types.empty_uri, host: Some("1227.0.0.1")))) }), it("ipv6 parse", fn() { uri.parse("//[::127.0.0.1]") - |> should.equal(Ok(Uri(..uri.empty_uri, host: Some("::127.0.0.1")))) + |> should.equal(Ok(Uri(..types.empty_uri, host: Some("::127.0.0.1")))) uri.parse("//[2001:0db8:0000:0000:0000:0000:1428:07ab]") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some("2001:0db8:0000:0000:0000:0000:1428:07ab"), ), )) uri.parse("//[::127.0.0.1]/over/there") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some("::127.0.0.1"), path: "/over/there"), + Uri(..types.empty_uri, host: Some("::127.0.0.1"), path: "/over/there"), )) uri.parse("//[::127.0.0.1]?name=ferret") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some("::127.0.0.1"), query: Some("name=ferret"), ), )) uri.parse("//[::127.0.0.1]#nose") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some("::127.0.0.1"), fragment: Some("nose")), + Uri( + ..types.empty_uri, + host: Some("::127.0.0.1"), + fragment: Some("nose"), + ), )) uri.parse("//[::127.0.0.x]") |> should.be_error @@ -235,21 +240,23 @@ pub fn parse_port_tests() { describe("port parsing", [ it("simple parse", fn() { uri.parse("/:8042") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "/:8042"))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "/:8042"))) uri.parse("//:8042") - |> should.equal(Ok(Uri(..uri.empty_uri, host: Some(""), port: Some(8042)))) + |> should.equal(Ok( + Uri(..types.empty_uri, host: Some(""), port: Some(8042)), + )) uri.parse("//example.com:8042") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some("example.com"), port: Some(8042)), + Uri(..types.empty_uri, host: Some("example.com"), port: Some(8042)), )) uri.parse("foo:/:8042") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("foo"), path: "/:8042"), + Uri(..types.empty_uri, scheme: Some("foo"), path: "/:8042"), )) uri.parse("foo://:8042") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some(""), port: Some(8042), @@ -258,7 +265,7 @@ pub fn parse_port_tests() { uri.parse("foo://example.com:8042") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some("example.com"), port: Some(8042), @@ -269,23 +276,25 @@ pub fn parse_port_tests() { }), it("undefined port", fn() { uri.parse("/:") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "/:"))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "/:"))) uri.parse("//:") - |> should.equal(Ok(Uri(..uri.empty_uri, host: Some(""), port: None))) + |> should.equal(Ok(Uri(..types.empty_uri, host: Some(""), port: None))) uri.parse("//example.com:") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some("example.com"), port: None), + Uri(..types.empty_uri, host: Some("example.com"), port: None), )) uri.parse("foo:/:") - |> should.equal(Ok(Uri(..uri.empty_uri, scheme: Some("foo"), path: "/:"))) + |> should.equal(Ok( + Uri(..types.empty_uri, scheme: Some("foo"), path: "/:"), + )) uri.parse("foo://:") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("foo"), host: Some(""), port: None), + Uri(..types.empty_uri, scheme: Some("foo"), host: Some(""), port: None), )) uri.parse("foo://example.com:") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some("example.com"), port: None, @@ -301,17 +310,17 @@ pub fn parse_path_tests() { describe("path parsing", [ it("simple parse", fn() { uri.parse("over/there") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "over/there"))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "over/there"))) uri.parse("/over/there") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "/over/there"))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "/over/there"))) uri.parse("foo:/over/there") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("foo"), path: "/over/there"), + Uri(..types.empty_uri, scheme: Some("foo"), path: "/over/there"), )) uri.parse("foo://example.com/over/there") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some("example.com"), path: "/over/there", @@ -320,7 +329,7 @@ pub fn parse_path_tests() { uri.parse("foo://example.com:8042/over/there") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some("example.com"), port: Some(8042), @@ -336,12 +345,12 @@ pub fn parse_query_tests() { it("simple parse", fn() { uri.parse("foo:?name=ferret") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("foo"), query: Some("name=ferret")), + Uri(..types.empty_uri, scheme: Some("foo"), query: Some("name=ferret")), )) uri.parse("foo:over/there?name=ferret") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), path: "over/there", query: Some("name=ferret"), @@ -350,7 +359,7 @@ pub fn parse_query_tests() { uri.parse("foo:/over/there?name=ferret") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), path: "/over/there", query: Some("name=ferret"), @@ -359,7 +368,7 @@ pub fn parse_query_tests() { uri.parse("foo://example.com?name=ferret") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some("example.com"), query: Some("name=ferret"), @@ -368,7 +377,7 @@ pub fn parse_query_tests() { uri.parse("foo://example.com/?name=ferret") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some("example.com"), path: "/", @@ -378,24 +387,24 @@ pub fn parse_query_tests() { uri.parse("?name=ferret") |> should.equal(Ok( - Uri(..uri.empty_uri, path: "", query: Some("name=ferret")), + Uri(..types.empty_uri, path: "", query: Some("name=ferret")), )) uri.parse("over/there?name=ferret") |> should.equal(Ok( - Uri(..uri.empty_uri, path: "over/there", query: Some("name=ferret")), + Uri(..types.empty_uri, path: "over/there", query: Some("name=ferret")), )) uri.parse("/?name=ferret") |> should.equal(Ok( - Uri(..uri.empty_uri, path: "/", query: Some("name=ferret")), + Uri(..types.empty_uri, path: "/", query: Some("name=ferret")), )) uri.parse("/over/there?name=ferret") |> should.equal(Ok( - Uri(..uri.empty_uri, path: "/over/there", query: Some("name=ferret")), + Uri(..types.empty_uri, path: "/over/there", query: Some("name=ferret")), )) uri.parse("//example.com?name=ferret") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some("example.com"), query: Some("name=ferret"), ), @@ -403,7 +412,7 @@ pub fn parse_query_tests() { uri.parse("//example.com/?name=ferret") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some("example.com"), path: "/", query: Some("name=ferret"), @@ -414,7 +423,7 @@ pub fn parse_query_tests() { uri.parse("foo://example.com/?name=%E5%90%88%E6%B0%97%E9%81%93") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some("example.com"), path: "/", @@ -424,7 +433,7 @@ pub fn parse_query_tests() { uri.parse("//example.com/?name=%E5%90%88%E6%B0%97%E9%81%93") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some("example.com"), path: "/", query: Some("name=%E5%90%88%E6%B0%97%E9%81%93"), @@ -439,12 +448,12 @@ pub fn parse_fragment_tests() { it("simple parse", fn() { uri.parse("foo:#nose") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("foo"), fragment: Some("nose")), + Uri(..types.empty_uri, scheme: Some("foo"), fragment: Some("nose")), )) uri.parse("foo:over/there#nose") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), path: "over/there", fragment: Some("nose"), @@ -453,7 +462,7 @@ pub fn parse_fragment_tests() { uri.parse("foo:/over/there#nose") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), path: "/over/there", fragment: Some("nose"), @@ -462,7 +471,7 @@ pub fn parse_fragment_tests() { uri.parse("foo://example.com#nose") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some("example.com"), fragment: Some("nose"), @@ -471,7 +480,7 @@ pub fn parse_fragment_tests() { uri.parse("foo://example.com/#nose") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some("example.com"), path: "/", @@ -481,7 +490,7 @@ pub fn parse_fragment_tests() { uri.parse("foo://example.com#nose") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some("example.com"), fragment: Some("nose"), @@ -489,27 +498,31 @@ pub fn parse_fragment_tests() { )) uri.parse("#nose") - |> should.equal(Ok(Uri(..uri.empty_uri, fragment: Some("nose")))) + |> should.equal(Ok(Uri(..types.empty_uri, fragment: Some("nose")))) uri.parse("over/there#nose") |> should.equal(Ok( - Uri(..uri.empty_uri, path: "over/there", fragment: Some("nose")), + Uri(..types.empty_uri, path: "over/there", fragment: Some("nose")), )) uri.parse("/#nose") |> should.equal(Ok( - Uri(..uri.empty_uri, path: "/", fragment: Some("nose")), + Uri(..types.empty_uri, path: "/", fragment: Some("nose")), )) uri.parse("/over/there#nose") |> should.equal(Ok( - Uri(..uri.empty_uri, path: "/over/there", fragment: Some("nose")), + Uri(..types.empty_uri, path: "/over/there", fragment: Some("nose")), )) uri.parse("//example.com#nose") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some("example.com"), fragment: Some("nose")), + Uri( + ..types.empty_uri, + host: Some("example.com"), + fragment: Some("nose"), + ), )) uri.parse("//example.com/#nose") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some("example.com"), path: "/", fragment: Some("nose"), @@ -520,7 +533,7 @@ pub fn parse_fragment_tests() { uri.parse("foo://example.com#%E5%90%88%E6%B0%97%E9%81%93") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some("example.com"), fragment: Some("%E5%90%88%E6%B0%97%E9%81%93"), @@ -529,7 +542,7 @@ pub fn parse_fragment_tests() { uri.parse("//example.com/#%E5%90%88%E6%B0%97%E9%81%93") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some("example.com"), path: "/", fragment: Some("%E5%90%88%E6%B0%97%E9%81%93"), @@ -543,15 +556,17 @@ fn parse_special_tests() { describe("special parsing", [ it("special 1", fn() { uri.parse("//?") - |> should.equal(Ok(Uri(..uri.empty_uri, host: Some(""), query: Some("")))) + |> should.equal(Ok( + Uri(..types.empty_uri, host: Some(""), query: Some("")), + )) uri.parse("//#") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some(""), fragment: Some("")), + Uri(..types.empty_uri, host: Some(""), fragment: Some("")), )) uri.parse("//?#") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some(""), query: Some(""), fragment: Some(""), @@ -560,7 +575,7 @@ fn parse_special_tests() { uri.parse("foo://?") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some(""), query: Some(""), @@ -569,7 +584,7 @@ fn parse_special_tests() { uri.parse("foo://#") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some(""), fragment: Some(""), @@ -578,7 +593,7 @@ fn parse_special_tests() { uri.parse("foo://?#") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some(""), query: Some(""), @@ -586,23 +601,23 @@ fn parse_special_tests() { ), )) uri.parse("///") - |> should.equal(Ok(Uri(..uri.empty_uri, host: Some(""), path: "/"))) + |> should.equal(Ok(Uri(..types.empty_uri, host: Some(""), path: "/"))) uri.parse("///hostname") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some(""), path: "/hostname"), + Uri(..types.empty_uri, host: Some(""), path: "/hostname"), )) uri.parse("///?") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some(""), path: "/", query: Some("")), + Uri(..types.empty_uri, host: Some(""), path: "/", query: Some("")), )) uri.parse("///#") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some(""), path: "/", fragment: Some("")), + Uri(..types.empty_uri, host: Some(""), path: "/", fragment: Some("")), )) uri.parse("///?#") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some(""), path: "/", query: Some(""), @@ -611,27 +626,27 @@ fn parse_special_tests() { )) uri.parse("//foo?") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some("foo"), query: Some("")), + Uri(..types.empty_uri, host: Some("foo"), query: Some("")), )) uri.parse("//foo#") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some("foo"), fragment: Some("")), + Uri(..types.empty_uri, host: Some("foo"), fragment: Some("")), )) uri.parse("//foo?#") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, host: Some("foo"), query: Some(""), fragment: Some(""), ), )) uri.parse("//foo/") - |> should.equal(Ok(Uri(..uri.empty_uri, host: Some("foo"), path: "/"))) + |> should.equal(Ok(Uri(..types.empty_uri, host: Some("foo"), path: "/"))) uri.parse("http://foo?") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("http"), host: Some("foo"), query: Some(""), @@ -640,7 +655,7 @@ fn parse_special_tests() { uri.parse("http://foo#") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("http"), host: Some("foo"), fragment: Some(""), @@ -649,7 +664,7 @@ fn parse_special_tests() { uri.parse("http://foo?#") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("http"), host: Some("foo"), query: Some(""), @@ -658,12 +673,17 @@ fn parse_special_tests() { )) uri.parse("http://foo/") |> should.equal(Ok( - Uri(..uri.empty_uri, scheme: Some("http"), host: Some("foo"), path: "/"), + Uri( + ..types.empty_uri, + scheme: Some("http"), + host: Some("foo"), + path: "/", + ), )) uri.parse("http://foo:80?") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("http"), host: Some("foo"), port: Some(80), @@ -673,7 +693,7 @@ fn parse_special_tests() { uri.parse("http://foo:80#") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("http"), host: Some("foo"), port: Some(80), @@ -683,7 +703,7 @@ fn parse_special_tests() { uri.parse("http://foo:80?#") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("http"), host: Some("foo"), port: Some(80), @@ -694,7 +714,7 @@ fn parse_special_tests() { uri.parse("http://foo:80/") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("http"), host: Some("foo"), port: Some(80), @@ -702,23 +722,23 @@ fn parse_special_tests() { ), )) uri.parse("?") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "", query: Some("")))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "", query: Some("")))) uri.parse("??") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "", query: Some("?")))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "", query: Some("?")))) uri.parse("???") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "", query: Some("??")))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "", query: Some("??")))) uri.parse("#") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "", fragment: Some("")))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "", fragment: Some("")))) uri.parse("##") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "", fragment: Some("#")))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "", fragment: Some("#")))) uri.parse("###") - |> should.equal(Ok(Uri(..uri.empty_uri, path: "", fragment: Some("##")))) + |> should.equal(Ok(Uri(..types.empty_uri, path: "", fragment: Some("##")))) }), it("special 2", fn() { uri.parse("a://:1/") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("a"), host: Some(""), port: Some(1), @@ -726,15 +746,15 @@ fn parse_special_tests() { ), )) uri.parse("a:/a/") - |> should.equal(Ok(Uri(..uri.empty_uri, scheme: Some("a"), path: "/a/"))) + |> should.equal(Ok(Uri(..types.empty_uri, scheme: Some("a"), path: "/a/"))) uri.parse("//@") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some(""), path: "", userinfo: Some("")), + Uri(..types.empty_uri, host: Some(""), path: "", userinfo: Some("")), )) uri.parse("foo://@") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some(""), path: "", @@ -743,12 +763,12 @@ fn parse_special_tests() { )) uri.parse("//@/") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some(""), path: "/", userinfo: Some("")), + Uri(..types.empty_uri, host: Some(""), path: "/", userinfo: Some("")), )) uri.parse("foo://@/") |> should.equal(Ok( Uri( - ..uri.empty_uri, + ..types.empty_uri, scheme: Some("foo"), host: Some(""), path: "/", @@ -757,11 +777,11 @@ fn parse_special_tests() { )) uri.parse("//localhost:/") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some("localhost"), path: "/"), + Uri(..types.empty_uri, host: Some("localhost"), path: "/"), )) uri.parse("//:") |> should.equal(Ok( - Uri(..uri.empty_uri, host: Some("localhost"), path: ""), + Uri(..types.empty_uri, host: Some("localhost"), path: ""), )) }), ]) @@ -777,6 +797,8 @@ pub fn parse_failure_tests() { uri.parse("//host/path#foƶ") |> should.be_error uri.parse("//[:::127.0.0.1]") |> should.be_error uri.parse("//localhost:A8") |> should.be_error + uri.parse("http://f%ff%%ff/") |> should.be_error + uri.parse("http://f%ff%fr/") |> should.be_error }), ]) }