use crate::errors::ValidationError;
use crate::traits::ValueObject;
use url::Url;
pub type WebsiteInput = String;
pub type WebsiteOutput = String;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Website(String);
impl ValueObject for Website {
type Input = WebsiteInput;
type Output = WebsiteOutput;
type Error = ValidationError;
fn new(value: Self::Input) -> Result<Self, Self::Error> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(ValidationError::empty("Website"));
}
let parsed =
Url::parse(trimmed).map_err(|_| ValidationError::invalid("Website", trimmed))?;
match parsed.scheme() {
"http" | "https" => {}
_ => return Err(ValidationError::invalid("Website", trimmed)),
}
if parsed.host().is_none() {
return Err(ValidationError::invalid("Website", trimmed));
}
Ok(Self(parsed.to_string()))
}
fn value(&self) -> &Self::Output {
&self.0
}
fn into_inner(self) -> Self::Input {
self.0
}
}
impl Website {
pub fn is_https(&self) -> bool {
self.0.starts_with("https://")
}
pub fn host(&self) -> &str {
let after_scheme = self
.0
.find("://")
.map(|i| &self.0[i + 3..])
.unwrap_or(&self.0);
after_scheme.split('/').next().unwrap_or("")
}
}
impl TryFrom<&str> for Website {
type Error = ValidationError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value.to_owned())
}
}
impl std::fmt::Display for Website {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_https_url() {
let w = Website::new("https://example.com".into()).unwrap();
assert_eq!(w.value(), "https://example.com/");
}
#[test]
fn accepts_http_url() {
let w = Website::new("http://example.com".into()).unwrap();
assert_eq!(w.value(), "http://example.com/");
}
#[test]
fn normalises_host_to_lowercase() {
let w = Website::new("https://EXAMPLE.COM/Path".into()).unwrap();
assert_eq!(w.value(), "https://example.com/Path");
}
#[test]
fn trims_surrounding_whitespace() {
let w = Website::new(" https://example.com ".into()).unwrap();
assert_eq!(w.value(), "https://example.com/");
}
#[test]
fn rejects_ftp_scheme() {
assert!(Website::new("ftp://example.com".into()).is_err());
}
#[test]
fn rejects_non_url() {
assert!(Website::new("not-a-url".into()).is_err());
}
#[test]
fn rejects_empty_string() {
assert!(Website::new(String::new()).is_err());
}
#[test]
fn equal_after_normalisation() {
let a = Website::new("https://example.com/".into()).unwrap();
let b = Website::new("https://example.com/".into()).unwrap();
assert_eq!(a, b);
}
#[test]
fn is_https_returns_true_for_https() {
let w = Website::new("https://example.com".into()).unwrap();
assert!(w.is_https());
}
#[test]
fn is_https_returns_false_for_http() {
let w = Website::new("http://example.com".into()).unwrap();
assert!(!w.is_https());
}
#[test]
fn host_returns_domain() {
let w = Website::new("https://example.com/path".into()).unwrap();
assert_eq!(w.host(), "example.com");
}
#[test]
fn try_from_str() {
let w: Website = "https://example.com".try_into().unwrap();
assert!(w.is_https());
}
}