use std::borrow::Cow;
use std::fmt;
use std::fmt::Formatter;
use std::ops::Deref;
use std::sync::Arc;
use data_privacy::{Classified, RedactedDebug, RedactedDisplay, RedactedToString, RedactionEngine, Sensitive};
use http::uri::PathAndQuery as HttpPathAndQuery;
use crate::error::UriError;
use crate::{PathAndQueryTemplate, Uri};
#[derive(Clone)]
pub struct PathAndQuery(PathAndQueryInner);
#[derive(Clone)]
enum PathAndQueryInner {
Static(Sensitive<HttpPathAndQuery>),
Templated(Arc<dyn PathAndQueryTemplate>),
}
impl PathAndQuery {
pub fn from_template(template: impl PathAndQueryTemplate) -> Self {
Self(PathAndQueryInner::Templated(Arc::new(template)))
}
#[must_use]
pub fn from_static(path: &'static str) -> Self {
Self::from(HttpPathAndQuery::from_static(path))
}
#[must_use]
pub fn template(&self) -> Cow<'static, str> {
match &self.0 {
PathAndQueryInner::Static(classified_pq) => Cow::Owned(classified_pq.declassify_ref().to_string()),
PathAndQueryInner::Templated(templated) => Cow::Borrowed(templated.template()),
}
}
#[must_use]
pub fn label(&self) -> Option<Cow<'static, str>> {
match &self.0 {
PathAndQueryInner::Static(_) => None,
PathAndQueryInner::Templated(templated) => templated.label().map(Cow::Borrowed),
}
}
pub fn to_string(&self) -> Sensitive<String> {
let s = match &self.0 {
PathAndQueryInner::Static(classified_pq) => classified_pq.declassify_ref().to_string(),
PathAndQueryInner::Templated(templated) => templated.render(),
};
Sensitive::new(s, Uri::DATA_CLASS)
}
}
impl RedactedDisplay for PathAndQuery {
#[cfg_attr(test, mutants::skip)] fn fmt(&self, engine: &RedactionEngine, f: &mut Formatter<'_>) -> fmt::Result {
match &self.0 {
PathAndQueryInner::Static(classified_pq) => {
let reclassified = Sensitive::new(classified_pq.declassify_ref().as_str(), classified_pq.data_class().clone());
RedactedDisplay::fmt(&reclassified, engine, f)
}
PathAndQueryInner::Templated(templated) => RedactedDisplay::fmt(&**templated, engine, f),
}
}
}
impl fmt::Debug for PathAndQuery {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut tuple = f.debug_tuple("PathAndQuery");
match &self.0 {
PathAndQueryInner::Static(_) => tuple.finish(),
PathAndQueryInner::Templated(templated) => tuple.field(templated).finish(),
}
}
}
impl RedactedDebug for PathAndQuery {
#[cfg_attr(test, mutants::skip)] fn fmt(&self, engine: &RedactionEngine, f: &mut Formatter<'_>) -> fmt::Result {
let mut tuple = f.debug_tuple("PathAndQuery");
match &self.0 {
PathAndQueryInner::Static(_) => tuple.finish(),
PathAndQueryInner::Templated(templated) => {
let rendered = templated.deref().to_redacted_string(engine);
tuple.field(&rendered).finish()
}
}
}
}
impl TryFrom<Uri> for PathAndQuery {
type Error = UriError;
fn try_from(uri: Uri) -> Result<Self, Self::Error> {
uri.path_and_query
.ok_or_else(|| UriError::invalid_uri("URI does not have a path and query component"))
}
}
impl From<HttpPathAndQuery> for PathAndQuery {
fn from(value: HttpPathAndQuery) -> Self {
Self(PathAndQueryInner::Static(Sensitive::new(value, Uri::DATA_CLASS)))
}
}
impl TryFrom<&PathAndQuery> for HttpPathAndQuery {
type Error = UriError;
fn try_from(value: &PathAndQuery) -> Result<Self, Self::Error> {
match &value.0 {
PathAndQueryInner::Static(classified_pq) => Ok(classified_pq.declassify_ref().clone()),
PathAndQueryInner::Templated(templated) => templated.to_path_and_query(),
}
}
}
impl TryFrom<PathAndQuery> for HttpPathAndQuery {
type Error = UriError;
fn try_from(value: PathAndQuery) -> Result<Self, Self::Error> {
Self::try_from(&value)
}
}
impl From<PathAndQuery> for Uri {
fn from(value: PathAndQuery) -> Self {
Self::new().with_path_and_query(value)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
use crate::BaseUri;
#[test]
fn from_path_and_query_roundtrip() {
let path = HttpPathAndQuery::from_str("/path/to/resource?query=param").unwrap();
let target_path: PathAndQuery = path.clone().into();
assert_eq!(target_path.template(), "/path/to/resource?query=param");
assert_eq!(target_path.to_string().declassify_ref(), "/path/to/resource?query=param");
assert_eq!(HttpPathAndQuery::try_from(&target_path).unwrap(), path);
assert_eq!(
Uri::from(target_path.clone()).to_string(),
Uri::default().with_path_and_query(target_path).to_string()
);
}
#[test]
fn try_from_uri_without_path_errors() {
let uri = Uri::default().with_base(BaseUri::from_static("https://example.com/"));
let result: Result<PathAndQuery, UriError> = uri.try_into();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("does not have a path and query component"));
}
#[test]
fn try_from_uri_with_path_succeeds() {
let path = HttpPathAndQuery::from_static("/test/path?query=value");
let uri = Uri::default().with_path_and_query(path);
let target_paq: PathAndQuery = uri.try_into().unwrap();
assert_eq!(target_paq.to_string().declassify_ref(), "/test/path?query=value");
}
#[test]
fn try_from_owned_uri_path_to_path_and_query() {
let path = HttpPathAndQuery::from_static("/owned/path?query=value");
let target_path: PathAndQuery = path.clone().into();
let converted: HttpPathAndQuery = HttpPathAndQuery::try_from(target_path.clone()).unwrap();
assert_eq!(converted, path);
let converted_ref: HttpPathAndQuery = HttpPathAndQuery::try_from(&target_path).unwrap();
assert_eq!(converted, converted_ref);
}
}