Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse URI queries in to key value pairs #237

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions benchmark/bench.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,19 @@ fn lightbug_benchmark_request_parse(mut b: Bencher):
fn lightbug_benchmark_request_encode(mut b: Bencher):
@always_inline
@parameter
fn request_encode():
fn request_encode() raises:
var uri = URI.parse("http://127.0.0.1:8080/some-path")
var req = HTTPRequest(
URI.parse("http://127.0.0.1:8080/some-path"),
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
Expand Down
5 changes: 4 additions & 1 deletion lightbug_http/client.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 35 additions & 6 deletions lightbug_http/uri.mojo
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -10,13 +11,28 @@ 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):
var _original_path: String
var scheme: String
var path: String
var query_string: String
var queries: QueryMap
var _hash: String
var host: String

Expand All @@ -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]
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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:
saviorand marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand All @@ -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:
Expand Down
52 changes: 52 additions & 0 deletions tests/lightbug_http/test_uri.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -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"], "[email protected]") - 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():
Expand Down
Loading