Skip to content

Commit 103b76f

Browse files
committed
Use Erlang uri parser
1 parent bc07680 commit 103b76f

File tree

3 files changed

+158
-142
lines changed

3 files changed

+158
-142
lines changed

src/gleam/uri.gleam

Lines changed: 114 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -50,144 +50,135 @@ pub fn parse(uri_string: String) -> Result(Uri, Nil) {
5050
do_parse(uri_string)
5151
}
5252

53-
fn do_parse(uri_string: String) -> Result(Uri, Nil) {
54-
// From https://tools.ietf.org/html/rfc3986#appendix-B
55-
let pattern =
56-
// 12 3 4 5 6 7 8
57-
"^(([a-z][a-z0-9\\+\\-\\.]*):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#.*)?"
58-
let matches =
59-
pattern
60-
|> regex_submatches(uri_string)
61-
|> pad_list(8)
62-
63-
let #(scheme, authority, path, query, fragment) = case matches {
64-
[
65-
_scheme_with_colon,
66-
scheme,
67-
authority_with_slashes,
68-
_authority,
69-
path,
70-
query_with_question_mark,
71-
_query,
72-
fragment,
73-
] -> #(
74-
scheme,
75-
authority_with_slashes,
76-
path,
77-
query_with_question_mark,
78-
fragment,
79-
)
80-
_ -> #(None, None, None, None, None)
81-
}
82-
83-
let scheme = noneify_empty_string(scheme)
84-
let path = option.unwrap(path, "")
85-
let query = noneify_query(query)
86-
let #(userinfo, host, port) = split_authority(authority)
87-
let fragment =
88-
fragment
89-
|> option.to_result(Nil)
90-
|> result.then(string.pop_grapheme)
91-
|> result.map(pair.second)
92-
|> option.from_result
93-
let port = case port {
94-
None -> default_port(scheme)
95-
_ -> port
96-
}
97-
let scheme =
98-
scheme
99-
|> noneify_empty_string
100-
|> option.map(string.lowercase)
101-
Ok(Uri(
102-
scheme: scheme,
103-
userinfo: userinfo,
104-
host: host,
105-
port: port,
106-
path: path,
107-
query: query,
108-
fragment: fragment,
109-
))
53+
if erlang {
54+
external fn do_parse(String) -> Result(Uri, Nil) =
55+
"gleam_stdlib" "uri_parse"
11056
}
11157

112-
fn default_port(scheme: Option(String)) -> Option(Int) {
113-
case scheme {
114-
Some("ftp") -> Some(21)
115-
Some("sftp") -> Some(22)
116-
Some("tftp") -> Some(69)
117-
Some("http") -> Some(80)
118-
Some("https") -> Some(443)
119-
Some("ldap") -> Some(389)
120-
_ -> None
58+
if javascript {
59+
fn do_parse(uri_string: String) -> Result(Uri, Nil) {
60+
// From https://tools.ietf.org/html/rfc3986#appendix-B
61+
let pattern =
62+
// 12 3 4 5 6 7 8
63+
"^(([a-z][a-z0-9\\+\\-\\.]*):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#.*)?"
64+
let matches =
65+
pattern
66+
|> regex_submatches(uri_string)
67+
|> pad_list(8)
68+
69+
let #(scheme, authority, path, query, fragment) = case matches {
70+
[
71+
_scheme_with_colon,
72+
scheme,
73+
authority_with_slashes,
74+
_authority,
75+
path,
76+
query_with_question_mark,
77+
_query,
78+
fragment,
79+
] -> #(
80+
scheme,
81+
authority_with_slashes,
82+
path,
83+
query_with_question_mark,
84+
fragment,
85+
)
86+
_ -> #(None, None, None, None, None)
87+
}
88+
89+
let scheme = noneify_empty_string(scheme)
90+
let path = option.unwrap(path, "")
91+
let query = noneify_query(query)
92+
let #(userinfo, host, port) = split_authority(authority)
93+
let fragment =
94+
fragment
95+
|> option.to_result(Nil)
96+
|> result.then(string.pop_grapheme)
97+
|> result.map(pair.second)
98+
|> option.from_result
99+
let scheme =
100+
scheme
101+
|> noneify_empty_string
102+
|> option.map(string.lowercase)
103+
Ok(Uri(
104+
scheme: scheme,
105+
userinfo: userinfo,
106+
host: host,
107+
port: port,
108+
path: path,
109+
query: query,
110+
fragment: fragment,
111+
))
121112
}
122-
}
123113

124-
fn regex_submatches(pattern: String, string: String) -> List(Option(String)) {
125-
pattern
126-
|> regex.compile(regex.Options(case_insensitive: True, multi_line: False))
127-
|> result.nil_error
128-
|> result.map(regex.scan(_, string))
129-
|> result.then(list.head)
130-
|> result.map(fn(m: regex.Match) { m.submatches })
131-
|> result.unwrap([])
132-
}
114+
fn regex_submatches(pattern: String, string: String) -> List(Option(String)) {
115+
pattern
116+
|> regex.compile(regex.Options(case_insensitive: True, multi_line: False))
117+
|> result.nil_error
118+
|> result.map(regex.scan(_, string))
119+
|> result.then(list.head)
120+
|> result.map(fn(m: regex.Match) { m.submatches })
121+
|> result.unwrap([])
122+
}
133123

134-
fn noneify_query(x: Option(String)) -> Option(String) {
135-
case x {
136-
None -> None
137-
Some(x) ->
138-
case string.pop_grapheme(x) {
139-
Ok(#("?", query)) -> Some(query)
140-
_ -> None
141-
}
124+
fn noneify_query(x: Option(String)) -> Option(String) {
125+
case x {
126+
None -> None
127+
Some(x) ->
128+
case string.pop_grapheme(x) {
129+
Ok(#("?", query)) -> Some(query)
130+
_ -> None
131+
}
132+
}
142133
}
143-
}
144134

145-
fn noneify_empty_string(x: Option(String)) -> Option(String) {
146-
case x {
147-
Some("") | None -> None
148-
Some(_) -> x
135+
fn noneify_empty_string(x: Option(String)) -> Option(String) {
136+
case x {
137+
Some("") | None -> None
138+
Some(_) -> x
139+
}
149140
}
150-
}
151141

152-
// Split an authority into its userinfo, host and port parts.
153-
fn split_authority(
154-
authority: Option(String),
155-
) -> #(Option(String), Option(String), Option(Int)) {
156-
case option.unwrap(authority, "") {
157-
"" -> #(None, None, None)
158-
"//" -> #(None, Some(""), None)
159-
authority -> {
160-
let matches =
161-
"^(//)?((.*)@)?(\\[[a-zA-Z0-9:.]*\\]|[^:]*)(:(\\d*))?"
162-
|> regex_submatches(authority)
163-
|> pad_list(6)
164-
case matches {
165-
[_, _, userinfo, host, _, port] -> {
166-
let userinfo = noneify_empty_string(userinfo)
167-
let host = noneify_empty_string(host)
168-
let port =
169-
port
170-
|> option.unwrap("")
171-
|> int.parse
172-
|> option.from_result
173-
#(userinfo, host, port)
142+
// Split an authority into its userinfo, host and port parts.
143+
fn split_authority(
144+
authority: Option(String),
145+
) -> #(Option(String), Option(String), Option(Int)) {
146+
case option.unwrap(authority, "") {
147+
"" -> #(None, None, None)
148+
"//" -> #(None, Some(""), None)
149+
authority -> {
150+
let matches =
151+
"^(//)?((.*)@)?(\\[[a-zA-Z0-9:.]*\\]|[^:]*)(:(\\d*))?"
152+
|> regex_submatches(authority)
153+
|> pad_list(6)
154+
case matches {
155+
[_, _, userinfo, host, _, port] -> {
156+
let userinfo = noneify_empty_string(userinfo)
157+
let host = noneify_empty_string(host)
158+
let port =
159+
port
160+
|> option.unwrap("")
161+
|> int.parse
162+
|> option.from_result
163+
#(userinfo, host, port)
164+
}
165+
_ -> #(None, None, None)
174166
}
175-
_ -> #(None, None, None)
176167
}
177168
}
178169
}
179-
}
180170

181-
fn pad_list(list: List(Option(a)), size: Int) -> List(Option(a)) {
182-
list
183-
|> list.append(list.repeat(None, extra_required(list, size)))
184-
}
171+
fn pad_list(list: List(Option(a)), size: Int) -> List(Option(a)) {
172+
list
173+
|> list.append(list.repeat(None, extra_required(list, size)))
174+
}
185175

186-
fn extra_required(list: List(a), remaining: Int) -> Int {
187-
case list {
188-
_ if remaining == 0 -> 0
189-
[] -> remaining
190-
[_, ..xs] -> extra_required(xs, remaining - 1)
176+
fn extra_required(list: List(a), remaining: Int) -> Int {
177+
case list {
178+
_ if remaining == 0 -> 0
179+
[] -> remaining
180+
[_, ..xs] -> extra_required(xs, remaining - 1)
181+
}
191182
}
192183
}
193184

src/gleam_stdlib.erl

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
decode_float/1, decode_thunk/1, decode_list/1, decode_option/2,
77
decode_field/2, parse_int/1, parse_float/1, less_than/2,
88
string_pop_grapheme/1, string_starts_with/2, wrap_list/1,
9-
string_ends_with/2, string_pad/4, decode_map/1,
9+
string_ends_with/2, string_pad/4, decode_map/1, uri_parse/1,
1010
bit_string_int_to_u32/1, bit_string_int_from_u32/1, decode_result/1,
1111
bit_string_slice/3, decode_bit_string/1, compile_regex/2, regex_scan/2,
1212
percent_encode/1, percent_decode/1, regex_check/2, regex_split/2,
@@ -298,3 +298,39 @@ check_utf8(Cs) ->
298298
{error, _, _} -> {error, nil};
299299
_ -> {ok, Cs}
300300
end.
301+
302+
uri_parse(String) ->
303+
case uri_string:parse(String) of
304+
{error, _, _} -> {error, nil};
305+
Uri ->
306+
% #{
307+
% host := Host, path := Path, port := Port, query := Query,
308+
% scheme := Scheme, userinfo := Userinfo
309+
% } ->
310+
% scheme: Option(String),
311+
% userinfo: Option(String),
312+
% host: Option(String),
313+
% port: Option(Int),
314+
% path: String,
315+
% query: Option(String),
316+
% fragment: Option(String),
317+
{ok, {uri,
318+
maps_get_optional(Uri, scheme),
319+
maps_get_optional(Uri, userinfo),
320+
maps_get_optional(Uri, host),
321+
maps_get_optional(Uri, port),
322+
maps_get_or(Uri, path, <<>>),
323+
maps_get_optional(Uri, query),
324+
maps_get_optional(Uri, fragment)
325+
}}
326+
end.
327+
328+
maps_get_optional(Map, Key) ->
329+
try {some, maps:get(Key, Map)}
330+
catch _:_ -> none
331+
end.
332+
333+
maps_get_or(Map, Key, Default) ->
334+
try maps:get(Key, Map)
335+
catch _:_ -> Default
336+
end.

test/gleam/uri_test.gleam

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,6 @@ pub fn parse_only_host_test() {
4141
should.equal(parsed.fragment, None)
4242
}
4343

44-
pub fn colon_uri_test() {
45-
assert Ok(parsed) = uri.parse("::")
46-
should.equal(parsed.scheme, None)
47-
should.equal(parsed.userinfo, None)
48-
should.equal(parsed.host, None)
49-
should.equal(parsed.port, None)
50-
should.equal(parsed.path, "::")
51-
should.equal(parsed.query, None)
52-
should.equal(parsed.fragment, None)
53-
}
54-
5544
pub fn parse_scheme_test() {
5645
uri.parse("http://one.com/path/to/something?one=two&two=one#fragment")
5746
|> should.equal(Ok(uri.Uri(
@@ -60,7 +49,7 @@ pub fn parse_scheme_test() {
6049
path: "/path/to/something",
6150
query: Some("one=two&two=one"),
6251
fragment: Some("fragment"),
63-
port: Some(80),
52+
port: None,
6453
userinfo: None,
6554
)))
6655
}
@@ -73,7 +62,7 @@ pub fn parse_https_scheme_test() {
7362
path: "",
7463
query: None,
7564
fragment: None,
76-
port: Some(443),
65+
port: None,
7766
userinfo: None,
7867
)))
7968
}
@@ -101,7 +90,7 @@ pub fn parse_ftp_scheme_test() {
10190
path: "/my_directory/my_file.txt",
10291
query: None,
10392
fragment: None,
104-
port: Some(21),
93+
port: None,
10594
)))
10695
}
10796

@@ -115,7 +104,7 @@ pub fn parse_sftp_scheme_test() {
115104
path: "/my_directory/my_file.txt",
116105
query: None,
117106
fragment: None,
118-
port: Some(22),
107+
port: None,
119108
)))
120109
}
121110

@@ -129,7 +118,7 @@ pub fn parse_tftp_scheme_test() {
129118
path: "/my_directory/my_file.txt",
130119
query: None,
131120
fragment: None,
132-
port: Some(69),
121+
port: None,
133122
)))
134123
}
135124

@@ -143,7 +132,7 @@ pub fn parse_ldap_scheme_test() {
143132
path: "/dc=example,dc=com",
144133
query: Some("?sub?(givenName=John)"),
145134
fragment: None,
146-
port: Some(389),
135+
port: None,
147136
)))
148137
}
149138

@@ -157,7 +146,7 @@ pub fn parse_ldap_2_scheme_test() {
157146
path: "/cn=John%20Doe,dc=foo,dc=com",
158147
query: None,
159148
fragment: None,
160-
port: Some(389),
149+
port: None,
161150
)))
162151
}
163152

0 commit comments

Comments
 (0)