use super::generics::{FromAttributes, ParseFrom};
use crate::util::{date::parse_date, text::bytes_to_string};
use chrono::{DateTime, Utc};
use compact_str::CompactString;
use serde_json::Value;
use std::ops::Deref;
use std::sync::Arc;
pub type SmallString = CompactString;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Url(String);
impl Url {
#[inline]
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
#[inline]
pub fn into_inner(self) -> String {
self.0
}
}
impl Deref for Url {
type Target = str;
#[inline]
fn deref(&self) -> &str {
&self.0
}
}
impl From<String> for Url {
#[inline]
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Url {
#[inline]
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl AsRef<str> for Url {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Url {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for Url {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for Url {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<String> for Url {
fn eq(&self, other: &String) -> bool {
&self.0 == other
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MimeType(Arc<str>);
impl serde::Serialize for MimeType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0)
}
}
impl<'de> serde::Deserialize<'de> for MimeType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = <String as serde::Deserialize>::deserialize(deserializer)?;
Ok(Self::new(s))
}
}
impl MimeType {
#[inline]
pub fn new(s: impl AsRef<str>) -> Self {
Self(Arc::from(s.as_ref()))
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
pub const TEXT_HTML: &'static str = "text/html";
pub const TEXT_PLAIN: &'static str = "text/plain";
pub const APPLICATION_XML: &'static str = "application/xml";
pub const APPLICATION_JSON: &'static str = "application/json";
}
impl Default for MimeType {
#[inline]
fn default() -> Self {
Self(Arc::from(""))
}
}
impl Deref for MimeType {
type Target = str;
#[inline]
fn deref(&self) -> &str {
&self.0
}
}
impl From<String> for MimeType {
#[inline]
fn from(s: String) -> Self {
Self(Arc::from(s.as_str()))
}
}
impl From<&str> for MimeType {
#[inline]
fn from(s: &str) -> Self {
Self(Arc::from(s))
}
}
impl AsRef<str> for MimeType {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for MimeType {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for MimeType {
fn eq(&self, other: &str) -> bool {
&*self.0 == other
}
}
impl PartialEq<&str> for MimeType {
fn eq(&self, other: &&str) -> bool {
&*self.0 == *other
}
}
impl PartialEq<String> for MimeType {
fn eq(&self, other: &String) -> bool {
&*self.0 == other
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Email(String);
impl Email {
#[inline]
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
#[inline]
pub fn as_str(&self) -> &str {
&self.0
}
#[inline]
pub fn into_inner(self) -> String {
self.0
}
}
impl Deref for Email {
type Target = str;
#[inline]
fn deref(&self) -> &str {
&self.0
}
}
impl From<String> for Email {
#[inline]
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Email {
#[inline]
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl AsRef<str> for Email {
#[inline]
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Email {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl PartialEq<str> for Email {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for Email {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<String> for Email {
fn eq(&self, other: &String) -> bool {
&self.0 == other
}
}
#[derive(Debug, Clone, Default)]
pub struct Link {
pub href: Url,
pub rel: Option<SmallString>,
pub link_type: Option<MimeType>,
pub title: Option<String>,
pub length: Option<String>,
pub hreflang: Option<SmallString>,
pub thr_count: Option<u32>,
pub thr_updated: Option<DateTime<Utc>>,
}
impl Link {
#[inline]
pub fn new(href: impl Into<Url>, rel: impl AsRef<str>) -> Self {
Self {
href: href.into(),
rel: Some(rel.as_ref().into()),
link_type: None,
title: None,
length: None,
hreflang: None,
thr_count: None,
thr_updated: None,
}
}
#[inline]
pub fn alternate(href: impl Into<Url>) -> Self {
Self::new(href, "alternate")
}
#[inline]
pub fn self_link(href: impl Into<Url>, mime_type: impl Into<MimeType>) -> Self {
Self {
href: href.into(),
rel: Some("self".into()),
link_type: Some(mime_type.into()),
title: None,
length: None,
hreflang: None,
thr_count: None,
thr_updated: None,
}
}
#[inline]
pub fn enclosure(href: impl Into<Url>, mime_type: Option<MimeType>) -> Self {
Self {
href: href.into(),
rel: Some("enclosure".into()),
link_type: mime_type,
title: None,
length: None,
hreflang: None,
thr_count: None,
thr_updated: None,
}
}
#[inline]
pub fn related(href: impl Into<Url>) -> Self {
Self::new(href, "related")
}
#[inline]
pub fn banner(href: impl Into<Url>) -> Self {
Self::new(href, "banner")
}
#[inline]
pub fn hub(href: impl Into<Url>) -> Self {
Self::new(href, "hub")
}
#[inline]
#[must_use]
pub fn with_type(mut self, mime_type: impl Into<MimeType>) -> Self {
self.link_type = Some(mime_type.into());
self
}
}
#[derive(Debug, Clone, Default)]
pub struct Person {
pub name: Option<SmallString>,
pub email: Option<Email>,
pub uri: Option<String>,
pub avatar: Option<String>,
}
impl Person {
#[inline]
pub fn from_name(name: impl AsRef<str>) -> Self {
Self {
name: Some(name.as_ref().into()),
email: None,
uri: None,
avatar: None,
}
}
#[must_use]
pub fn flat_string(&self) -> Option<SmallString> {
match (&self.name, &self.email) {
(Some(name), Some(email)) => Some(format!("{name} ({email})").into()),
(Some(name), None) => Some(name.clone()),
(None, Some(email)) => Some(email.as_str().into()),
(None, None) => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Tag {
pub term: SmallString,
pub scheme: Option<SmallString>,
pub label: Option<SmallString>,
}
impl Tag {
#[inline]
pub fn new(term: impl AsRef<str>) -> Self {
Self {
term: term.as_ref().into(),
scheme: None,
label: None,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Cloud {
pub domain: Option<String>,
pub port: Option<String>,
pub path: Option<String>,
pub register_procedure: Option<String>,
pub protocol: Option<String>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct TextInput {
pub title: Option<String>,
pub description: Option<String>,
pub name: Option<String>,
pub link: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Image {
pub url: Url,
pub title: Option<String>,
pub link: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub description: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Enclosure {
pub url: Url,
pub length: Option<String>,
pub enclosure_type: Option<MimeType>,
pub title: Option<String>,
pub duration: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Content {
pub value: String,
pub content_type: Option<MimeType>,
pub language: Option<SmallString>,
pub base: Option<String>,
pub src: Option<String>,
}
impl Content {
#[inline]
pub fn html(value: impl Into<String>) -> Self {
Self {
value: value.into(),
content_type: Some(MimeType::new(MimeType::TEXT_HTML)),
language: None,
base: None,
src: None,
}
}
#[inline]
pub fn plain(value: impl Into<String>) -> Self {
Self {
value: value.into(),
content_type: Some(MimeType::new(MimeType::TEXT_PLAIN)),
language: None,
base: None,
src: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextType {
Text,
Html,
Xhtml,
}
#[derive(Debug, Clone)]
pub struct TextConstruct {
pub value: String,
pub content_type: TextType,
pub language: Option<SmallString>,
pub base: Option<String>,
}
impl TextConstruct {
#[inline]
pub fn text(value: impl Into<String>) -> Self {
Self {
value: value.into(),
content_type: TextType::Text,
language: None,
base: None,
}
}
#[inline]
pub fn html(value: impl Into<String>) -> Self {
Self {
value: value.into(),
content_type: TextType::Html,
language: None,
base: None,
}
}
#[inline]
#[must_use]
pub fn with_language(mut self, language: impl AsRef<str>) -> Self {
self.language = Some(language.as_ref().into());
self
}
}
#[derive(Debug, Clone)]
pub struct Generator {
pub name: String,
pub href: Option<String>,
pub version: Option<SmallString>,
}
#[derive(Debug, Clone)]
pub struct Source {
pub title: Option<String>,
pub href: Option<String>,
pub link: Option<String>,
pub author: Option<String>,
pub id: Option<String>,
pub links: Vec<Link>,
pub updated: Option<DateTime<Utc>>,
pub updated_str: Option<String>,
pub rights: Option<String>,
pub guidislink: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct MediaThumbnail {
pub url: Url,
pub width: Option<String>,
pub height: Option<String>,
pub time: Option<String>,
}
#[derive(Debug, Clone)]
pub struct MediaContent {
pub url: Url,
pub content_type: Option<MimeType>,
pub medium: Option<String>,
pub filesize: Option<String>,
pub width: Option<String>,
pub height: Option<String>,
pub duration: Option<String>,
pub bitrate: Option<String>,
pub lang: Option<String>,
pub channels: Option<String>,
pub codec: Option<String>,
pub expression: Option<String>,
pub isdefault: Option<String>,
pub samplingrate: Option<String>,
pub framerate: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MediaRating {
pub scheme: Option<String>,
pub content: String,
}
impl FromAttributes for Link {
fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
where
I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
{
let mut href = None;
let mut rel = None;
let mut link_type = None;
let mut title = None;
let mut hreflang = None;
let mut length = None;
let mut thr_count = None;
let mut thr_updated = None;
for attr in attrs {
if attr.value.len() > max_attr_length {
continue;
}
match attr.key.as_ref() {
b"href" => href = Some(bytes_to_string(&attr.value)),
b"rel" => rel = Some(bytes_to_string(&attr.value)),
b"type" => link_type = Some(bytes_to_string(&attr.value)),
b"title" => title = Some(bytes_to_string(&attr.value)),
b"hreflang" => hreflang = Some(bytes_to_string(&attr.value)),
b"length" => length = Some(bytes_to_string(&attr.value)),
b"thr:count" => {
thr_count = bytes_to_string(&attr.value).trim().parse::<u32>().ok();
}
b"thr:updated" => {
thr_updated = parse_date(bytes_to_string(&attr.value).trim());
}
_ => {}
}
}
let rel_str: Option<SmallString> = rel
.map(std::convert::Into::into)
.or_else(|| Some("alternate".into()));
let resolved_type = link_type
.map(MimeType::new)
.or_else(|| match rel_str.as_deref() {
Some("self") => Some(MimeType::new("application/atom+xml")),
_ => Some(MimeType::new("text/html")),
});
href.map(|href| Self {
href: Url::new(href),
rel: rel_str,
link_type: resolved_type,
title,
length,
hreflang: hreflang.map(std::convert::Into::into),
thr_count,
thr_updated,
})
}
}
impl FromAttributes for Tag {
fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
where
I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
{
let mut term = None;
let mut scheme = None;
let mut label = None;
for attr in attrs {
if attr.value.len() > max_attr_length {
continue;
}
match attr.key.as_ref() {
b"term" => term = Some(bytes_to_string(&attr.value)),
b"scheme" | b"domain" => scheme = Some(bytes_to_string(&attr.value)),
b"label" => label = Some(bytes_to_string(&attr.value)),
_ => {}
}
}
term.map(|term| Self {
term: term.into(),
scheme: scheme.map(std::convert::Into::into),
label: label.map(std::convert::Into::into),
})
}
}
impl FromAttributes for Enclosure {
fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
where
I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
{
let mut url = None;
let mut length = None;
let mut enclosure_type = None;
for attr in attrs {
if attr.value.len() > max_attr_length {
continue;
}
match attr.key.as_ref() {
b"url" => url = Some(bytes_to_string(&attr.value)),
b"length" => length = Some(bytes_to_string(&attr.value)),
b"type" => enclosure_type = Some(bytes_to_string(&attr.value)),
_ => {}
}
}
url.map(|url| Self {
url: Url::new(url),
length,
enclosure_type: enclosure_type.map(MimeType::new),
title: None,
duration: None,
})
}
}
impl FromAttributes for MediaThumbnail {
fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
where
I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
{
let mut url = None;
let mut width = None;
let mut height = None;
let mut time = None;
for attr in attrs {
if attr.value.len() > max_attr_length {
continue;
}
match attr.key.as_ref() {
b"url" => url = Some(bytes_to_string(&attr.value)),
b"width" => width = Some(bytes_to_string(&attr.value)),
b"height" => height = Some(bytes_to_string(&attr.value)),
b"time" => time = Some(bytes_to_string(&attr.value)),
_ => {}
}
}
url.map(|url| Self {
url: Url::new(url),
width,
height,
time,
})
}
}
impl FromAttributes for MediaContent {
fn from_attributes<'a, I>(attrs: I, max_attr_length: usize) -> Option<Self>
where
I: Iterator<Item = quick_xml::events::attributes::Attribute<'a>>,
{
let mut url = None;
let mut content_type = None;
let mut medium = None;
let mut filesize = None;
let mut width = None;
let mut height = None;
let mut duration = None;
let mut bitrate = None;
let mut lang = None;
let mut channels = None;
let mut codec = None;
let mut expression = None;
let mut isdefault = None;
let mut samplingrate = None;
let mut framerate = None;
for attr in attrs {
if attr.value.len() > max_attr_length {
continue;
}
match attr.key.as_ref() {
b"url" => url = Some(bytes_to_string(&attr.value)),
b"type" => content_type = Some(bytes_to_string(&attr.value)),
b"medium" => medium = Some(bytes_to_string(&attr.value)),
b"fileSize" => filesize = Some(bytes_to_string(&attr.value)),
b"width" => width = Some(bytes_to_string(&attr.value)),
b"height" => height = Some(bytes_to_string(&attr.value)),
b"duration" => duration = Some(bytes_to_string(&attr.value)),
b"bitrate" => bitrate = Some(bytes_to_string(&attr.value)),
b"lang" => lang = Some(bytes_to_string(&attr.value)),
b"channels" => channels = Some(bytes_to_string(&attr.value)),
b"codec" => codec = Some(bytes_to_string(&attr.value)),
b"expression" => expression = Some(bytes_to_string(&attr.value)),
b"isDefault" => isdefault = Some(bytes_to_string(&attr.value)),
b"samplingrate" => samplingrate = Some(bytes_to_string(&attr.value)),
b"framerate" => framerate = Some(bytes_to_string(&attr.value)),
_ => {}
}
}
url.map(|url| Self {
url: Url::new(url),
content_type: content_type.map(MimeType::new),
medium,
filesize,
width,
height,
duration,
bitrate,
lang,
channels,
codec,
expression,
isdefault,
samplingrate,
framerate,
})
}
}
impl ParseFrom<&Value> for Person {
fn parse_from(json: &Value) -> Option<Self> {
json.as_object().map(|obj| Self {
name: obj
.get("name")
.and_then(Value::as_str)
.map(std::convert::Into::into),
email: None, uri: obj.get("url").and_then(Value::as_str).map(String::from),
avatar: obj.get("avatar").and_then(Value::as_str).map(String::from),
})
}
}
impl ParseFrom<&Value> for Enclosure {
fn parse_from(json: &Value) -> Option<Self> {
let obj = json.as_object()?;
let url = obj.get("url").and_then(Value::as_str)?;
Some(Self {
url: Url::new(url),
length: obj
.get("size_in_bytes")
.and_then(Value::as_u64)
.map(|v| v.to_string()),
enclosure_type: obj
.get("mime_type")
.and_then(Value::as_str)
.map(MimeType::new),
title: obj.get("title").and_then(Value::as_str).map(String::from),
duration: obj
.get("duration_in_seconds")
.and_then(Value::as_u64)
.map(|v| v.to_string()),
})
}
}
#[derive(Debug, Clone, Default)]
pub struct MediaCredit {
pub role: Option<String>,
pub scheme: Option<String>,
pub content: String,
}
#[derive(Debug, Clone, Default)]
pub struct MediaCopyright {
pub url: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_link_default() {
let link = Link::default();
assert!(link.href.is_empty());
assert!(link.rel.is_none());
}
#[test]
fn test_link_builders() {
let link = Link::alternate("https://example.com");
assert_eq!(link.href, "https://example.com");
assert_eq!(link.rel.as_deref(), Some("alternate"));
let link = Link::self_link("https://example.com/feed", "application/feed+json");
assert_eq!(link.rel.as_deref(), Some("self"));
assert_eq!(link.link_type.as_deref(), Some("application/feed+json"));
let link = Link::enclosure("https://example.com/audio.mp3", Some("audio/mpeg".into()));
assert_eq!(link.rel.as_deref(), Some("enclosure"));
assert_eq!(link.link_type.as_deref(), Some("audio/mpeg"));
let link = Link::related("https://other.com");
assert_eq!(link.rel.as_deref(), Some("related"));
}
#[test]
fn test_tag_builder() {
let tag = Tag::new("rust");
assert_eq!(tag.term, "rust");
assert!(tag.scheme.is_none());
}
#[test]
fn test_text_construct_builders() {
let text = TextConstruct::text("Hello");
assert_eq!(text.value, "Hello");
assert_eq!(text.content_type, TextType::Text);
let html = TextConstruct::html("<p>Hello</p>");
assert_eq!(html.content_type, TextType::Html);
let with_lang = TextConstruct::text("Hello").with_language("en");
assert_eq!(with_lang.language.as_deref(), Some("en"));
}
#[test]
fn test_content_builders() {
let html = Content::html("<p>Content</p>");
assert_eq!(html.content_type.as_deref(), Some("text/html"));
let plain = Content::plain("Content");
assert_eq!(plain.content_type.as_deref(), Some("text/plain"));
}
#[test]
fn test_person_default() {
let person = Person::default();
assert!(person.name.is_none());
assert!(person.email.is_none());
assert!(person.uri.is_none());
}
#[test]
fn test_person_parse_from_json() {
let json = json!({"name": "John Doe", "url": "https://example.com"});
let person = Person::parse_from(&json).unwrap();
assert_eq!(person.name.as_deref(), Some("John Doe"));
assert_eq!(person.uri.as_deref(), Some("https://example.com"));
assert!(person.email.is_none());
}
#[test]
fn test_person_parse_from_empty_json() {
let json = json!({});
let person = Person::parse_from(&json).unwrap();
assert!(person.name.is_none());
}
#[test]
fn test_enclosure_parse_from_json() {
let json = json!({
"url": "https://example.com/file.mp3",
"mime_type": "audio/mpeg",
"size_in_bytes": 12345
});
let enclosure = Enclosure::parse_from(&json).unwrap();
assert_eq!(enclosure.url, "https://example.com/file.mp3");
assert_eq!(enclosure.enclosure_type.as_deref(), Some("audio/mpeg"));
assert_eq!(enclosure.length.as_deref(), Some("12345"));
}
#[test]
fn test_enclosure_parse_from_json_missing_url() {
let json = json!({"mime_type": "audio/mpeg"});
assert!(Enclosure::parse_from(&json).is_none());
}
#[test]
fn test_text_type_equality() {
assert_eq!(TextType::Text, TextType::Text);
assert_ne!(TextType::Text, TextType::Html);
}
#[test]
fn test_url_new() {
let url = Url::new("https://example.com");
assert_eq!(url.as_str(), "https://example.com");
}
#[test]
fn test_url_from_string() {
let url: Url = String::from("https://example.com").into();
assert_eq!(url.as_str(), "https://example.com");
}
#[test]
fn test_url_from_str() {
let url: Url = "https://example.com".into();
assert_eq!(url.as_str(), "https://example.com");
}
#[test]
fn test_url_deref() {
let url = Url::new("https://example.com");
assert_eq!(url.len(), 19);
assert!(url.starts_with("https://"));
}
#[test]
fn test_url_into_inner() {
let url = Url::new("https://example.com");
let inner = url.into_inner();
assert_eq!(inner, "https://example.com");
}
#[test]
fn test_url_default() {
let url = Url::default();
assert_eq!(url.as_str(), "");
}
#[test]
fn test_url_clone() {
let url1 = Url::new("https://example.com");
let url2 = url1.clone();
assert_eq!(url1, url2);
}
#[test]
fn test_mime_type_new() {
let mime = MimeType::new("text/html");
assert_eq!(mime.as_str(), "text/html");
}
#[test]
fn test_mime_type_from_string() {
let mime: MimeType = String::from("application/json").into();
assert_eq!(mime.as_str(), "application/json");
}
#[test]
fn test_mime_type_from_str() {
let mime: MimeType = "text/plain".into();
assert_eq!(mime.as_str(), "text/plain");
}
#[test]
fn test_mime_type_deref() {
let mime = MimeType::new("text/html");
assert_eq!(mime.len(), 9);
assert!(mime.starts_with("text/"));
}
#[test]
fn test_mime_type_default() {
let mime = MimeType::default();
assert_eq!(mime.as_str(), "");
}
#[test]
fn test_mime_type_clone() {
let mime1 = MimeType::new("application/xml");
let mime2 = mime1.clone();
assert_eq!(mime1, mime2);
}
#[test]
fn test_mime_type_constants() {
assert_eq!(MimeType::TEXT_HTML, "text/html");
assert_eq!(MimeType::TEXT_PLAIN, "text/plain");
assert_eq!(MimeType::APPLICATION_XML, "application/xml");
assert_eq!(MimeType::APPLICATION_JSON, "application/json");
}
#[test]
fn test_email_new() {
let email = Email::new("user@example.com");
assert_eq!(email.as_str(), "user@example.com");
}
#[test]
fn test_email_from_string() {
let email: Email = String::from("user@example.com").into();
assert_eq!(email.as_str(), "user@example.com");
}
#[test]
fn test_email_from_str() {
let email: Email = "user@example.com".into();
assert_eq!(email.as_str(), "user@example.com");
}
#[test]
fn test_email_deref() {
let email = Email::new("user@example.com");
assert_eq!(email.len(), 16);
assert!(email.contains('@'));
}
#[test]
fn test_email_into_inner() {
let email = Email::new("user@example.com");
let inner = email.into_inner();
assert_eq!(inner, "user@example.com");
}
#[test]
fn test_email_default() {
let email = Email::default();
assert_eq!(email.as_str(), "");
}
#[test]
fn test_email_clone() {
let email1 = Email::new("user@example.com");
let email2 = email1.clone();
assert_eq!(email1, email2);
}
}