Skip to content

Commit 1fdca16

Browse files
committed
Add QueryString custom struct
1 parent fe37831 commit 1fdca16

3 files changed

Lines changed: 134 additions & 3 deletions

File tree

src/http/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
pub use method::Method;
2+
pub use query_string::{QueryString, Value};
23
pub use request::ParseError;
34
pub use request::Request;
45

56
mod method;
7+
mod query_string;
68
mod request;

src/http/query_string.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use ::std::collections::HashMap;
2+
use std::{
3+
convert::From,
4+
fmt::{self, Display},
5+
};
6+
7+
/// Represents a value in a query string, which can be either a single value or multiple values for the same key.
8+
#[derive(Debug, Clone, PartialEq, Eq)]
9+
pub enum Value<'buf> {
10+
Single(&'buf str),
11+
Multiple(Vec<&'buf str>), // heap allocated array, dynamically growing
12+
}
13+
14+
/// Represents a parsed query string, storing key-value pairs where both key and value are string slices from the same buffer.
15+
#[derive(Debug, Clone, PartialEq, Eq)]
16+
pub struct QueryString<'buf> {
17+
// both key and value comes from the same buffer, so they have the same lifetime as `buf`
18+
data: HashMap<&'buf str, Value<'buf>>,
19+
}
20+
impl<'buf> QueryString<'buf> {
21+
/// Retrieves the value associated with the given key from the query string.
22+
///
23+
/// # Arguments
24+
///
25+
/// * `key` - The key to look up in the query string.
26+
///
27+
/// # Returns
28+
///
29+
/// An `Option` containing a reference to the `Value` if the key exists, or `None` otherwise.
30+
pub fn get(&self, key: &str) -> Option<&Value> {
31+
self.data.get(key)
32+
}
33+
}
34+
35+
/// Implements conversion from a string slice to a `QueryString`.
36+
///
37+
/// # Note
38+
///
39+
/// The `From` trait is used instead of `TryFrom` because the conversion from a string to a `QueryString` cannot fail.
40+
/// The input string buffer is assumed to have a valid format for query strings (e.g., `key1=value1&key2=value2`).
41+
impl<'buf> From<&'buf str> for QueryString<'buf> {
42+
/// Converts a string slice into a QueryString by parsing key-value pairs.
43+
///
44+
/// # Arguments
45+
///
46+
/// * `s` - A string slice containing the query string to parse (e.g., "key1=value1&key2=value2")
47+
///
48+
/// # Returns
49+
///
50+
/// A new `QueryString` instance containing the parsed key-value pairs.
51+
///
52+
/// # Examples
53+
///
54+
/// ```
55+
/// let query = QueryString::from("name=John&age=30");
56+
/// ```
57+
fn from(s: &'buf str) -> Self {
58+
// Initialize an empty HashMap to store the key-value pairs
59+
let mut data = HashMap::new();
60+
61+
// Split the input string by '&' to get individual key-value pairs
62+
for sub_str in s.split('&') {
63+
// Default values in case no '=' is found
64+
let mut key = sub_str;
65+
let mut value = "";
66+
67+
// If '=' is found, split the substring into key and value
68+
if let Some(i) = sub_str.find('=') {
69+
key = &sub_str[..i];
70+
value = &sub_str[i + 1..];
71+
}
72+
73+
// Insert the key-value pair into the HashMap
74+
data.entry(key)
75+
// Handle the case where a key already exists in the HashMap
76+
.and_modify(|existing: &mut Value| match existing {
77+
// If the key exists with a single value, convert it to a Multiple value
78+
Value::Single(prev_value) => {
79+
*existing = Value::Multiple(vec![prev_value, value]); // dereference the existing value and assign a new value
80+
}
81+
// If the key already has multiple values, append the new value
82+
Value::Multiple(vec) => vec.push(value),
83+
})
84+
// If the key does not exist, insert a new single value
85+
.or_insert(Value::Single(value));
86+
}
87+
88+
// Create and return a new QueryString with the parsed data
89+
QueryString { data }
90+
}
91+
}
92+
93+
/// Implements the `Display` trait for `QueryString`, formatting it as a string of joined keys separated by '&'.
94+
impl<'buf> Display for QueryString<'buf> {
95+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96+
write!(
97+
f,
98+
"{}",
99+
self.data
100+
.keys()
101+
.map(|key| key.to_string())
102+
.collect::<Vec<String>>()
103+
.join("&")
104+
)
105+
}
106+
}

src/http/request.rs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,33 @@
11
use super::method::{Method, MethodError}; // Import Method and MethodError from the method module
2+
use super::{QueryString, Value};
23
use std::convert::TryFrom; // convert::From doesn't handle errors, convert::TryFrom handles errors
34
use std::error::Error; // Error trait is used for error handling in Rust
45
use std::fmt::{Debug, Display, Result as FmtResult};
56
use std::str;
67
use std::str::Utf8Error; // Utf8Error is used to handle errors when converting bytes to a string
78

9+
/// Extracts the first word from a string, separated by spaces or carriage returns.
10+
///
11+
/// # Arguments
12+
///
13+
/// * `request` - A string slice containing the text to parse
14+
///
15+
/// # Returns
16+
///
17+
/// Returns `Some((word, rest))` where:
18+
/// * `word` is the first word found before a space or carriage return
19+
/// * `rest` is the remaining string after the delimiter
20+
///
21+
/// Returns `None` if no delimiter is found
22+
///
23+
/// # Examples
24+
///
25+
/// ```
26+
/// let text = "GET /path HTTP/1.1";
27+
/// let (word, rest) = get_next_word(text).unwrap();
28+
/// assert_eq!(word, "GET");
29+
/// assert_eq!(rest, "/path HTTP/1.1");
30+
/// ```
831
fn get_next_word<'buf>(request: &'buf str) -> Option<(&'buf str, &'buf str)> {
932
for (i, c) in request.chars().enumerate() {
1033
if c == ' ' || c == '\r' {
@@ -19,7 +42,7 @@ fn get_next_word<'buf>(request: &'buf str) -> Option<(&'buf str, &'buf str)> {
1942

2043
pub struct Request<'buf> {
2144
method: Method,
22-
query_string: Option<&'buf str>, // query string may or may not exist on URL
45+
query_string: Option<QueryString<'buf>>, // query string may or may not exist on URL
2346
path: &'buf str,
2447
}
2548
impl<'buf> Request<'buf> {
@@ -50,11 +73,11 @@ impl<'buf> TryFrom<&'buf [u8]> for Request<'buf> {
5073
}
5174

5275
let method: Method = method.parse()?;
53-
let mut query_string: Option<&'buf str> = None;
76+
let mut query_string: Option<QueryString<'buf>> = None;
5477
if let Some(i) = path.find('?') {
5578
// `if let` syntax allows you to only match on variants you care about
5679
path = &path[..i]; // Update path to exclude the query string
57-
query_string = Some(&path[i + 1..]); // If the path contains a query string, extract it
80+
query_string = Some(QueryString::from(&path[i + 1..])); // If the path contains a query string, extract it
5881
}
5982

6083
Ok(Self {

0 commit comments

Comments
 (0)