From 3815b38b7ed4f5311a1931b43140a808d99e31a5 Mon Sep 17 00:00:00 2001 From: "Hristo (Izo) Gueorguiev" <53634432+izo0x90@users.noreply.github.com> Date: Sun, 26 Jan 2025 15:34:50 -0500 Subject: [PATCH 1/6] Parse URI queries in to key value pairs - Adds `queries` dict to request uri object containing the uri queries as key value pairs --- lightbug_http/uri.mojo | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/lightbug_http/uri.mojo b/lightbug_http/uri.mojo index d56295ea..19f2003f 100644 --- a/lightbug_http/uri.mojo +++ b/lightbug_http/uri.mojo @@ -1,3 +1,4 @@ +from collections import Dict from utils import Variant from lightbug_http.io.bytes import Bytes, bytes from lightbug_http.strings import ( @@ -10,6 +11,20 @@ from lightbug_http.strings import ( https, ) +alias QueryMap = Dict[String, String] + + +struct QueryDelimiters: + alias STRING_START = "?" + alias ITEM = "&" + alias ITEM_ASSIGN = "=" + + +struct URIDelimiters: + alias SCHEMA = "://" + alias PATH = strSlash + alias ROOT_PATH = strSlash + @value struct URI(Writable, Stringable, Representable): @@ -17,6 +32,7 @@ struct URI(Writable, Stringable, Representable): var scheme: String var path: String var query_string: String + var queries: QueryMap var _hash: String var host: String @@ -27,11 +43,11 @@ struct URI(Writable, Stringable, Representable): var password: String @staticmethod - fn parse(uri: String) -> URI: + fn parse(uri: String) raises -> URI: var proto_str = String(strHttp11) var is_https = False - var proto_end = uri.find("://") + var proto_end = uri.find(URIDelimiters.SCHEMA) var remainder_uri: String if proto_end >= 0: proto_str = uri[:proto_end] @@ -41,7 +57,7 @@ struct URI(Writable, Stringable, Representable): else: remainder_uri = uri - var path_start = remainder_uri.find("/") + var path_start = remainder_uri.find(URIDelimiters.PATH) var host_and_port: String var request_uri: String var host: String @@ -60,7 +76,7 @@ struct URI(Writable, Stringable, Representable): else: scheme = http - var n = request_uri.find("?") + var n = request_uri.find(QueryDelimiters.STRING_START) var original_path: String var query_string: String if n >= 0: @@ -70,11 +86,24 @@ struct URI(Writable, Stringable, Representable): original_path = request_uri query_string = "" + var queries = QueryMap() + if query_string: + var query_items = query_string.split(QueryDelimiters.ITEM) + + for item in query_items: + var key_val = item[].split(QueryDelimiters.ITEM_ASSIGN, 1) + + if key_val[0]: + queries[key_val[0]] = "" + if len(key_val) == 2: + queries[key_val[0]] = key_val[1] + return URI( _original_path=original_path, scheme=scheme, path=original_path, query_string=query_string, + queries=queries, _hash="", host=host, full_uri=uri, @@ -84,9 +113,9 @@ struct URI(Writable, Stringable, Representable): ) fn __str__(self) -> String: - var result = String.write(self.scheme, "://", self.host, self.path) + var result = String.write(self.scheme, URIDelimiters.SCHEMA, self.host, self.path) if len(self.query_string) > 0: - result.write("?", self.query_string) + result.write(QueryDelimiters.STRING_START, self.query_string) return result^ fn __repr__(self) -> String: From 16e273fa0b76a8dfcefabcf000e0ffdec58140e5 Mon Sep 17 00:00:00 2001 From: Val Date: Mon, 27 Jan 2025 15:25:39 +0100 Subject: [PATCH 2/6] add unit tests --- tests/lightbug_http/test_uri.mojo | 52 +++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/lightbug_http/test_uri.mojo b/tests/lightbug_http/test_uri.mojo index 7f332841..f8a88d62 100644 --- a/tests/lightbug_http/test_uri.mojo +++ b/tests/lightbug_http/test_uri.mojo @@ -88,6 +88,58 @@ def test_uri_parse_http_with_query_string(): testing.assert_equal(uri._original_path, "/job") testing.assert_equal(uri.request_uri, "/job?title=engineer") testing.assert_equal(uri.query_string, "title=engineer") + testing.assert_equal(uri.queries["title"], "engineer") + +def test_uri_parse_multiple_query_parameters(): + var uri = URI.parse("http://example.com/search?q=python&page=1&limit=20") + testing.assert_equal(uri.scheme, "http") + testing.assert_equal(uri.host, "example.com") + testing.assert_equal(uri.path, "/search") + testing.assert_equal(uri.query_string, "q=python&page=1&limit=20") + testing.assert_equal(uri.queries["q"], "python") + testing.assert_equal(uri.queries["page"], "1") + testing.assert_equal(uri.queries["limit"], "20") + testing.assert_equal(uri.request_uri, "/search?q=python&page=1&limit=20") + +def test_uri_parse_query_with_special_characters(): + var uri = URI.parse("https://example.com/path?name=John+Doe&email=john%40example.com") + testing.assert_equal(uri.scheme, "https") + testing.assert_equal(uri.host, "example.com") + testing.assert_equal(uri.path, "/path") + testing.assert_equal(uri.query_string, "name=John+Doe&email=john%40example.com") + # testing.assert_equal(uri.queries["name"], "John Doe") - fails, contains John+Doe + # testing.assert_equal(uri.queries["email"], "john@example.com") - fails, contains john%40example.com + +def test_uri_parse_empty_query_values(): + var uri = URI.parse("http://example.com/api?key=&token=&empty") + testing.assert_equal(uri.query_string, "key=&token=&empty") + testing.assert_equal(uri.queries["key"], "") + testing.assert_equal(uri.queries["token"], "") + testing.assert_equal(uri.queries["empty"], "") + +def test_uri_parse_complex_query(): + var uri = URI.parse("https://example.com/search?q=test&filter[category]=books&filter[price]=10-20&sort=desc&page=1") + testing.assert_equal(uri.scheme, "https") + testing.assert_equal(uri.host, "example.com") + testing.assert_equal(uri.path, "/search") + testing.assert_equal(uri.query_string, "q=test&filter[category]=books&filter[price]=10-20&sort=desc&page=1") + testing.assert_equal(uri.queries["q"], "test") + testing.assert_equal(uri.queries["filter[category]"], "books") + testing.assert_equal(uri.queries["filter[price]"], "10-20") + testing.assert_equal(uri.queries["sort"], "desc") + testing.assert_equal(uri.queries["page"], "1") + +def test_uri_parse_query_with_unicode(): + var uri = URI.parse("http://example.com/search?q=%E2%82%AC&lang=%F0%9F%87%A9%F0%9F%87%AA") + testing.assert_equal(uri.query_string, "q=%E2%82%AC&lang=%F0%9F%87%A9%F0%9F%87%AA") +# testing.assert_equal(uri.queries["q"], "€") - fails, contains %E2%82%AC + # testing.assert_equal(uri.queries["lang"], "🇩🇪") - fails, contains %F0%9F%87%A9%F0%9F%87%AA + +# def test_uri_parse_query_with_fragments(): +# var uri = URI.parse("http://example.com/page?id=123#section1") +# testing.assert_equal(uri.query_string, "id=123") +# testing.assert_equal(uri.queries["id"], "123") +# testing.assert_equal(...) - how do we treat fragments? def test_uri_parse_http_with_hash(): From cfa6c84dba63ff35e9f13966ac90bade37ec379b Mon Sep 17 00:00:00 2001 From: Val Date: Mon, 27 Jan 2025 15:54:15 +0100 Subject: [PATCH 3/6] handle exceptions --- benchmark/bench.mojo | 7 ++++++- lightbug_http/client.mojo | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/benchmark/bench.mojo b/benchmark/bench.mojo index accd1ad5..ecbd5079 100644 --- a/benchmark/bench.mojo +++ b/benchmark/bench.mojo @@ -100,8 +100,13 @@ fn lightbug_benchmark_request_encode(mut b: Bencher): @always_inline @parameter fn request_encode(): + var uri = URI() + try: + uri = URI.parse("http://127.0.0.1:8080/some-path") + except e: + print("Could not parse URI, error: ", e) var req = HTTPRequest( - URI.parse("http://127.0.0.1:8080/some-path"), + headers=headers_struct, body=body_bytes, ) diff --git a/lightbug_http/client.mojo b/lightbug_http/client.mojo index 2f655199..ac03a023 100644 --- a/lightbug_http/client.mojo +++ b/lightbug_http/client.mojo @@ -160,7 +160,10 @@ struct Client: raise Error("Client._handle_redirect: `Location` header was not received in the response.") if new_location and new_location.startswith("http"): - new_uri = URI.parse(new_location) + try: + new_uri = URI.parse(new_location) + except e: + raise Error("Client._handle_redirect: Failed to parse the new URI: " + str(e)) original_req.headers[HeaderKey.HOST] = new_uri.host else: new_uri = original_req.uri From 19fc97e2a2aab12c955a6a84f1302962bf9552cf Mon Sep 17 00:00:00 2001 From: Val Date: Mon, 27 Jan 2025 15:55:25 +0100 Subject: [PATCH 4/6] add missing argument --- benchmark/bench.mojo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/bench.mojo b/benchmark/bench.mojo index ecbd5079..746e4325 100644 --- a/benchmark/bench.mojo +++ b/benchmark/bench.mojo @@ -106,7 +106,7 @@ fn lightbug_benchmark_request_encode(mut b: Bencher): except e: print("Could not parse URI, error: ", e) var req = HTTPRequest( - + uri=uri, headers=headers_struct, body=body_bytes, ) From e65bea49127ab8acf2b9c02f20444bba6ff2a28d Mon Sep 17 00:00:00 2001 From: Val Date: Mon, 27 Jan 2025 15:58:14 +0100 Subject: [PATCH 5/6] make method raising --- benchmark/bench.mojo | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/benchmark/bench.mojo b/benchmark/bench.mojo index 746e4325..b0f505b1 100644 --- a/benchmark/bench.mojo +++ b/benchmark/bench.mojo @@ -99,20 +99,19 @@ fn lightbug_benchmark_request_parse(mut b: Bencher): fn lightbug_benchmark_request_encode(mut b: Bencher): @always_inline @parameter - fn request_encode(): - var uri = URI() - try: - uri = URI.parse("http://127.0.0.1:8080/some-path") - except e: - print("Could not parse URI, error: ", e) + fn request_encode() raises: + var uri = URI.parse("http://127.0.0.1:8080/some-path") var req = HTTPRequest( - uri=uri, - headers=headers_struct, - body=body_bytes, + uri=uri, + headers=headers_struct, + body=body_bytes, ) _ = encode(req^) - - b.iter[request_encode]() + + try: + b.iter[request_encode]() + except e: + print("failed to encode request, error: ", e) @parameter From 00906a6cd6562db347c27a372eea4870e611c190 Mon Sep 17 00:00:00 2001 From: "Hristo (Izo) Gueorguiev" <53634432+izo0x90@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:03:28 -0500 Subject: [PATCH 6/6] Add todo - query values from uri should be decoded --- lightbug_http/uri.mojo | 2 ++ tests/lightbug_http/test_uri.mojo | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lightbug_http/uri.mojo b/lightbug_http/uri.mojo index 19f2003f..5f7aeb1f 100644 --- a/lightbug_http/uri.mojo +++ b/lightbug_http/uri.mojo @@ -96,6 +96,8 @@ struct URI(Writable, Stringable, Representable): if key_val[0]: queries[key_val[0]] = "" if len(key_val) == 2: + # TODO: Query values are going to be URI encoded strings and should be decoded as part of the + # query processing queries[key_val[0]] = key_val[1] return URI( diff --git a/tests/lightbug_http/test_uri.mojo b/tests/lightbug_http/test_uri.mojo index f8a88d62..39b1cc93 100644 --- a/tests/lightbug_http/test_uri.mojo +++ b/tests/lightbug_http/test_uri.mojo @@ -132,7 +132,7 @@ def test_uri_parse_complex_query(): def test_uri_parse_query_with_unicode(): var uri = URI.parse("http://example.com/search?q=%E2%82%AC&lang=%F0%9F%87%A9%F0%9F%87%AA") testing.assert_equal(uri.query_string, "q=%E2%82%AC&lang=%F0%9F%87%A9%F0%9F%87%AA") -# testing.assert_equal(uri.queries["q"], "€") - fails, contains %E2%82%AC + # testing.assert_equal(uri.queries["q"], "€") - fails, contains %E2%82%AC # testing.assert_equal(uri.queries["lang"], "🇩🇪") - fails, contains %F0%9F%87%A9%F0%9F%87%AA # def test_uri_parse_query_with_fragments():