use std::fmt;
use crate::url::errors::InvalidUri;
use crate::url::query::Query;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PathAndQuery {
pub path: String,
pub query: Query,
}
impl PathAndQuery {
pub fn parse(s: &str) -> Result<Self, InvalidUri> {
if s.is_empty() {
return Ok(PathAndQuery {
path: "/".to_string(),
query: Query::new(),
});
}
let (path_str, query) = if let Some(q_pos) = s.find('?') {
let query_str = &s[q_pos + 1..];
let query = Query::parse(query_str)
.map_err(|e| InvalidUri::new(format!("invalid query: {e}")))?;
(&s[..q_pos], query)
} else {
(s, Query::new())
};
Self::validate_path(path_str)?;
let path = if path_str.is_empty() || path_str.starts_with('/') {
if path_str.is_empty() {
"/".to_string()
} else {
path_str.to_string()
}
} else {
format!("/{path_str}")
};
Ok(PathAndQuery { path, query })
}
fn validate_path(s: &str) -> Result<(), InvalidUri> {
for c in s.chars() {
if !(c.is_ascii_alphanumeric()
|| c == '-'
|| c == '.'
|| c == '_'
|| c == '~' || c == '!'
|| c == '$'
|| c == '&'
|| c == '\''
|| c == '('
|| c == ')'
|| c == '*'
|| c == '+'
|| c == ','
|| c == ';'
|| c == '=' || c == ':'
|| c == '@' || c == '/' || c == '%')
{
return Err(InvalidUri::new(format!("invalid path character: {c}")));
}
}
Ok(())
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub fn query(&self) -> Option<String> {
if self.query.is_empty() {
None
} else {
Some(self.query.to_string())
}
}
#[must_use]
pub fn query_params(&self) -> &Query {
&self.query
}
#[must_use]
pub fn query_params_mut(&mut self) -> &mut Query {
&mut self.query
}
}
impl fmt::Display for PathAndQuery {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.path)?;
if !self.query.is_empty() {
write!(f, "?{}", self.query)?;
}
Ok(())
}
}