Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
43 changes: 37 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,26 @@ 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:
# 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(
_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 +115,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"], "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():
Expand Down
Loading