perf: Don't parse for userinfo if not necessary

If the URI doesn't contain @ then there can't be a userinfo so
completely ignore this part of parsing to improve performance
This commit is contained in:
2025-09-14 22:14:49 +01:00
parent 67798d1dcf
commit 869c5cf06c
2 changed files with 43 additions and 48 deletions

View File

@@ -10,5 +10,5 @@
## v2.0.1
- Improved parsing performance slightly and reduced memory usage up to 50%
- Improved parsing performance significantly and reduced memory usage up to 50%
- Significantly improved IPV4 parsing performance

View File

@@ -1,6 +1,7 @@
import gleam/bool
import gleam/int
import gleam/list
import gleam/option.{None, Some}
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
import gleam/uri.{type Uri, Uri, empty}
@@ -19,9 +20,9 @@ pub fn parse(uri: String) -> Result(Uri, Nil) {
Ok(#(scheme, rest)) -> {
use #(rel_part, rest) <- result.try(parse_hier_part(rest))
use #(query, rest) <- result.try(parse_query(rest))
let #(query, rest) = parse_query(rest)
use #(fragment, rest) <- result.try(parse_fragment(rest))
let #(fragment, rest) = parse_fragment(rest)
case rest {
"" -> Ok(combine_uris([scheme, rel_part, query, fragment]))
@@ -31,9 +32,9 @@ pub fn parse(uri: String) -> Result(Uri, Nil) {
Error(_) -> {
use #(rel_part, rest) <- result.try(parse_relative_part(uri))
use #(query, rest) <- result.try(parse_query(rest))
let #(query, rest) = parse_query(rest)
use #(fragment, rest) <- result.try(parse_fragment(rest))
let #(fragment, rest) = parse_fragment(rest)
case rest {
"" -> Ok(combine_uris([rel_part, query, fragment]))
@@ -60,26 +61,26 @@ fn parse_hier_part(str: String) -> Result(#(Uri, String), Nil) {
}
// query = *( pchar / "/" / "?" )
fn parse_query(str: String) -> Result(#(Uri, String), Nil) {
fn parse_query(str: String) -> #(Uri, String) {
case str {
"?" <> rest -> {
let #(query, rest) =
parse_optional(rest, parse_multiple(_, parse_query_fragment))
Ok(#(Uri(..empty, query: Some(query)), rest))
#(Uri(..empty, query: Some(query)), rest)
}
_ -> Ok(#(empty, str))
_ -> #(empty, str)
}
}
// fragment = *( pchar / "/" / "?" )
fn parse_fragment(str: String) -> Result(#(Uri, String), Nil) {
fn parse_fragment(str: String) -> #(Uri, String) {
case str {
"#" <> rest -> {
let #(fragment, rest) =
parse_optional(rest, parse_multiple(_, parse_query_fragment))
Ok(#(Uri(..empty, fragment: Some(fragment)), rest))
#(Uri(..empty, fragment: Some(fragment)), rest)
}
_ -> Ok(#(empty, str))
_ -> #(empty, str)
}
}
@@ -167,50 +168,43 @@ fn parse_authority(str: String) -> Result(#(Uri, String), Nil) {
}
fn parse_authority_part(str: String) -> Result(#(Uri, String), Nil) {
let #(ui, rest) = case parse_userinfo(str, "") {
Ok(#(ui, rest)) -> #(Some(ui), rest)
Error(_) -> #(None, str)
}
let #(userinfo, rest) = parse_userinfo(str, "")
use #(host, rest) <- result.try(parse_host(rest))
let #(port, rest) = case parse_port(rest) {
Ok(#("", rest)) -> #(None, rest)
Error(_) -> #(None, rest)
Ok(#(port, rest)) -> {
#(int.parse(port) |> option.from_result, rest)
}
}
let #(port, rest) = parse_port(rest)
let #(path, rest) = parse_path_abempty(rest)
Ok(#(Uri(None, ui, Some(host), port, path, None, None), rest))
Ok(#(Uri(None, userinfo, Some(host), port, path, None, None), rest))
}
// userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
fn parse_userinfo(
str: String,
userinfo: String,
) -> Result(#(String, String), Nil) {
fn parse_userinfo(str: String, userinfo: String) -> #(Option(String), String) {
use <- bool.guard(when: !string.contains(str, "@"), return: #(None, str))
case str {
"@" <> rest -> Ok(#(userinfo, rest))
"" -> Error(Nil)
"@" <> rest -> #(Some(userinfo), rest)
"" -> #(None, userinfo <> str)
_ -> {
use #(part, rest) <- result.try(try_parsers(
[
parse_unreserved,
parse_pct_encoded,
parse_sub_delim,
fn(str: String) {
case str {
":" as l <> rest -> Ok(#(l, rest))
_ -> Error(Nil)
}
},
],
str,
))
parse_userinfo(rest, userinfo <> part)
case
try_parsers(
[
parse_unreserved,
parse_pct_encoded,
parse_sub_delim,
fn(str: String) {
case str {
":" as l <> rest -> Ok(#(l, rest))
_ -> Error(Nil)
}
},
],
str,
)
{
Ok(#(part, rest)) -> parse_userinfo(rest, userinfo <> part)
Error(_) -> #(None, userinfo <> str)
}
}
}
}
@@ -221,12 +215,13 @@ fn parse_host(str: String) {
}
// port = *DIGIT
fn parse_port(str: String) {
fn parse_port(str: String) -> #(Option(Int), String) {
case str {
":" <> rest -> {
Ok(parse_digits(rest, ""))
let #(port, rest) = parse_digits(rest, "")
#(int.parse(port) |> option.from_result, rest)
}
_ -> Error(Nil)
_ -> #(None, str)
}
}