use crate::{
data::{DataError, Fingerprint, Validate, ValidationError, validate::validate_irl},
emit_error,
};
use core::fmt;
use iri_string::{
convert::MappedToUri,
format::ToDedicatedString,
types::{IriStr, IriString, UriString},
};
use serde::{Deserialize, Serialize};
use std::{
cmp::Ordering,
hash::{Hash, Hasher},
};
const SEPARATOR: char = '~';
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
pub struct Account {
#[serde(rename = "homePage")]
home_page: IriString,
name: String,
}
impl Account {
fn from(home_page: &str, name: &str) -> Result<Self, DataError> {
let home_page = home_page.trim();
if home_page.is_empty() {
emit_error!(DataError::Validation(ValidationError::Empty(
"home_page".into()
)))
}
let home_page = IriStr::new(home_page)?;
validate_irl(home_page)?;
let name = name.trim();
if name.is_empty() {
emit_error!(DataError::Validation(ValidationError::Empty("name".into())))
}
Ok(Account {
home_page: home_page.into(),
name: name.to_owned(),
})
}
pub fn builder() -> AccountBuilder<'static> {
AccountBuilder::default()
}
pub fn home_page(&self) -> &IriStr {
&self.home_page
}
pub fn home_page_as_str(&self) -> &str {
self.home_page.as_str()
}
pub fn home_page_as_uri(&self) -> UriString {
let uri = MappedToUri::from(&self.home_page).to_dedicated_string();
uri.normalize().to_dedicated_string()
}
pub fn name(&self) -> &str {
&self.name
}
pub fn as_joined_str(&self) -> String {
format!("{}{}{}", self.home_page, SEPARATOR, self.name)
}
}
impl fmt::Display for Account {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Account{{ homePage: \"{}\", name: \"{}\" }}",
self.home_page_as_str(),
self.name
)
}
}
impl Fingerprint for Account {
fn fingerprint<H: Hasher>(&self, state: &mut H) {
let (x, y) = self.home_page.as_slice().to_absolute_and_fragment();
x.normalize().to_string().hash(state);
y.hash(state);
self.name.hash(state);
}
}
impl PartialOrd for Account {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let (x1, y1) = self.home_page.as_slice().to_absolute_and_fragment();
let (x2, y2) = other.home_page.as_slice().to_absolute_and_fragment();
match x1
.normalize()
.to_string()
.partial_cmp(&x2.normalize().to_string())
{
Some(Ordering::Equal) => match y1.partial_cmp(&y2) {
Some(Ordering::Equal) => {}
x => return x,
},
x => return x,
}
self.name.partial_cmp(&other.name)
}
}
impl Validate for Account {
fn validate(&self) -> Vec<ValidationError> {
let mut vec: Vec<ValidationError> = vec![];
validate_irl(self.home_page.as_ref()).unwrap_or_else(|x| vec.push(x));
if self.name.trim().is_empty() {
vec.push(ValidationError::Empty("name".into()))
}
vec
}
}
impl TryFrom<String> for Account {
type Error = DataError;
fn try_from(value: String) -> Result<Self, Self::Error> {
let mut parts = value.split(SEPARATOR);
Account::from(
parts.next().ok_or_else(|| {
DataError::Validation(ValidationError::MissingField("home_page".into()))
})?,
parts.next().ok_or_else(|| {
DataError::Validation(ValidationError::MissingField("name".into()))
})?,
)
}
}
#[derive(Debug, Default)]
pub struct AccountBuilder<'a> {
_home_page: Option<&'a IriStr>,
_name: &'a str,
}
impl<'a> AccountBuilder<'a> {
pub fn from(s: &str) -> Result<Account, DataError> {
let parts: Vec<_> = s.trim().split(SEPARATOR).collect();
if parts.len() < 2 {
emit_error!(DataError::Validation(ValidationError::ConstraintViolation(
"Missing separator".into()
)))
}
Account::builder()
.home_page(parts[0])?
.name(parts[1])?
.build()
}
pub fn home_page(mut self, val: &'a str) -> Result<Self, DataError> {
let home_page = val.trim();
if home_page.is_empty() {
emit_error!(DataError::Validation(ValidationError::Empty(
"home_page".into()
)))
} else {
let home_page = IriStr::new(home_page)?;
validate_irl(home_page)?;
self._home_page = Some(home_page);
Ok(self)
}
}
pub fn name(mut self, val: &'a str) -> Result<Self, DataError> {
let name = val.trim();
if name.is_empty() {
emit_error!(DataError::Validation(ValidationError::Empty("name".into())))
} else {
self._name = val;
Ok(self)
}
}
pub fn build(&self) -> Result<Account, DataError> {
if let Some(z_home_page) = self._home_page {
if self._name.is_empty() {
emit_error!(DataError::Validation(ValidationError::MissingField(
"name".into()
)))
} else {
Ok(Account {
home_page: z_home_page.into(),
name: self._name.to_owned(),
})
}
} else {
emit_error!(DataError::Validation(ValidationError::MissingField(
"home_page".into()
)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tracing_test::traced_test;
#[traced_test]
#[test]
fn test_serde() -> Result<(), DataError> {
const JSON: &str = r#"{"homePage":"https://inter.net/","name":"user"}"#;
let a1 = Account::builder()
.home_page("https://inter.net/")?
.name("user")?
.build()?;
let se_result = serde_json::to_string(&a1);
assert!(se_result.is_ok());
let json = se_result.unwrap();
assert_eq!(json, JSON);
let de_result = serde_json::from_str::<Account>(JSON);
assert!(de_result.is_ok());
let a2 = de_result.unwrap();
assert_eq!(a1, a2);
const JSON_: &str = r#"{"name":"user","homePage":"https://inter.net/"}"#;
let de_result = serde_json::from_str::<Account>(JSON_);
assert!(de_result.is_ok());
let a4 = de_result.unwrap();
assert_eq!(a1, a4);
Ok(())
}
#[traced_test]
#[test]
fn test_display() -> Result<(), DataError> {
const DISPLAY: &str =
r#"Account{ homePage: "http://résumé.example.org/", name: "zRésumé" }"#;
let a = Account::builder()
.home_page("http://résumé.example.org/")?
.name("zRésumé")?
.build()?;
let disp = a.to_string();
assert_eq!(disp, DISPLAY);
assert_eq!(a.home_page_as_str(), "http://résumé.example.org/");
assert_eq!(
a.home_page()
.encode_to_uri()
.to_dedicated_string()
.normalize()
.to_dedicated_string()
.as_str(),
"http://r%C3%A9sum%C3%A9.example.org/"
);
assert_eq!(
a.home_page()
.encode_to_uri()
.to_dedicated_string()
.normalize()
.to_dedicated_string()
.as_str(),
a.home_page_as_uri()
);
Ok(())
}
#[traced_test]
#[test]
fn test_validation() -> Result<(), DataError> {
let a = Account::builder()
.home_page("http://résumé.example.org/")?
.name("zRésumé")?
.build()?;
let r = a.validate();
assert!(r.is_empty());
Ok(())
}
#[test]
fn test_runtime_error_macro() -> Result<(), DataError> {
let r1 = Account::builder().home_page("");
let e1 = r1.err().unwrap();
assert!(matches!(e1, DataError::Validation { .. }));
let r2 = Account::builder()
.home_page("http://résumé.example.org/")?
.build();
let e2 = r2.err().unwrap();
assert!(matches!(e2, DataError::Validation { .. }));
Ok(())
}
}