use thiserror::Error;
#[derive(Debug, Error)]
pub enum CliError {
#[error(transparent)]
Core(#[from] klirr_core_invoice::Error),
#[error(transparent)]
Render(#[from] klirr_render_typst::Error),
#[error(transparent)]
EmailFromTui(#[from] EmailFromTuiError),
#[error(transparent)]
InvoiceDataFromTui(#[from] InvalidInvoiceData),
#[error("Specified output path does not exist: {path}")]
SpecifiedOutputPathDoesNotExist { path: String },
}
#[derive(Debug, Error)]
pub enum EmailFromTuiError {
#[error("Passwords do not match")]
PasswordDoesNotMatch,
#[error(
"Email password is too short, expected at least {min_length} characters, but found {actual_length}"
)]
EmailPasswordTooShort {
min_length: usize,
actual_length: usize,
},
#[error("Failed to parse email atom template: {underlying}")]
EmailAtomTemplateError { underlying: String },
#[error("Invalid email address for: {role}, because: {underlying}")]
InvalidEmailAddress { role: String, underlying: String },
#[error("Invalid name for email for: {role}, because: {underlying}")]
InvalidNameForEmail { role: String, underlying: String },
#[error("Invalid password for email {purpose}, because: {underlying}")]
InvalidPasswordForEmail { purpose: String, underlying: String },
#[error("Recipient addresses cannot be empty")]
RecipientAddressesCannotBeEmpty,
#[error("Failed to parse SMTP Server, because: {underlying}")]
InvalidSmtpServer { underlying: String },
}
impl EmailFromTuiError {
pub fn email_atom_template_error(underlying: impl std::fmt::Display) -> Self {
Self::EmailAtomTemplateError {
underlying: underlying.to_string(),
}
}
pub fn invalid_smtp_server(underlying: impl std::fmt::Display) -> Self {
Self::InvalidSmtpServer {
underlying: underlying.to_string(),
}
}
pub fn invalid_email_address_for_role<E: std::fmt::Display>(
role: impl std::fmt::Display,
) -> impl FnOnce(E) -> Self {
let role = role.to_string();
move |e| Self::InvalidEmailAddress {
role,
underlying: e.to_string(),
}
}
pub fn invalid_name_for_email_for_role<E: std::fmt::Display>(
role: impl std::fmt::Display,
) -> impl FnOnce(E) -> Self {
let role = role.to_string();
move |e| Self::InvalidNameForEmail {
role,
underlying: e.to_string(),
}
}
pub fn invalid_password_for_email_purpose<E: std::fmt::Display>(
purpose: impl std::fmt::Display,
) -> impl FnOnce(E) -> Self {
let purpose = purpose.to_string();
move |e| Self::InvalidPasswordForEmail {
purpose,
underlying: e.to_string(),
}
}
}
#[derive(Debug, Error)]
pub enum InvalidInvoiceData {
#[error("Failed to build CompanyInformation from Terminal UI input, because: {reason}")]
CompanyInformation { reason: String },
#[error("Failed to build InvoiceInfo from Terminal UI input, because: {reason}")]
InvoiceInfo { reason: String },
#[error("Failed to build PaymentInfo from Terminal UI input, because: {reason}")]
PaymentInfo { reason: String },
#[error("Failed to build ServiceFees from Terminal UI input, because: {reason}")]
ServiceFees { reason: String },
#[error("Invalid date, underlying: {underlying}")]
Date { underlying: String },
}
impl InvalidInvoiceData {
pub fn invalid_date(underlying: impl std::fmt::Display) -> Self {
Self::Date {
underlying: underlying.to_string(),
}
}
pub fn invalid_company_information(reason: impl std::fmt::Debug) -> Self {
Self::CompanyInformation {
reason: format!("{reason:?}"),
}
}
pub fn invalid_invoice_info(reason: impl std::fmt::Debug) -> Self {
Self::InvoiceInfo {
reason: format!("{reason:?}"),
}
}
pub fn invalid_payment_info(reason: impl std::fmt::Debug) -> Self {
Self::PaymentInfo {
reason: format!("{reason:?}"),
}
}
pub fn invalid_service_fees(reason: impl std::fmt::Debug) -> Self {
Self::ServiceFees {
reason: format!("{reason:?}"),
}
}
}
pub type InvoiceDataFromTuiError = InvalidInvoiceData;
pub type Result<T, E = CliError> = std::result::Result<T, E>;
pub type CliResult<T> = Result<T, CliError>;
#[cfg(test)]
mod tests {
use super::{EmailFromTuiError, InvalidInvoiceData};
use std::fmt;
struct DebugPassthrough(&'static str);
impl fmt::Debug for DebugPassthrough {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[test]
fn email_atom_template_error_keeps_underlying_message() {
let err = EmailFromTuiError::email_atom_template_error("bad template");
assert!(matches!(
err,
EmailFromTuiError::EmailAtomTemplateError { underlying } if underlying == "bad template"
));
}
#[test]
fn invalid_smtp_server_keeps_underlying_message() {
let err = EmailFromTuiError::invalid_smtp_server("bad smtp");
assert!(matches!(
err,
EmailFromTuiError::InvalidSmtpServer { underlying } if underlying == "bad smtp"
));
}
#[test]
fn invalid_email_address_for_role_mapper_sets_role_and_message() {
let err = EmailFromTuiError::invalid_email_address_for_role("Sender")("not-an-email");
assert!(matches!(
err,
EmailFromTuiError::InvalidEmailAddress { role, underlying }
if role == "Sender" && underlying == "not-an-email"
));
}
#[test]
fn invalid_name_for_email_for_role_mapper_sets_role_and_message() {
let err =
EmailFromTuiError::invalid_name_for_email_for_role("Reply-To")("name parse failed");
assert!(matches!(
err,
EmailFromTuiError::InvalidNameForEmail { role, underlying }
if role == "Reply-To" && underlying == "name parse failed"
));
}
#[test]
fn invalid_password_for_email_purpose_mapper_sets_purpose_and_message() {
let err =
EmailFromTuiError::invalid_password_for_email_purpose("SMTP app password")("too short");
assert!(matches!(
err,
EmailFromTuiError::InvalidPasswordForEmail { purpose, underlying }
if purpose == "SMTP app password" && underlying == "too short"
));
}
#[test]
fn invalid_date_keeps_underlying_message() {
let err = InvalidInvoiceData::invalid_date("invalid date");
assert!(matches!(
err,
InvalidInvoiceData::Date { underlying } if underlying == "invalid date"
));
}
#[test]
fn invalid_company_information_keeps_reason_message() {
let err =
InvalidInvoiceData::invalid_company_information(DebugPassthrough("company invalid"));
assert!(matches!(
err,
InvalidInvoiceData::CompanyInformation { reason } if reason == "company invalid"
));
}
#[test]
fn invalid_invoice_info_keeps_reason_message() {
let err = InvalidInvoiceData::invalid_invoice_info(DebugPassthrough("invoice invalid"));
assert!(matches!(
err,
InvalidInvoiceData::InvoiceInfo { reason } if reason == "invoice invalid"
));
}
#[test]
fn invalid_payment_info_keeps_reason_message() {
let err = InvalidInvoiceData::invalid_payment_info(DebugPassthrough("payment invalid"));
assert!(matches!(
err,
InvalidInvoiceData::PaymentInfo { reason } if reason == "payment invalid"
));
}
#[test]
fn invalid_service_fees_keeps_reason_message() {
let err = InvalidInvoiceData::invalid_service_fees(DebugPassthrough("fees invalid"));
assert!(matches!(
err,
InvalidInvoiceData::ServiceFees { reason } if reason == "fees invalid"
));
}
}