use std::fmt::{Debug, Display};
use std::str::FromStr;
#[cfg(feature = "mysql")]
use cot::db::impl_mysql::MySqlValueRef;
#[cfg(feature = "postgres")]
use cot::db::impl_postgres::PostgresValueRef;
#[cfg(feature = "sqlite")]
use cot::db::impl_sqlite::SqliteValueRef;
use cot::form::FormFieldValidationError;
use email_address::EmailAddress;
use thiserror::Error;
#[cfg(feature = "db")]
use crate::db::{ColumnType, DatabaseField, DbValue, FromDbValue, SqlxValueRef, ToDbValue};
const MAX_EMAIL_LENGTH: u32 = 254;
#[derive(Clone)]
pub struct Password(String);
impl Debug for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Password").field(&"**********").finish()
}
}
impl Password {
#[must_use]
pub fn new<T: Into<String>>(password: T) -> Self {
Self(password.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl From<&Password> for Password {
fn from(password: &Password) -> Self {
password.clone()
}
}
impl From<&str> for Password {
fn from(password: &str) -> Self {
Self::new(password)
}
}
impl From<String> for Password {
fn from(password: String) -> Self {
Self::new(password)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Url(url::Url);
impl Url {
pub fn new<S: AsRef<str>>(s: S) -> Result<Url, UrlParseError> {
url::Url::from_str(s.as_ref())
.map(Self)
.map_err(UrlParseError)
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
#[must_use]
pub fn into_string(self) -> String {
self.0.into()
}
#[must_use]
pub fn scheme(&self) -> &str {
self.0.scheme()
}
#[must_use]
pub fn host(&self) -> Option<&str> {
self.0.host_str()
}
#[must_use]
pub fn path(&self) -> &str {
self.0.path()
}
#[must_use]
pub fn query(&self) -> Option<&str> {
self.0.query()
}
#[must_use]
pub fn fragment(&self) -> Option<&str> {
self.0.fragment()
}
}
impl FromStr for Url {
type Err = UrlParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Url::new(s)
}
}
impl Display for Url {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Error)]
#[error(transparent)]
#[expect(missing_copy_implementations)] pub struct UrlParseError(url::ParseError);
impl From<UrlParseError> for FormFieldValidationError {
fn from(error: UrlParseError) -> Self {
FormFieldValidationError::from_string(error.to_string())
}
}
#[cfg(feature = "db")]
impl ToDbValue for Url {
fn to_db_value(&self) -> DbValue {
self.0.clone().to_string().into()
}
}
#[cfg(feature = "db")]
impl FromDbValue for Url {
#[cfg(feature = "sqlite")]
fn from_sqlite(value: SqliteValueRef<'_>) -> cot::db::Result<Self>
where
Self: Sized,
{
Url::new(value.get::<String>()?).map_err(cot::db::DatabaseError::value_decode)
}
#[cfg(feature = "postgres")]
fn from_postgres(value: PostgresValueRef<'_>) -> cot::db::Result<Self>
where
Self: Sized,
{
Url::new(value.get::<String>()?).map_err(cot::db::DatabaseError::value_decode)
}
#[cfg(feature = "mysql")]
fn from_mysql(value: MySqlValueRef<'_>) -> cot::db::Result<Self>
where
Self: Sized,
{
Url::new(value.get::<String>()?).map_err(cot::db::DatabaseError::value_decode)
}
}
#[cfg(feature = "db")]
impl DatabaseField for Url {
const TYPE: ColumnType = ColumnType::Text;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Email(EmailAddress);
impl Email {
pub fn new<S: AsRef<str>>(email: S) -> Result<Email, EmailParseError> {
EmailAddress::from_str(email.as_ref())
.map(Self)
.map_err(EmailParseError)
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
#[must_use]
pub fn domain(&self) -> &str {
self.0.domain()
}
#[must_use]
pub fn to_uri(&self) -> String {
self.0.to_uri()
}
#[must_use]
pub fn to_display(&self, display_name: &str) -> String {
self.0.to_display(display_name)
}
#[must_use]
pub fn email(&self) -> String {
self.0.email()
}
#[must_use]
pub fn local_part(&self) -> &str {
self.0.local_part()
}
#[must_use]
pub fn display_part(&self) -> &str {
self.0.display_part()
}
}
impl FromStr for Email {
type Err = EmailParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Email::new(s)
}
}
impl TryFrom<&str> for Email {
type Error = EmailParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Email::new(value)
}
}
impl TryFrom<String> for Email {
type Error = EmailParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Email::new(value)
}
}
#[derive(Debug, Error)]
#[error(transparent)]
pub struct EmailParseError(email_address::Error);
impl From<EmailParseError> for FormFieldValidationError {
fn from(error: EmailParseError) -> Self {
FormFieldValidationError::from_string(error.to_string())
}
}
#[cfg(feature = "db")]
impl ToDbValue for Email {
fn to_db_value(&self) -> DbValue {
self.0.clone().email().into()
}
}
#[cfg(feature = "db")]
impl FromDbValue for Email {
#[cfg(feature = "sqlite")]
fn from_sqlite(value: SqliteValueRef<'_>) -> cot::db::Result<Self>
where
Self: Sized,
{
Email::new(value.get::<String>()?).map_err(cot::db::DatabaseError::value_decode)
}
#[cfg(feature = "postgres")]
fn from_postgres(value: PostgresValueRef<'_>) -> cot::db::Result<Self>
where
Self: Sized,
{
Email::new(value.get::<String>()?).map_err(cot::db::DatabaseError::value_decode)
}
#[cfg(feature = "mysql")]
fn from_mysql(value: MySqlValueRef<'_>) -> cot::db::Result<Self>
where
Self: Sized,
{
Email::new(value.get::<String>()?).map_err(cot::db::DatabaseError::value_decode)
}
}
#[cfg(feature = "db")]
impl DatabaseField for Email {
const TYPE: ColumnType = ColumnType::String(MAX_EMAIL_LENGTH);
}
impl Display for Email {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0.as_str())
}
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
use super::*;
use crate::Template;
#[test]
fn url_new() {
let parse_url = Url::new("https://example.com/").unwrap();
assert_eq!(parse_url.as_str(), "https://example.com/");
assert_eq!(parse_url.to_string(), "https://example.com/");
assert_eq!(parse_url.scheme(), "https");
assert_eq!(parse_url.host(), Some("example.com"));
}
#[test]
fn url_new_normalize() {
let parse_url = Url::new("https://example.com").unwrap();
assert_eq!(parse_url.as_str(), "https://example.com/"); }
#[test]
fn askama_renders_url_field() {
#[derive(Template)]
#[template(source = "{{ url }}", ext = "html")]
struct UrlTestTemplate {
url: Url,
}
let url = Url::new("https://example.com").unwrap();
let tpl = UrlTestTemplate { url };
let rendered = tpl.render().expect("template failed to render");
assert_eq!(rendered, "https://example.com/");
}
#[test]
fn password_debug() {
let password = Password::new("password");
assert_eq!(format!("{password:?}"), "Password(\"**********\")");
}
#[test]
fn password_str() {
let password = Password::new("password");
assert_eq!(password.as_str(), "password");
assert_eq!(password.into_string(), "password");
}
#[test]
fn test_valid_email_creation() {
let email = Email::new("user@example.com").unwrap();
assert_eq!(email.as_str(), "user@example.com");
assert_eq!(email.to_string(), "user@example.com");
assert_eq!(email.domain(), "example.com");
}
#[test]
fn test_invalid_email_creation() {
let result = Email::new("invalid");
assert!(result.is_err());
}
#[test]
fn test_from_str_trait() {
let email: Email = "user@example.com".parse().unwrap();
assert_eq!(email.as_str(), "user@example.com");
}
#[test]
fn test_try_from_trait() {
let email = Email::try_from("user@example.com").unwrap();
assert_eq!(email.as_str(), "user@example.com");
}
#[test]
fn askama_renders_email_field() {
#[derive(Template)]
#[template(source = "{{ email }}", ext = "html")]
struct EmailTestTemplate {
email: Email,
}
let email = Email::new("foo@example.com").unwrap();
let tpl = EmailTestTemplate { email };
let rendered = tpl.render().expect("template failed to render");
assert_eq!(rendered, "foo@example.com");
}
#[cfg(feature = "db")]
mod db_tests {
use super::*;
use crate::db::ToDbValue;
#[test]
fn test_email_to_db_value() {
let email = Email::new("user@example.com").unwrap();
let db_value = email.to_db_value();
let email_str = email.as_str();
let db_value_str = format!("{db_value:?}");
assert!(db_value_str.contains(email_str));
}
#[test]
fn test_email_to_db_value_is_normalized() {
let with_display = Email::new("John Doe <user@example.com>").unwrap();
let bare = Email::new("user@example.com").unwrap();
let db1 = with_display.to_db_value();
let db2 = bare.to_db_value();
assert_eq!(db1, db2);
}
#[test]
fn test_url_to_db_value() {
let url = Url::new("https://example.com/").unwrap();
let db_value = url.to_db_value();
let url_str = url.as_str();
let db_value_str = format!("{db_value:?}");
assert!(db_value_str.contains(url_str));
}
}
}