Skip to content

Commit 369005e

Browse files
authored
Merge pull request #237 from izo0x90/Hristo/Parse-uri-queris-as-key-vals
Parse URI queries in to key value pairs
2 parents 997774c + 00906a6 commit 369005e

File tree

4 files changed

+103
-13
lines changed

4 files changed

+103
-13
lines changed

benchmark/bench.mojo

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,19 @@ fn lightbug_benchmark_request_parse(mut b: Bencher):
9999
fn lightbug_benchmark_request_encode(mut b: Bencher):
100100
@always_inline
101101
@parameter
102-
fn request_encode():
102+
fn request_encode() raises:
103+
var uri = URI.parse("http://127.0.0.1:8080/some-path")
103104
var req = HTTPRequest(
104-
URI.parse("http://127.0.0.1:8080/some-path"),
105-
headers=headers_struct,
106-
body=body_bytes,
105+
uri=uri,
106+
headers=headers_struct,
107+
body=body_bytes,
107108
)
108109
_ = encode(req^)
109-
110-
b.iter[request_encode]()
110+
111+
try:
112+
b.iter[request_encode]()
113+
except e:
114+
print("failed to encode request, error: ", e)
111115

112116

113117
@parameter

lightbug_http/client.mojo

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,10 @@ struct Client:
160160
raise Error("Client._handle_redirect: `Location` header was not received in the response.")
161161

162162
if new_location and new_location.startswith("http"):
163-
new_uri = URI.parse(new_location)
163+
try:
164+
new_uri = URI.parse(new_location)
165+
except e:
166+
raise Error("Client._handle_redirect: Failed to parse the new URI: " + str(e))
164167
original_req.headers[HeaderKey.HOST] = new_uri.host
165168
else:
166169
new_uri = original_req.uri

lightbug_http/uri.mojo

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import Dict
12
from utils import Variant
23
from lightbug_http.io.bytes import Bytes, bytes
34
from lightbug_http.strings import (
@@ -10,13 +11,28 @@ from lightbug_http.strings import (
1011
https,
1112
)
1213

14+
alias QueryMap = Dict[String, String]
15+
16+
17+
struct QueryDelimiters:
18+
alias STRING_START = "?"
19+
alias ITEM = "&"
20+
alias ITEM_ASSIGN = "="
21+
22+
23+
struct URIDelimiters:
24+
alias SCHEMA = "://"
25+
alias PATH = strSlash
26+
alias ROOT_PATH = strSlash
27+
1328

1429
@value
1530
struct URI(Writable, Stringable, Representable):
1631
var _original_path: String
1732
var scheme: String
1833
var path: String
1934
var query_string: String
35+
var queries: QueryMap
2036
var _hash: String
2137
var host: String
2238

@@ -27,11 +43,11 @@ struct URI(Writable, Stringable, Representable):
2743
var password: String
2844

2945
@staticmethod
30-
fn parse(uri: String) -> URI:
46+
fn parse(uri: String) raises -> URI:
3147
var proto_str = String(strHttp11)
3248
var is_https = False
3349

34-
var proto_end = uri.find("://")
50+
var proto_end = uri.find(URIDelimiters.SCHEMA)
3551
var remainder_uri: String
3652
if proto_end >= 0:
3753
proto_str = uri[:proto_end]
@@ -41,7 +57,7 @@ struct URI(Writable, Stringable, Representable):
4157
else:
4258
remainder_uri = uri
4359

44-
var path_start = remainder_uri.find("/")
60+
var path_start = remainder_uri.find(URIDelimiters.PATH)
4561
var host_and_port: String
4662
var request_uri: String
4763
var host: String
@@ -60,7 +76,7 @@ struct URI(Writable, Stringable, Representable):
6076
else:
6177
scheme = http
6278

63-
var n = request_uri.find("?")
79+
var n = request_uri.find(QueryDelimiters.STRING_START)
6480
var original_path: String
6581
var query_string: String
6682
if n >= 0:
@@ -70,11 +86,26 @@ struct URI(Writable, Stringable, Representable):
7086
original_path = request_uri
7187
query_string = ""
7288

89+
var queries = QueryMap()
90+
if query_string:
91+
var query_items = query_string.split(QueryDelimiters.ITEM)
92+
93+
for item in query_items:
94+
var key_val = item[].split(QueryDelimiters.ITEM_ASSIGN, 1)
95+
96+
if key_val[0]:
97+
queries[key_val[0]] = ""
98+
if len(key_val) == 2:
99+
# TODO: Query values are going to be URI encoded strings and should be decoded as part of the
100+
# query processing
101+
queries[key_val[0]] = key_val[1]
102+
73103
return URI(
74104
_original_path=original_path,
75105
scheme=scheme,
76106
path=original_path,
77107
query_string=query_string,
108+
queries=queries,
78109
_hash="",
79110
host=host,
80111
full_uri=uri,
@@ -84,9 +115,9 @@ struct URI(Writable, Stringable, Representable):
84115
)
85116

86117
fn __str__(self) -> String:
87-
var result = String.write(self.scheme, "://", self.host, self.path)
118+
var result = String.write(self.scheme, URIDelimiters.SCHEMA, self.host, self.path)
88119
if len(self.query_string) > 0:
89-
result.write("?", self.query_string)
120+
result.write(QueryDelimiters.STRING_START, self.query_string)
90121
return result^
91122

92123
fn __repr__(self) -> String:

tests/lightbug_http/test_uri.mojo

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,58 @@ def test_uri_parse_http_with_query_string():
8888
testing.assert_equal(uri._original_path, "/job")
8989
testing.assert_equal(uri.request_uri, "/job?title=engineer")
9090
testing.assert_equal(uri.query_string, "title=engineer")
91+
testing.assert_equal(uri.queries["title"], "engineer")
92+
93+
def test_uri_parse_multiple_query_parameters():
94+
var uri = URI.parse("http://example.com/search?q=python&page=1&limit=20")
95+
testing.assert_equal(uri.scheme, "http")
96+
testing.assert_equal(uri.host, "example.com")
97+
testing.assert_equal(uri.path, "/search")
98+
testing.assert_equal(uri.query_string, "q=python&page=1&limit=20")
99+
testing.assert_equal(uri.queries["q"], "python")
100+
testing.assert_equal(uri.queries["page"], "1")
101+
testing.assert_equal(uri.queries["limit"], "20")
102+
testing.assert_equal(uri.request_uri, "/search?q=python&page=1&limit=20")
103+
104+
def test_uri_parse_query_with_special_characters():
105+
var uri = URI.parse("https://example.com/path?name=John+Doe&email=john%40example.com")
106+
testing.assert_equal(uri.scheme, "https")
107+
testing.assert_equal(uri.host, "example.com")
108+
testing.assert_equal(uri.path, "/path")
109+
testing.assert_equal(uri.query_string, "name=John+Doe&email=john%40example.com")
110+
# testing.assert_equal(uri.queries["name"], "John Doe") - fails, contains John+Doe
111+
# testing.assert_equal(uri.queries["email"], "john@example.com") - fails, contains john%40example.com
112+
113+
def test_uri_parse_empty_query_values():
114+
var uri = URI.parse("http://example.com/api?key=&token=&empty")
115+
testing.assert_equal(uri.query_string, "key=&token=&empty")
116+
testing.assert_equal(uri.queries["key"], "")
117+
testing.assert_equal(uri.queries["token"], "")
118+
testing.assert_equal(uri.queries["empty"], "")
119+
120+
def test_uri_parse_complex_query():
121+
var uri = URI.parse("https://example.com/search?q=test&filter[category]=books&filter[price]=10-20&sort=desc&page=1")
122+
testing.assert_equal(uri.scheme, "https")
123+
testing.assert_equal(uri.host, "example.com")
124+
testing.assert_equal(uri.path, "/search")
125+
testing.assert_equal(uri.query_string, "q=test&filter[category]=books&filter[price]=10-20&sort=desc&page=1")
126+
testing.assert_equal(uri.queries["q"], "test")
127+
testing.assert_equal(uri.queries["filter[category]"], "books")
128+
testing.assert_equal(uri.queries["filter[price]"], "10-20")
129+
testing.assert_equal(uri.queries["sort"], "desc")
130+
testing.assert_equal(uri.queries["page"], "1")
131+
132+
def test_uri_parse_query_with_unicode():
133+
var uri = URI.parse("http://example.com/search?q=%E2%82%AC&lang=%F0%9F%87%A9%F0%9F%87%AA")
134+
testing.assert_equal(uri.query_string, "q=%E2%82%AC&lang=%F0%9F%87%A9%F0%9F%87%AA")
135+
# testing.assert_equal(uri.queries["q"], "€") - fails, contains %E2%82%AC
136+
# testing.assert_equal(uri.queries["lang"], "🇩🇪") - fails, contains %F0%9F%87%A9%F0%9F%87%AA
137+
138+
# def test_uri_parse_query_with_fragments():
139+
# var uri = URI.parse("http://example.com/page?id=123#section1")
140+
# testing.assert_equal(uri.query_string, "id=123")
141+
# testing.assert_equal(uri.queries["id"], "123")
142+
# testing.assert_equal(...) - how do we treat fragments?
91143

92144

93145
def test_uri_parse_http_with_hash():

0 commit comments

Comments
 (0)