use std::fmt::{Debug, Display};
use std::str::FromStr;
use http::uri::PathAndQuery;
use crate::ValidationError;
use crate::uri::{Authority, Parts, Scheme};
mod base_path;
mod origin;
pub use base_path::BasePath;
pub use origin::Origin;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BaseUri {
origin: Origin,
path: BasePath,
}
impl BaseUri {
pub fn new(
scheme: impl TryInto<Scheme, Error: Into<http::Error>>,
authority: impl TryInto<Authority, Error: Into<http::Error>>,
) -> Result<Self, ValidationError> {
let origin = Origin::new(scheme, authority)?;
Ok(Self {
origin,
path: BasePath::default(),
})
}
pub fn with_path<P>(mut self, path: P) -> Result<Self, ValidationError>
where
P: TryInto<BasePath>,
ValidationError: From<<P as TryInto<BasePath>>::Error>,
{
self.path = path.try_into()?;
Ok(self)
}
pub fn from_parts(scheme: Scheme, host: impl AsRef<str>, port: u16, path: BasePath) -> Result<Self, ValidationError> {
Self::new(scheme, format!("{}:{}", host.as_ref(), port))?.with_path(path)
}
#[must_use]
pub fn from_uri_static(uri: &'static str) -> Self {
Self::from_http_uri(&http::Uri::from_static(uri)).expect("static str is not a valid base_uri URI")
}
pub fn from_uri_str(uri: &str) -> Result<Self, ValidationError> {
uri.parse::<http::Uri>()
.map_err(Into::into)
.and_then(|uri| Self::from_http_uri(&uri))
}
pub fn from_http_uri(uri: &http::Uri) -> Result<Self, ValidationError> {
let (Some(scheme), Some(authority)) = (uri.scheme(), uri.authority()) else {
return Err(ValidationError::invalid_uri("URI must have both scheme and authority components"));
};
let path = match uri.path() {
"" | "/" => BasePath::default(),
p => BasePath::try_from(p)?,
};
Self::new(scheme.clone(), authority.clone())?.with_path(path)
}
pub const fn scheme(&self) -> &Scheme {
self.origin.scheme()
}
pub const fn authority(&self) -> &Authority {
self.origin.authority()
}
pub fn host(&self) -> &str {
self.origin.authority().host()
}
pub fn origin(&self) -> &Origin {
&self.origin
}
#[must_use]
pub fn with_origin(self, origin: Origin) -> Self {
Self { origin, path: self.path }
}
pub fn port(&self) -> u16 {
self.origin.port()
}
#[must_use]
pub fn with_port(self, port: u16) -> Self {
Self {
origin: self.origin.with_port(port),
path: self.path,
}
}
pub const fn path(&self) -> &BasePath {
&self.path
}
pub fn is_https(&self) -> bool {
self.origin.is_https()
}
pub fn build_http_uri(&self, path: impl TryInto<PathAndQuery, Error: Into<http::Error>>) -> Result<http::Uri, ValidationError> {
let full_path = self.path.join(path)?;
let mut parts = Parts::default();
parts.scheme = Some(self.scheme().clone());
parts.authority = Some(self.authority().clone());
parts.path_and_query = Some(full_path);
http::Uri::from_parts(parts).map_err(Into::into)
}
}
impl TryFrom<http::Uri> for BaseUri {
type Error = ValidationError;
fn try_from(uri: http::Uri) -> Result<Self, Self::Error> {
Self::from_http_uri(&uri)
}
}
impl From<Origin> for BaseUri {
fn from(origin: Origin) -> Self {
Self {
origin,
path: BasePath::default(),
}
}
}
impl FromStr for BaseUri {
type Err = ValidationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_uri_str(s)
}
}
impl From<BaseUri> for http::Uri {
fn from(value: BaseUri) -> Self {
let (scheme, authority) = value.origin.into_parts();
let mut parts = Parts::default();
parts.scheme = Some(scheme);
parts.authority = Some(authority);
parts.path_and_query = Some(value.path.into());
Self::from_parts(parts).expect("all inputs are already validated, this call never fails")
}
}
impl Display for BaseUri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}://", self.scheme())?;
match (self.scheme().as_str(), self.port()) {
("http", 80) | ("https", 443) => write!(f, "{}", self.host())?,
_ => write!(f, "{}", self.authority())?,
}
write!(f, "{}", self.path)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for BaseUri {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for BaseUri {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
mod new {
use super::*;
#[test]
fn valid_base_uri() {
let base_uri = BaseUri::new(Scheme::HTTPS, "example.com").unwrap();
assert_eq!(base_uri.scheme(), &Scheme::HTTPS);
assert_eq!(base_uri.authority().as_str(), "example.com");
}
#[test]
fn with_custom_port() {
let base_uri = BaseUri::new(Scheme::HTTP, "example.com:8080").unwrap();
assert_eq!(base_uri.scheme(), &Scheme::HTTP);
assert_eq!(base_uri.authority().as_str(), "example.com:8080");
}
#[test]
fn with_string_scheme() {
let base_uri = BaseUri::new("https", "example.com").unwrap();
assert_eq!(base_uri.scheme(), &Scheme::HTTPS);
}
#[test]
fn invalid_scheme() {
let err = BaseUri::new("ftp", "example.com").unwrap_err();
assert!(err.to_string().contains("unsupported scheme: ftp"));
}
#[test]
fn invalid_authority() {
let err = BaseUri::new(Scheme::HTTPS, "exam/ple.com:123").unwrap_err();
assert!(err.to_string().contains("invalid uri"));
}
}
mod from_parts {
use super::*;
#[test]
fn valid_parts() {
let base_uri = BaseUri::from_parts(Scheme::HTTPS, "example.com", 443, BasePath::from_str("/example/").unwrap()).unwrap();
assert_eq!(base_uri.scheme(), &Scheme::HTTPS);
assert_eq!(base_uri.authority().as_str(), "example.com:443");
assert_eq!(base_uri.to_string(), "https://example.com/example/");
}
#[test]
fn invalid_host() {
let err = BaseUri::from_parts(Scheme::HTTPS, "exa/mple.com", 443, BasePath::from_str("/example/").unwrap()).unwrap_err();
assert!(err.to_string().contains("invalid uri"));
}
}
mod from_uri_static {
use super::*;
#[test]
fn valid_uri() {
let base_uri = BaseUri::from_uri_static("https://example.com");
assert_eq!(base_uri.scheme(), &Scheme::HTTPS);
assert_eq!(base_uri.authority().as_str(), "example.com");
}
#[test]
fn with_path() {
let base_uri = BaseUri::from_uri_static("https://example.com/path/to/resource/");
assert_eq!(base_uri.scheme(), &Scheme::HTTPS);
assert_eq!(base_uri.authority().as_str(), "example.com");
assert_eq!(base_uri.to_string(), "https://example.com/path/to/resource/");
}
#[should_panic(expected = "static str is not a valid base_uri URI")]
#[test]
fn invalid_uri() {
let _base_uri = BaseUri::from_uri_static("not-a-valid-uri");
}
}
mod from_uri_str {
use ohno::ErrorExt;
use super::*;
#[test]
fn valid_uri() {
let base_uri = BaseUri::from_uri_str("https://example.com/").unwrap();
assert_eq!(base_uri.scheme(), &Scheme::HTTPS);
assert_eq!(base_uri.authority().as_str(), "example.com");
}
#[test]
fn with_path() {
let base_uri = BaseUri::from_uri_str("https://example.com/path/").unwrap();
assert_eq!(base_uri.scheme(), &Scheme::HTTPS);
assert_eq!(base_uri.authority().as_str(), "example.com");
}
#[test]
fn strips_query_from_path() {
let base_uri = BaseUri::from_uri_str("https://example.com/path/?query=1&other=2").unwrap();
assert_eq!(base_uri.to_string(), "https://example.com/path/");
}
#[test]
fn invalid_uri() {
let err = BaseUri::from_uri_str("not-a-valid-uri").unwrap_err();
assert_eq!(err.message(), "URI must have both scheme and authority components");
}
}
mod from_uri {
use super::*;
#[test]
fn valid_uri() {
let uri = http::Uri::from_static("https://example.com");
let base_uri = BaseUri::from_http_uri(&uri).unwrap();
assert_eq!(base_uri.scheme(), &Scheme::HTTPS);
assert_eq!(base_uri.authority().as_str(), "example.com");
}
#[test]
fn with_path() {
let uri = http::Uri::from_static("https://example.com/path/");
let base_uri = BaseUri::from_http_uri(&uri).unwrap();
assert_eq!(base_uri.scheme(), &Scheme::HTTPS);
assert_eq!(base_uri.authority().as_str(), "example.com");
assert_eq!(base_uri.path().as_str(), "/path/");
}
#[test]
fn strips_query_from_path() {
let uri = http::Uri::from_static("https://example.com/path/?query=1");
let base_uri = BaseUri::from_http_uri(&uri).unwrap();
assert_eq!(base_uri.to_string(), "https://example.com/path/");
}
#[test]
fn missing_components() {
let uri = http::Uri::from_static("/just-a-path");
let err = BaseUri::from_http_uri(&uri).unwrap_err();
assert!(err.to_string().contains("URI must have both scheme and authority"));
}
}
mod accessors {
use super::*;
#[test]
fn scheme() {
let base_uri = BaseUri::from_uri_static("https://example.com");
assert_eq!(base_uri.scheme().as_str(), "https");
}
#[test]
fn authority() {
let base_uri = BaseUri::from_uri_static("https://example.com:8443");
assert_eq!(base_uri.authority().as_str(), "example.com:8443");
}
#[test]
fn host() {
let base_uri = BaseUri::from_uri_static("https://example.com:8443");
assert_eq!(base_uri.host(), "example.com");
}
#[test]
fn port_explicit() {
let base_uri = BaseUri::from_uri_static("https://example.com:8443");
assert_eq!(base_uri.port(), 8443);
}
#[test]
fn port_default_https() {
let base_uri = BaseUri::from_uri_static("https://example.com");
assert_eq!(base_uri.port(), 443);
}
#[test]
fn port_default_http() {
let base_uri = BaseUri::from_uri_static("http://example.com");
assert_eq!(base_uri.port(), 80);
}
}
mod is_https {
use super::*;
#[test]
fn secure() {
let base_uri = BaseUri::from_uri_static("https://example.com");
assert!(base_uri.is_https());
}
#[test]
fn insecure() {
let base_uri = BaseUri::from_uri_static("http://example.com");
assert!(!base_uri.is_https());
}
}
mod build_uri {
use super::*;
#[test]
fn with_path_string() {
let base_uri = BaseUri::from_uri_static("https://example.com");
let uri = base_uri.build_http_uri("/api/resource").unwrap();
assert_eq!(uri.to_string(), "https://example.com/api/resource");
}
#[test]
fn with_empty_uri() {
let base_uri = BaseUri::from_uri_static("https://example.com");
let uri = base_uri.build_http_uri("/").unwrap();
assert_eq!(uri.to_string(), "https://example.com/");
}
#[test]
fn with_path_and_query_string() {
let base_uri = BaseUri::from_uri_static("https://example.com");
let uri = base_uri.build_http_uri("/api/resource?param=value").unwrap();
assert_eq!(uri.to_string(), "https://example.com/api/resource?param=value");
}
#[test]
fn with_path_and_query_object() {
let base_uri = BaseUri::from_uri_static("https://example.com");
let path_and_query = PathAndQuery::from_static("/api/resource?param=value");
let uri = base_uri.build_http_uri(path_and_query).unwrap();
assert_eq!(uri.to_string(), "https://example.com/api/resource?param=value");
}
#[test]
fn invalid_path() {
let base_uri = BaseUri::from_uri_static("https://example.com");
let invalid_path = "some path/?invalid\\character";
let err = base_uri.build_http_uri(invalid_path).unwrap_err();
assert!(err.to_string().contains("invalid uri character"));
}
}
mod conversions {
use super::*;
#[test]
fn uri_to_base_uri() {
let uri = http::Uri::from_static("https://example.com/path/");
let base_uri: BaseUri = uri.try_into().unwrap();
assert_eq!(base_uri.scheme(), &Scheme::HTTPS);
assert_eq!(base_uri.authority().as_str(), "example.com");
}
#[test]
fn base_uri_to_uri() {
let base_uri = BaseUri::from_uri_static("https://example.com");
let uri: http::Uri = base_uri.into();
assert_eq!(uri.to_string(), "https://example.com/");
}
#[test]
fn origin_to_base_uri() {
let origin = Origin::new(Scheme::HTTPS, "example.com:8443").unwrap();
let base_uri: BaseUri = origin.into();
assert_eq!(base_uri.to_string(), "https://example.com:8443/");
}
#[test]
fn from_str_valid() {
let base_uri: BaseUri = "https://example.com:8443/api/".parse().unwrap();
assert_eq!(base_uri.to_string(), "https://example.com:8443/api/");
}
#[test]
fn from_str_invalid() {
let err = "not-a-valid-uri".parse::<BaseUri>().unwrap_err();
assert!(err.to_string().contains("URI must have both scheme and authority"));
}
}
mod display {
use super::*;
#[test]
fn http_default_port() {
let base_uri = BaseUri::from_uri_static("http://example.com:80");
assert_eq!(base_uri.to_string(), "http://example.com/");
}
#[test]
fn https_default_port() {
let base_uri = BaseUri::from_uri_static("https://example.com:443");
assert_eq!(base_uri.to_string(), "https://example.com/");
}
#[test]
fn custom_port() {
let base_uri = BaseUri::from_uri_static("https://example.com:8443");
assert_eq!(base_uri.to_string(), "https://example.com:8443/");
}
}
mod with_origin {
use super::*;
#[test]
fn replaces_origin() {
let base_uri = BaseUri::from_uri_static("https://example.com/api/");
let new_origin = Origin::new(Scheme::HTTPS, "new-example.com:8080").unwrap();
let new_base_uri = base_uri.with_origin(new_origin.clone());
assert_eq!(new_base_uri.origin(), &new_origin);
assert_eq!(new_base_uri.scheme(), &Scheme::HTTPS);
assert_eq!(new_base_uri.authority().as_str(), "new-example.com:8080");
assert_eq!(new_base_uri.port(), 8080);
assert_eq!(new_base_uri.path().as_str(), "/api/");
assert_eq!(new_base_uri.to_string(), "https://new-example.com:8080/api/");
}
}
mod with_port {
use super::*;
#[test]
fn changes_port() {
let base_uri = BaseUri::from_uri_static("https://example.com/api/");
let new_base_uri = base_uri.with_port(8443);
assert_eq!(new_base_uri.origin().port(), 8443);
assert_eq!(new_base_uri.port(), 8443);
assert_eq!(new_base_uri.to_string(), "https://example.com:8443/api/");
}
}
#[cfg(feature = "serde")]
mod serde_tests {
use super::*;
#[test]
fn base_uri_roundtrip() {
let original = BaseUri::from_uri_static("https://example.com:8443/api/");
let json = serde_json::to_string(&original).unwrap();
assert_eq!(json, r#""https://example.com:8443/api/""#);
let deserialized: BaseUri = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn base_uri_deserialize_rejects_invalid() {
serde_json::from_str::<BaseUri>(r#""not-a-uri""#).unwrap_err();
}
}
}