feat: More work on uri api
This commit is contained in:
505
src/internal/utils.gleam
Normal file
505
src/internal/utils.gleam
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
import gleam/bool
|
||||||
|
import gleam/int
|
||||||
|
import gleam/list
|
||||||
|
import gleam/option.{None, Some}
|
||||||
|
import gleam/result
|
||||||
|
import gleam/string
|
||||||
|
import internal/parser
|
||||||
|
import splitter.{type Splitter}
|
||||||
|
import types.{type Uri, Uri}
|
||||||
|
|
||||||
|
pub fn merge(base: Uri, relative: Uri) -> Result(Uri, Nil) {
|
||||||
|
use <- bool.guard(when: base.scheme == None, return: Error(Nil))
|
||||||
|
let uri = case relative.scheme {
|
||||||
|
Some(_) -> {
|
||||||
|
Uri(..relative, path: remove_dot_segments(relative.path))
|
||||||
|
}
|
||||||
|
None -> {
|
||||||
|
let scheme = base.scheme
|
||||||
|
case relative.host, relative.port, relative.userinfo {
|
||||||
|
Some(_), _, _ | _, Some(_), _ | _, _, Some(_) -> {
|
||||||
|
Uri(..relative, scheme:, path: remove_dot_segments(relative.path))
|
||||||
|
}
|
||||||
|
_, _, _ -> {
|
||||||
|
case relative.path {
|
||||||
|
"" -> {
|
||||||
|
let query = case relative.query {
|
||||||
|
Some(_) -> relative.query
|
||||||
|
_ -> base.query
|
||||||
|
}
|
||||||
|
Uri(..base, query:)
|
||||||
|
}
|
||||||
|
"/" <> _ -> {
|
||||||
|
Uri(
|
||||||
|
..base,
|
||||||
|
path: remove_dot_segments(relative.path),
|
||||||
|
query: relative.query,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ -> {
|
||||||
|
let path = merge_paths(base, relative)
|
||||||
|
Uri(
|
||||||
|
..base,
|
||||||
|
path: remove_dot_segments(path),
|
||||||
|
query: relative.query,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri(..uri, fragment: relative.fragment) |> Ok
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_authority(uri: Uri) -> Bool {
|
||||||
|
case uri.host {
|
||||||
|
Some(_) -> True
|
||||||
|
_ -> False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_paths(base: Uri, relative: Uri) -> String {
|
||||||
|
case has_authority(base), base.path {
|
||||||
|
True, "" -> "/" <> relative.path
|
||||||
|
_, _ -> {
|
||||||
|
remove_segment(base.path) <> "/" <> relative.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 |> remove_dot_segments
|
||||||
|
let query = uri.query |> option.map(percent_normaliser)
|
||||||
|
let fragment = uri.fragment |> option.map(percent_normaliser)
|
||||||
|
|
||||||
|
Uri(scheme, userinfo, host, port, path, query, fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_dot_segments(path: String) -> String {
|
||||||
|
do_remove_dot_segments(path, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_remove_dot_segments(path: String, acc: String) -> String {
|
||||||
|
case path {
|
||||||
|
"../" <> rest | "./" <> rest -> do_remove_dot_segments(rest, acc)
|
||||||
|
"/./" <> rest -> do_remove_dot_segments("/" <> rest, acc)
|
||||||
|
"/." -> acc <> "/"
|
||||||
|
"/../" <> rest -> do_remove_dot_segments("/" <> rest, remove_segment(acc))
|
||||||
|
"/.." -> remove_segment(acc) <> "/"
|
||||||
|
"." | ".." | "" -> acc <> path
|
||||||
|
_ -> {
|
||||||
|
let assert Ok(#(char, rest)) = string.pop_grapheme(path)
|
||||||
|
do_remove_dot_segments(rest, acc <> char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalise_percent(percent_splitter: Splitter, str: String) -> String {
|
||||||
|
do_normalise_percent(percent_splitter, str, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 -> #(string.lowercase(v), rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Error(_) -> #("", after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Error(_) -> #("", after)
|
||||||
|
}
|
||||||
|
do_normalise_percent(percent_splitter, rest, res <> before <> pc_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unescape_percent(str: String) -> String {
|
||||||
|
case int.base_parse(str, 16) {
|
||||||
|
Error(_) -> str
|
||||||
|
Ok(ascii) -> {
|
||||||
|
case is_unreserved_char(ascii) {
|
||||||
|
True -> {
|
||||||
|
let assert Ok(cpnt) = string.utf_codepoint(ascii)
|
||||||
|
string.from_utf_codepoints([cpnt])
|
||||||
|
}
|
||||||
|
False -> str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encoding_not_needed(i: Int) -> Bool {
|
||||||
|
// $-_.+!*'()
|
||||||
|
case i {
|
||||||
|
36 | 45 | 95 | 46 | 43 | 33 | 42 | 39 | 40 | 41 -> True
|
||||||
|
_ -> False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_unreserved_char(i: Int) -> Bool {
|
||||||
|
case i {
|
||||||
|
45 | 46 | 95 | 126 -> True
|
||||||
|
_ if i >= 48 && i <= 57 -> True
|
||||||
|
_ if i >= 65 && i <= 90 -> True
|
||||||
|
_ if i >= 97 && i <= 122 -> True
|
||||||
|
_ -> False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn percent_decode(str: String) -> Result(String, Nil) {
|
||||||
|
let percent_splitter = splitter.new(["%"])
|
||||||
|
do_percent_decode(percent_splitter, str, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_percent_decode(
|
||||||
|
splitter: splitter.Splitter,
|
||||||
|
str: String,
|
||||||
|
acc: String,
|
||||||
|
) -> Result(String, Nil) {
|
||||||
|
case splitter.split(splitter, str) {
|
||||||
|
#(before, "", "") -> Ok(acc <> before)
|
||||||
|
#(before, "%", after) -> {
|
||||||
|
use #(hd1, rest) <- result.try(parser.parse_hex_digit(after))
|
||||||
|
use #(hd2, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
|
||||||
|
use char <- result.try(int.base_parse(hd1 <> hd2, 16))
|
||||||
|
case int.bitwise_and(char, 128) {
|
||||||
|
0 -> {
|
||||||
|
use char <- result.try(string.utf_codepoint(char))
|
||||||
|
do_percent_decode(
|
||||||
|
splitter,
|
||||||
|
rest,
|
||||||
|
acc <> before <> string.from_utf_codepoints([char]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ -> {
|
||||||
|
case int.bitwise_and(char, 224) {
|
||||||
|
192 -> {
|
||||||
|
use #(char, rest) <- result.try(decode_2byte_utf(hd1 <> hd2, rest))
|
||||||
|
|
||||||
|
do_percent_decode(splitter, rest, acc <> before <> char)
|
||||||
|
}
|
||||||
|
_ -> {
|
||||||
|
case int.bitwise_and(char, 240) {
|
||||||
|
224 -> {
|
||||||
|
use #(char, rest) <- result.try(decode_3byte_utf(
|
||||||
|
hd1 <> hd2,
|
||||||
|
rest,
|
||||||
|
))
|
||||||
|
|
||||||
|
do_percent_decode(splitter, rest, acc <> before <> char)
|
||||||
|
}
|
||||||
|
_ -> {
|
||||||
|
case int.bitwise_and(char, 248) {
|
||||||
|
240 -> {
|
||||||
|
use #(char, rest) <- result.try(decode_4byte_utf(
|
||||||
|
hd1 <> hd2,
|
||||||
|
rest,
|
||||||
|
))
|
||||||
|
|
||||||
|
do_percent_decode(splitter, rest, acc <> before <> char)
|
||||||
|
}
|
||||||
|
_ -> Error(Nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ -> Error(Nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_3byte_utf(
|
||||||
|
first_byte: String,
|
||||||
|
rest: String,
|
||||||
|
) -> Result(#(String, String), Nil) {
|
||||||
|
use rest <- result.try(case rest {
|
||||||
|
"%" <> rest -> Ok(rest)
|
||||||
|
_ -> Error(Nil)
|
||||||
|
})
|
||||||
|
use #(hd3, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
use #(hd4, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
use rest <- result.try(case rest {
|
||||||
|
"%" <> rest -> Ok(rest)
|
||||||
|
_ -> Error(Nil)
|
||||||
|
})
|
||||||
|
use #(hd5, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
use #(hd6, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
|
||||||
|
use bytes <- result.try(int.base_parse(
|
||||||
|
first_byte <> hd3 <> hd4 <> hd5 <> hd6,
|
||||||
|
16,
|
||||||
|
))
|
||||||
|
|
||||||
|
let assert <<
|
||||||
|
_:size(4),
|
||||||
|
w:size(4),
|
||||||
|
_:size(2),
|
||||||
|
x:size(4),
|
||||||
|
y1:size(2),
|
||||||
|
_:size(2),
|
||||||
|
y2:size(2),
|
||||||
|
z:size(4),
|
||||||
|
>> = <<bytes:size(24)>>
|
||||||
|
let assert <<i:size(16)>> = <<
|
||||||
|
w:size(4),
|
||||||
|
x:size(4),
|
||||||
|
y1:size(2),
|
||||||
|
y2:size(2),
|
||||||
|
z:size(4),
|
||||||
|
>>
|
||||||
|
|
||||||
|
use res <- result.try(string.utf_codepoint(i))
|
||||||
|
|
||||||
|
Ok(#(string.from_utf_codepoints([res]), rest))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_2byte_utf(
|
||||||
|
first_byte: String,
|
||||||
|
rest: String,
|
||||||
|
) -> Result(#(String, String), Nil) {
|
||||||
|
use rest <- result.try(case rest {
|
||||||
|
"%" <> rest -> Ok(rest)
|
||||||
|
_ -> Error(Nil)
|
||||||
|
})
|
||||||
|
use #(hd3, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
use #(hd4, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
|
||||||
|
use bytes <- result.try(int.base_parse(first_byte <> hd3 <> hd4, 16))
|
||||||
|
let assert <<
|
||||||
|
_:size(3),
|
||||||
|
x:size(3),
|
||||||
|
y1:size(2),
|
||||||
|
_:size(2),
|
||||||
|
y2:size(2),
|
||||||
|
z:size(4),
|
||||||
|
>> = <<bytes:size(16)>>
|
||||||
|
let assert <<i:size(16)>> = <<
|
||||||
|
0:size(5),
|
||||||
|
x:size(3),
|
||||||
|
y1:size(2),
|
||||||
|
y2:size(2),
|
||||||
|
z:size(4),
|
||||||
|
>>
|
||||||
|
|
||||||
|
use res <- result.try(string.utf_codepoint(i))
|
||||||
|
|
||||||
|
Ok(#(string.from_utf_codepoints([res]), rest))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_4byte_utf(
|
||||||
|
first_byte: String,
|
||||||
|
rest: String,
|
||||||
|
) -> Result(#(String, String), Nil) {
|
||||||
|
use rest <- result.try(case rest {
|
||||||
|
"%" <> rest -> Ok(rest)
|
||||||
|
_ -> Error(Nil)
|
||||||
|
})
|
||||||
|
use #(hd3, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
use #(hd4, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
use rest <- result.try(case rest {
|
||||||
|
"%" <> rest -> Ok(rest)
|
||||||
|
_ -> Error(Nil)
|
||||||
|
})
|
||||||
|
use #(hd5, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
use #(hd6, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
use rest <- result.try(case rest {
|
||||||
|
"%" <> rest -> Ok(rest)
|
||||||
|
_ -> Error(Nil)
|
||||||
|
})
|
||||||
|
use #(hd7, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
use #(hd8, rest) <- result.try(parser.parse_hex_digit(rest))
|
||||||
|
|
||||||
|
use bytes <- result.try(int.base_parse(
|
||||||
|
first_byte <> hd3 <> hd4 <> hd5 <> hd6 <> hd7 <> hd8,
|
||||||
|
16,
|
||||||
|
))
|
||||||
|
|
||||||
|
let assert <<
|
||||||
|
_:size(5),
|
||||||
|
u:size(1),
|
||||||
|
v1:size(2),
|
||||||
|
_:size(2),
|
||||||
|
v2:size(2),
|
||||||
|
w:size(4),
|
||||||
|
_:size(2),
|
||||||
|
x:size(4),
|
||||||
|
y1:size(2),
|
||||||
|
_:size(2),
|
||||||
|
y2:size(2),
|
||||||
|
z:size(4),
|
||||||
|
>> = <<bytes:size(32)>>
|
||||||
|
let assert <<i:size(24)>> = <<
|
||||||
|
0:size(3),
|
||||||
|
u:size(1),
|
||||||
|
v1:size(2),
|
||||||
|
v2:size(2),
|
||||||
|
w:size(4),
|
||||||
|
x:size(4),
|
||||||
|
y1:size(2),
|
||||||
|
y2:size(2),
|
||||||
|
z:size(4),
|
||||||
|
>>
|
||||||
|
|
||||||
|
use res <- result.try(string.utf_codepoint(i))
|
||||||
|
|
||||||
|
Ok(#(string.from_utf_codepoints([res]), rest))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn do_percent_encode(str: String) -> String {
|
||||||
|
string.to_utf_codepoints(str)
|
||||||
|
|> list.map(string.utf_codepoint_to_int)
|
||||||
|
|> list.map(encode_codepoint)
|
||||||
|
|> string.concat
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_codepoint(codepoint: Int) -> String {
|
||||||
|
case codepoint <= 127 {
|
||||||
|
True -> {
|
||||||
|
case is_unreserved_char(codepoint) || encoding_not_needed(codepoint) {
|
||||||
|
True -> {
|
||||||
|
let assert Ok(cpnt) = string.utf_codepoint(codepoint)
|
||||||
|
string.from_utf_codepoints([cpnt])
|
||||||
|
}
|
||||||
|
False -> {
|
||||||
|
"%" <> int.to_base16(codepoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
False -> {
|
||||||
|
case codepoint <= 2047 {
|
||||||
|
True -> {
|
||||||
|
let assert <<_:size(5), x:size(3), y1:size(2), y2:size(2), z:size(4)>> = <<
|
||||||
|
codepoint:size(16),
|
||||||
|
>>
|
||||||
|
let res = <<
|
||||||
|
6:size(3),
|
||||||
|
x:size(3),
|
||||||
|
y1:size(2),
|
||||||
|
2:size(2),
|
||||||
|
y2:size(2),
|
||||||
|
z:size(4),
|
||||||
|
>>
|
||||||
|
let assert <<b1:size(8), b2:size(8)>> = res
|
||||||
|
"%" <> int.to_base16(b1) <> "%" <> int.to_base16(b2)
|
||||||
|
}
|
||||||
|
False -> {
|
||||||
|
case codepoint <= 65_535 {
|
||||||
|
True -> {
|
||||||
|
let assert <<
|
||||||
|
w:size(4),
|
||||||
|
x:size(4),
|
||||||
|
y1:size(2),
|
||||||
|
y2:size(2),
|
||||||
|
z:size(4),
|
||||||
|
>> = <<
|
||||||
|
codepoint:size(16),
|
||||||
|
>>
|
||||||
|
let res = <<
|
||||||
|
14:size(4),
|
||||||
|
w:size(4),
|
||||||
|
2:size(2),
|
||||||
|
x:size(4),
|
||||||
|
y1:size(2),
|
||||||
|
2:size(2),
|
||||||
|
y2:size(2),
|
||||||
|
z:size(4),
|
||||||
|
>>
|
||||||
|
let assert <<b1:size(8), b2:size(8), b3:size(8)>> = res
|
||||||
|
"%"
|
||||||
|
<> int.to_base16(b1)
|
||||||
|
<> "%"
|
||||||
|
<> int.to_base16(b2)
|
||||||
|
<> "%"
|
||||||
|
<> int.to_base16(b3)
|
||||||
|
}
|
||||||
|
False -> {
|
||||||
|
let assert <<
|
||||||
|
_:size(3),
|
||||||
|
u:size(1),
|
||||||
|
v1:size(2),
|
||||||
|
v2:size(2),
|
||||||
|
w:size(4),
|
||||||
|
x:size(4),
|
||||||
|
y1:size(2),
|
||||||
|
y2:size(2),
|
||||||
|
z:size(4),
|
||||||
|
>> = <<codepoint:size(24)>>
|
||||||
|
let res = <<
|
||||||
|
30:size(5),
|
||||||
|
u:size(1),
|
||||||
|
v1:size(2),
|
||||||
|
2:size(2),
|
||||||
|
v2:size(2),
|
||||||
|
w:size(4),
|
||||||
|
2:size(2),
|
||||||
|
x:size(4),
|
||||||
|
y1:size(2),
|
||||||
|
2:size(2),
|
||||||
|
y2:size(2),
|
||||||
|
z:size(4),
|
||||||
|
>>
|
||||||
|
|
||||||
|
let assert <<b1:size(8), b2:size(8), b3:size(8), b4:size(8)>> =
|
||||||
|
res
|
||||||
|
"%"
|
||||||
|
<> int.to_base16(b1)
|
||||||
|
<> "%"
|
||||||
|
<> int.to_base16(b2)
|
||||||
|
<> "%"
|
||||||
|
<> int.to_base16(b3)
|
||||||
|
<> "%"
|
||||||
|
<> int.to_base16(b4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
309
src/uri.gleam
309
src/uri.gleam
@@ -1,10 +1,9 @@
|
|||||||
import gleam/bool
|
import gleam/bool
|
||||||
import gleam/int
|
import gleam/int
|
||||||
import gleam/option.{None, Some}
|
import gleam/option.{None, Some}
|
||||||
import gleam/string
|
|
||||||
import gleam/uri
|
import gleam/uri
|
||||||
import internal/parser
|
import internal/parser
|
||||||
import splitter.{type Splitter}
|
import internal/utils
|
||||||
import types.{type Uri, Uri}
|
import types.{type Uri, Uri}
|
||||||
|
|
||||||
pub fn parse(uri: String) -> Result(Uri, Nil) {
|
pub fn parse(uri: String) -> Result(Uri, Nil) {
|
||||||
@@ -12,92 +11,47 @@ pub fn parse(uri: String) -> Result(Uri, Nil) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_string(uri: Uri) -> String {
|
pub fn to_string(uri: Uri) -> String {
|
||||||
let parts = case uri.fragment {
|
let uri_string = case uri.scheme {
|
||||||
Some(fragment) -> ["#", fragment]
|
Some(scheme) -> scheme <> ":"
|
||||||
None -> []
|
_ -> ""
|
||||||
}
|
}
|
||||||
let parts = case uri.query {
|
let uri_string = case uri.host {
|
||||||
Some(query) -> ["?", query, ..parts]
|
Some(_) -> {
|
||||||
None -> parts
|
uri_string
|
||||||
|
<> "//"
|
||||||
|
<> case uri.userinfo {
|
||||||
|
Some(userinfo) -> userinfo <> "@"
|
||||||
|
_ -> ""
|
||||||
|
}
|
||||||
|
<> case uri.host {
|
||||||
|
Some(host) -> host
|
||||||
|
_ -> ""
|
||||||
|
}
|
||||||
|
<> case uri.port {
|
||||||
|
Some(port) -> ":" <> int.to_string(port)
|
||||||
|
_ -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ -> uri_string
|
||||||
}
|
}
|
||||||
let parts = [uri.path, ..parts]
|
let uri_string = uri_string <> uri.path
|
||||||
let parts = case uri.host, string.starts_with(uri.path, "/") {
|
let uri_string =
|
||||||
Some(host), False if host != "" -> ["/", ..parts]
|
uri_string
|
||||||
_, _ -> parts
|
<> case uri.query {
|
||||||
}
|
Some(query) -> "?" <> query
|
||||||
let parts = case uri.host, uri.port {
|
_ -> ""
|
||||||
Some(_), Some(port) -> [":", int.to_string(port), ..parts]
|
}
|
||||||
_, _ -> parts
|
let uri_string =
|
||||||
}
|
uri_string
|
||||||
let parts = case uri.scheme, uri.userinfo, uri.host {
|
<> case uri.fragment {
|
||||||
Some(s), Some(u), Some(h) -> [s, "://", u, "@", h, ..parts]
|
Some(fragment) -> "#" <> fragment
|
||||||
Some(s), None, Some(h) -> [s, "://", h, ..parts]
|
_ -> ""
|
||||||
Some(s), Some(_), None | Some(s), None, None -> [s, ":", ..parts]
|
}
|
||||||
None, None, Some(h) -> ["//", h, ..parts]
|
uri_string
|
||||||
_, _, _ -> parts
|
|
||||||
}
|
|
||||||
string.concat(parts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn merge(base: Uri, relative: Uri) -> Result(Uri, Nil) {
|
pub fn merge(base: Uri, relative: Uri) -> Result(Uri, Nil) {
|
||||||
use <- bool.guard(when: base.scheme == None, return: Error(Nil))
|
utils.merge(base, relative)
|
||||||
let uri = case relative.scheme {
|
|
||||||
Some(_) -> {
|
|
||||||
Uri(..relative, path: remove_dot_segments(relative.path))
|
|
||||||
}
|
|
||||||
None -> {
|
|
||||||
let scheme = base.scheme
|
|
||||||
case relative.host, relative.port, relative.userinfo {
|
|
||||||
Some(_), _, _ | _, Some(_), _ | _, _, Some(_) -> {
|
|
||||||
Uri(..relative, scheme:, path: remove_dot_segments(relative.path))
|
|
||||||
}
|
|
||||||
_, _, _ -> {
|
|
||||||
case relative.path {
|
|
||||||
"" -> {
|
|
||||||
let query = case relative.query {
|
|
||||||
Some(_) -> relative.query
|
|
||||||
_ -> base.query
|
|
||||||
}
|
|
||||||
Uri(..base, query:)
|
|
||||||
}
|
|
||||||
"/" <> _ -> {
|
|
||||||
Uri(
|
|
||||||
..base,
|
|
||||||
path: remove_dot_segments(relative.path),
|
|
||||||
query: relative.query,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_ -> {
|
|
||||||
let path = merge_paths(base, relative)
|
|
||||||
Uri(
|
|
||||||
..base,
|
|
||||||
path: remove_dot_segments(path),
|
|
||||||
query: relative.query,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Uri(..uri, fragment: relative.fragment) |> Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_authority(uri: Uri) -> Bool {
|
|
||||||
case uri.host, uri.userinfo, uri.port {
|
|
||||||
Some(_), _, _ | _, Some(_), _ | _, _, Some(_) -> True
|
|
||||||
_, _, _ -> False
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_paths(base: Uri, relative: Uri) -> String {
|
|
||||||
case has_authority(base), base.path {
|
|
||||||
True, "" -> "/" <> relative.path
|
|
||||||
_, _ -> {
|
|
||||||
remove_segment(base.path) <> "/" <> relative.path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normalize(uri: Uri) -> Uri {
|
pub fn normalize(uri: Uri) -> Uri {
|
||||||
@@ -105,170 +59,7 @@ pub fn normalize(uri: Uri) -> Uri {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn normalise(uri: Uri) -> Uri {
|
pub fn normalise(uri: Uri) -> Uri {
|
||||||
let percent_splitter = splitter.new(["%"])
|
utils.normalise(uri)
|
||||||
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 |> remove_dot_segments
|
|
||||||
let query = uri.query |> option.map(percent_normaliser)
|
|
||||||
let fragment = uri.fragment |> option.map(percent_normaliser)
|
|
||||||
|
|
||||||
Uri(scheme, userinfo, host, port, path, query, fragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_dot_segments(path: String) -> String {
|
|
||||||
do_remove_dot_segments(path, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_remove_dot_segments(path: String, acc: String) -> String {
|
|
||||||
case path {
|
|
||||||
"../" <> rest | "./" <> rest -> do_remove_dot_segments(rest, acc)
|
|
||||||
"/./" <> rest -> do_remove_dot_segments("/" <> rest, acc)
|
|
||||||
"/." -> acc <> "/"
|
|
||||||
"/../" <> rest -> do_remove_dot_segments("/" <> rest, remove_segment(acc))
|
|
||||||
"/.." -> remove_segment(acc) <> "/"
|
|
||||||
"." | ".." | "" -> acc <> path
|
|
||||||
_ -> {
|
|
||||||
let assert Ok(#(char, rest)) = string.pop_grapheme(path)
|
|
||||||
do_remove_dot_segments(rest, acc <> char)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalise_percent(percent_splitter: Splitter, str: String) -> String {
|
|
||||||
do_normalise_percent(percent_splitter, str, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
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(_) -> #("", after)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Error(_) -> #("", after)
|
|
||||||
}
|
|
||||||
do_normalise_percent(percent_splitter, rest, res <> before <> pc_val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
_ -> str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn are_equivalent(uri1: Uri, uri2: Uri) {
|
pub fn are_equivalent(uri1: Uri, uri2: Uri) {
|
||||||
@@ -277,9 +68,7 @@ pub fn are_equivalent(uri1: Uri, uri2: Uri) {
|
|||||||
let uri1 = normalise(uri1)
|
let uri1 = normalise(uri1)
|
||||||
let uri2 = normalise(uri2)
|
let uri2 = normalise(uri2)
|
||||||
|
|
||||||
use <- bool.guard(when: uri1 == uri2, return: True)
|
uri1 == uri2
|
||||||
|
|
||||||
False
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_uri(uri: Uri) -> uri.Uri {
|
pub fn to_uri(uri: Uri) -> uri.Uri {
|
||||||
@@ -305,3 +94,23 @@ pub fn from_uri(uri: uri.Uri) -> Uri {
|
|||||||
uri.fragment,
|
uri.fragment,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn percent_decode(value: String) -> Result(String, Nil) {
|
||||||
|
utils.percent_decode(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn percent_encode(value: String) -> String {
|
||||||
|
utils.do_percent_encode(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_to_string(query: List(#(String, String))) -> String {
|
||||||
|
todo
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_query(query: String) -> Result(List(#(String, String)), Nil) {
|
||||||
|
todo
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn origin(uri: Uri) -> Result(String, Nil) {
|
||||||
|
todo
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import gleam/list
|
||||||
import gleam/option.{None, Some}
|
import gleam/option.{None, Some}
|
||||||
import gleeunit/should
|
import gleeunit/should
|
||||||
import startest.{describe, it}
|
import startest.{describe, it}
|
||||||
@@ -1094,6 +1095,117 @@ pub fn normalise_tests() {
|
|||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_string_tests() {
|
||||||
|
describe("to_string test", [
|
||||||
|
it("simple test", fn() {
|
||||||
|
let test_uri =
|
||||||
|
types.Uri(
|
||||||
|
Some("https"),
|
||||||
|
Some("weebl:bob"),
|
||||||
|
Some("example.com"),
|
||||||
|
Some(1234),
|
||||||
|
"/path",
|
||||||
|
Some("query=true"),
|
||||||
|
Some("fragment"),
|
||||||
|
)
|
||||||
|
uri.to_string(test_uri)
|
||||||
|
|> should.equal(
|
||||||
|
"https://weebl:bob@example.com:1234/path?query=true#fragment",
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
it("path only", fn() {
|
||||||
|
types.Uri(..types.empty_uri, path: "/")
|
||||||
|
|> uri.to_string
|
||||||
|
|> should.equal("/")
|
||||||
|
types.Uri(..types.empty_uri, path: "/blah")
|
||||||
|
|> uri.to_string
|
||||||
|
|> should.equal("/blah")
|
||||||
|
types.Uri(..types.empty_uri, userinfo: Some("user"), path: "/blah")
|
||||||
|
|> uri.to_string
|
||||||
|
|> should.equal("/blah")
|
||||||
|
types.Uri(..types.empty_uri, path: "")
|
||||||
|
|> uri.to_string
|
||||||
|
|> should.equal("")
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn equivalence_tests() {
|
||||||
|
describe("equivalence tests", [
|
||||||
|
it("equal", fn() {
|
||||||
|
let uri1 = uri.parse("http://example.com") |> should.be_ok
|
||||||
|
let uri2 = uri.parse("HTTP://EXAMPLE.COM") |> should.be_ok
|
||||||
|
uri.are_equivalent(uri1, uri2) |> should.be_true
|
||||||
|
|
||||||
|
let uri1 = uri.parse("http://example.com") |> should.be_ok
|
||||||
|
let uri2 = uri.parse("HTTP://EX%41MPLE.COM") |> should.be_ok |> echo
|
||||||
|
uri.are_equivalent(uri1, uri2) |> should.be_true
|
||||||
|
|
||||||
|
let uri1 = uri.parse("http://example.com/a/b/c") |> should.be_ok
|
||||||
|
let uri2 =
|
||||||
|
uri.parse("HTTP://EXaMPLE.COM/a/d/../b/e/../c") |> should.be_ok |> echo
|
||||||
|
uri.are_equivalent(uri1, uri2) |> should.be_true
|
||||||
|
|
||||||
|
let uri1 = uri.parse("http://example.com/a/b/c") |> should.be_ok
|
||||||
|
let uri2 =
|
||||||
|
uri.parse("HTTP://EXaMPLE.COM/a/../../../../a/b/e/../c")
|
||||||
|
|> should.be_ok
|
||||||
|
|> echo
|
||||||
|
uri.are_equivalent(uri1, uri2) |> should.be_true
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn percent_encode_tests() {
|
||||||
|
describe("percent encoding", [
|
||||||
|
it("encoding", fn() {
|
||||||
|
percent_codec_fixtures
|
||||||
|
|> list.map(fn(t) {
|
||||||
|
let #(a, b) = t
|
||||||
|
uri.percent_encode(a)
|
||||||
|
|> should.equal(b)
|
||||||
|
})
|
||||||
|
Nil
|
||||||
|
}),
|
||||||
|
it("decoding", fn() {
|
||||||
|
percent_codec_fixtures
|
||||||
|
|> list.map(fn(t) {
|
||||||
|
let #(a, b) = t
|
||||||
|
uri.percent_decode(b)
|
||||||
|
|> should.equal(Ok(a))
|
||||||
|
})
|
||||||
|
Nil
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const percent_codec_fixtures = [
|
||||||
|
#(" ", "%20"),
|
||||||
|
#(",", "%2C"),
|
||||||
|
#(";", "%3B"),
|
||||||
|
#(":", "%3A"),
|
||||||
|
#("!", "!"),
|
||||||
|
#("?", "%3F"),
|
||||||
|
#("'", "'"),
|
||||||
|
#("(", "("),
|
||||||
|
#(")", ")"),
|
||||||
|
#("[", "%5B"),
|
||||||
|
#("@", "%40"),
|
||||||
|
#("/", "%2F"),
|
||||||
|
#("\\", "%5C"),
|
||||||
|
#("&", "%26"),
|
||||||
|
#("#", "%23"),
|
||||||
|
#("=", "%3D"),
|
||||||
|
#("~", "~"),
|
||||||
|
#("ñ", "%C3%B1"),
|
||||||
|
#("-", "-"),
|
||||||
|
#("_", "_"),
|
||||||
|
#(".", "."),
|
||||||
|
#("*", "*"),
|
||||||
|
#("+", "+"),
|
||||||
|
#("100% great+fun", "100%25%20great+fun"),
|
||||||
|
]
|
||||||
// gleeunit test functions end in `_test`
|
// gleeunit test functions end in `_test`
|
||||||
// pub fn uri_test() {
|
// pub fn uri_test() {
|
||||||
// match("uri:")
|
// match("uri:")
|
||||||
|
|||||||
Reference in New Issue
Block a user