diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4a7fe22 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "27.1.2" + gleam-version: "1.12.0" + rebar3-version: "3" + # elixir-version: "1" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/README.md b/README.md index 4b8ec27..c4a2d34 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,27 @@ + # uri + Uri (RFC 3986) library for Gleam + +[![Package Version](https://img.shields.io/hexpm/v/uri)](https://hex.pm/packages/uri) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/uri/) + +```sh +gleam add uri@1 +``` +```gleam +import uri + +pub fn main() -> Nil { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..038605f --- /dev/null +++ b/gleam.toml @@ -0,0 +1,20 @@ +name = "uri" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" +startest = ">= 0.7.0 and < 1.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..44a039b --- /dev/null +++ b/manifest.toml @@ -0,0 +1,32 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "bigben", version = "1.0.1", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "bigben", source = "hex", outer_checksum = "190E489610A80D76C48BACC75EB8314BD184FF0220AB0F251ABE760B993B91BB" }, + { name = "birl", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_regexp", "gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "2AC7BA26F998E3DFADDB657148BD5DDFE966958AD4D6D6957DD0D22E5B56C400" }, + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, + { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, + { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, + { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, + { name = "gleam_otp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7987CBEBC8060B88F14575DEF546253F3116EBE2A5DA6FD82F38243FCE97C54B" }, + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, + { name = "gleam_stdlib", version = "0.62.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "0080706D3A5A9A36C40C68481D1D231D243AF602E6D2A2BE67BA8F8F4DFF45EC" }, + { name = "gleam_time", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "DCDDC040CE97DA3D2A925CDBBA08D8A78681139745754A83998641C8A3F6587E" }, + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, + { name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" }, + { name = "glint", version = "1.2.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "2214C7CEFDE457CEE62140C3D4899B964E05236DA74E4243DFADF4AF29C382BB" }, + { 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 = "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" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +startest = { version = ">= 0.7.0 and < 1.0.0" } diff --git a/src/uri.gleam b/src/uri.gleam new file mode 100644 index 0000000..2d6a805 --- /dev/null +++ b/src/uri.gleam @@ -0,0 +1,733 @@ +import gleam/bool +import gleam/int +import gleam/list.{Continue, Stop} +import gleam/option.{type Option, None, Some} +import gleam/result +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) + +pub fn main() { + // parse("test://asd@test!te%20ste/a/b/c?a=31#asd") |> echo + // parse("test:/blah") |> echo + parse("foo://user:password@localhost") |> echo + uri.parse("foo://user:password@localhost") |> echo + // uri.parse("../../") |> echo +} + +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 -> { + use #(seg1, rest) <- result.try(do_parse_segment_nz(rest)) + + 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)) + } + _ -> 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 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) { + 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(#("", "")) + } + } + } + }) + { + #("", _) -> 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_hex_digits(str, min, max) { + 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_hex_digit(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_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 + } + }) +} + +pub fn to_uri(uri: Uri) -> uri.Uri { + uri.Uri( + uri.scheme, + uri.userinfo, + uri.host, + uri.port, + uri.path, + uri.query, + uri.fragment, + ) +} + +pub fn from_uri(uri: uri.Uri) -> Uri { + Uri( + uri.scheme, + uri.userinfo, + uri.host, + uri.port, + uri.path, + uri.query, + uri.fragment, + ) +} diff --git a/test/uri_test.gleam b/test/uri_test.gleam new file mode 100644 index 0000000..7a49a77 --- /dev/null +++ b/test/uri_test.gleam @@ -0,0 +1,103 @@ +import gleam/option.{Some} +import gleeunit/should +import startest.{describe, it} +import uri.{Uri} + +pub fn main() { + startest.run(startest.default_config()) +} + +pub fn parse_scheme_tests() { + describe("scheme parsing", [ + it("should parse", fn() { + uri.parse("") |> should.equal(Ok(uri.empty_uri)) + uri.parse("foo") + |> should.equal(Ok(Uri(..uri.empty_uri, path: "foo"))) + uri.parse("foo:") + |> should.equal(Ok(Uri(..uri.empty_uri, scheme: Some("foo")))) + uri.parse("foo:bar:nisse") + |> should.equal(Ok( + Uri(..uri.empty_uri, scheme: Some("foo"), path: "bar:nisse"), + )) + uri.parse("foo://") + |> should.equal(Ok( + Uri(..uri.empty_uri, scheme: Some("foo"), host: Some("")), + )) + uri.parse("foo:///") + |> should.equal(Ok( + Uri(..uri.empty_uri, scheme: Some("foo"), host: Some(""), path: "/"), + )) + uri.parse("foo:////") + |> should.equal(Ok( + Uri(..uri.empty_uri, scheme: Some("foo"), host: Some(""), path: "//"), + )) + }), + ]) +} + +pub fn parse_userinfo_tests() { + describe("userinfo parsing", [ + it("should parse", fn() { + uri.parse("user:password@localhost") + |> should.equal(Ok( + Uri(..uri.empty_uri, scheme: Some("user"), path: "password@localhost"), + )) + uri.parse("user@") + |> should.equal(Ok(Uri(..uri.empty_uri, path: "user@"))) + uri.parse("/user@") + |> should.equal(Ok(Uri(..uri.empty_uri, path: "/user@"))) + uri.parse("user@localhost") + |> should.equal(Ok(Uri(..uri.empty_uri, path: "user@localhost"))) + uri.parse("//user@localhost") + |> should.equal(Ok( + Uri(..uri.empty_uri, userinfo: Some("user"), host: Some("localhost")), + )) + uri.parse("//user:password@localhost") + |> should.equal(Ok( + Uri( + ..uri.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.parse("foo://user@localhost") + |> should.equal(Ok( + Uri( + ..uri.empty_uri, + scheme: Some("foo"), + userinfo: Some("user"), + host: Some("localhost"), + ), + )) + uri.parse("foo://user:password@localhost") + |> should.equal(Ok( + Uri( + ..uri.empty_uri, + scheme: Some("foo"), + userinfo: Some("user:password"), + host: Some("localhost"), + ), + )) + }), + ]) +} +// gleeunit test functions end in `_test` +// pub fn uri_test() { +// match("uri:") +// match("//@") +// match("//") +// match("") +// match("?") +// match("#") +// match("#\t") +// match("//:") +// } + +// fn match(uri: String) { +// assert uri.parse(uri) |> uri.to_uri +// == uri2.parse(uri) +// }