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, Redactor, 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, redactor: &dyn Redactor, 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, redactor, f)
}
PathAndQueryInner::Templated(templated) => RedactedDisplay::fmt(&**templated, redactor, 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, redactor: &dyn Redactor, 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(redactor);
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<&str> for PathAndQuery {
type Error = UriError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(Self::from(HttpPathAndQuery::try_from(value)?))
}
}
impl TryFrom<String> for PathAndQuery {
type Error = UriError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(Self::from(HttpPathAndQuery::try_from(value)?))
}
}
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(feature = "serde")]
impl<'de> serde::Deserialize<'de> for PathAndQuery {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::try_from(s).map_err(serde::de::Error::custom)
}
}
#[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);
}
#[test]
fn try_from_str_succeeds() {
let target_path = PathAndQuery::try_from("/api/v1/users?active=true").unwrap();
assert_eq!(target_path.to_string().declassify_ref(), "/api/v1/users?active=true");
}
#[test]
fn try_from_str_invalid_errors() {
use ohno::Labeled;
let err = PathAndQuery::try_from("/invalid path\0").unwrap_err();
assert_eq!(err.label(), "uri_invalid");
}
#[test]
fn try_from_str_without_leading_slash_errors() {
use ohno::Labeled;
let err = PathAndQuery::try_from("api/v1/users").unwrap_err();
assert_eq!(err.label(), "uri_invalid");
}
#[test]
fn try_from_string_succeeds() {
let target_path = PathAndQuery::try_from(String::from("/api/v1/users?active=true")).unwrap();
assert_eq!(target_path.to_string().declassify_ref(), "/api/v1/users?active=true");
}
#[test]
fn try_from_string_invalid_errors() {
use ohno::Labeled;
let err = PathAndQuery::try_from(String::from("api/v1/users")).unwrap_err();
assert_eq!(err.label(), "uri_invalid");
}
}
#[cfg(all(test, feature = "serde"))]
mod serde_tests {
use super::PathAndQuery;
#[test]
fn deserialize_static_path_and_query() {
let paq: PathAndQuery = serde_json::from_str(r#""/api/v1/users?active=true""#).unwrap();
assert_eq!(paq.to_string().declassify_ref(), "/api/v1/users?active=true");
}
#[test]
fn deserialize_rejects_missing_leading_slash() {
serde_json::from_str::<PathAndQuery>(r#""api/v1/users""#).unwrap_err();
}
#[test]
fn deserialize_error_does_not_leak_input() {
let err = serde_json::from_str::<PathAndQuery>(r#""SECRETPATH_no_slash""#).unwrap_err();
assert!(
!err.to_string().contains("SECRETPATH"),
"deserialize error must not leak the raw input"
);
}
#[test]
fn path_and_query_does_not_implement_serialize() {
static_assertions::assert_not_impl_any!(PathAndQuery: serde::Serialize);
}
#[test]
fn deserialize_braces_are_literal_static_content() {
let paq: PathAndQuery = serde_json::from_str(r#""/users/{id}""#).unwrap();
assert_eq!(paq.to_string().declassify_ref(), "/users/{id}");
assert!(paq.label().is_none(), "deserialized value must be a static path, not a template");
}
}