use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use url::Url;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Uri {
canonical: String,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum UriError {
#[error("URI is empty")]
Empty,
#[error("invalid URI {input:?}: {reason}")]
Invalid {
input: String,
reason: String,
},
}
impl Uri {
pub fn parse(input: impl Into<String>) -> Result<Self, UriError> {
let canonical: String = input.into();
if canonical.is_empty() {
return Err(UriError::Empty);
}
Url::parse(&canonical).map_err(|e| UriError::Invalid {
input: canonical.clone(),
reason: e.to_string(),
})?;
Ok(Self { canonical })
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.canonical
}
pub fn to_url(&self) -> Result<Url, url::ParseError> {
Url::parse(&self.canonical)
}
}
impl fmt::Display for Uri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.canonical)
}
}
impl FromStr for Uri {
type Err = UriError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl AsRef<str> for Uri {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl std::ops::Deref for Uri {
type Target = str;
fn deref(&self) -> &str {
&self.canonical
}
}
impl std::borrow::Borrow<str> for Uri {
fn borrow(&self) -> &str {
&self.canonical
}
}
impl Serialize for Uri {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.canonical.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Uri {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::parse(s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_https() {
let u = Uri::parse("https://example.com/path?q=1").unwrap();
assert_eq!(u.as_str(), "https://example.com/path?q=1");
assert_eq!(u.to_url().unwrap().host_str(), Some("example.com"));
}
#[test]
fn parses_mailto() {
let u = Uri::parse("mailto:alice@example.com").unwrap();
assert_eq!(u.to_url().unwrap().scheme(), "mailto");
}
#[test]
fn rejects_empty() {
assert!(matches!(Uri::parse(""), Err(UriError::Empty)));
}
#[test]
fn rejects_no_scheme() {
assert!(matches!(
Uri::parse("just a string"),
Err(UriError::Invalid { .. })
));
}
#[test]
fn fromstr_works() {
let u: Uri = "https://example.com".parse().unwrap();
assert_eq!(u.as_str(), "https://example.com");
}
#[test]
fn serde_roundtrip_preserves_wire_form() {
let original = "https://example.com/a/b?c=d";
let u = Uri::parse(original).unwrap();
let s = serde_json::to_string(&u).unwrap();
assert_eq!(s, format!("\"{original}\""));
let u2: Uri = serde_json::from_str(&s).unwrap();
assert_eq!(u, u2);
}
#[test]
fn serde_rejects_invalid_on_deserialize() {
let bad: Result<Uri, _> = serde_json::from_str("\"not a uri\"");
assert!(bad.is_err());
}
}