use serde::Serialize;
use crate::{
client::{ServerUrlError, WekanClientFactoryError},
command_result::{LogoutScope, RawApiResponseData},
config::profiles::ProfileStoreError,
exit_code::StableExitCode,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
BatchIncomplete,
InvalidInput,
ConfigurationError,
ProfileNotFound,
ProfileAlreadyExists,
ProfileServerMismatch,
ProfileInUse,
ProfileHasCredential,
InsecureTransport,
TransportError,
UnexpectedRedirect,
ProtocolError,
ApiResponseTooLarge,
LoginRejected,
LoginRateLimited,
CredentialNotFound,
CredentialExpired,
CredentialAlreadyExists,
AuthenticationRejected,
PermissionDenied,
NotFound,
RegistrationRejected,
RegistrationDisabled,
ServerError,
CredentialStoreUnavailable,
CredentialStoreFailed,
InternalError,
FileIoError,
}
impl ErrorCode {
pub const fn as_str(self) -> &'static str {
match self {
Self::BatchIncomplete => "batch_incomplete",
Self::InvalidInput => "invalid_input",
Self::ConfigurationError => "configuration_error",
Self::ProfileNotFound => "profile_not_found",
Self::ProfileAlreadyExists => "profile_already_exists",
Self::ProfileServerMismatch => "profile_server_mismatch",
Self::ProfileInUse => "profile_in_use",
Self::ProfileHasCredential => "profile_has_credential",
Self::InsecureTransport => "insecure_transport",
Self::TransportError => "transport_error",
Self::UnexpectedRedirect => "unexpected_redirect",
Self::ProtocolError => "protocol_error",
Self::ApiResponseTooLarge => "api_response_too_large",
Self::LoginRejected => "login_rejected",
Self::LoginRateLimited => "login_rate_limited",
Self::CredentialNotFound => "credential_not_found",
Self::CredentialExpired => "credential_expired",
Self::CredentialAlreadyExists => "credential_already_exists",
Self::AuthenticationRejected => "authentication_rejected",
Self::PermissionDenied => "permission_denied",
Self::NotFound => "not_found",
Self::RegistrationRejected => "registration_rejected",
Self::RegistrationDisabled => "registration_disabled",
Self::ServerError => "server_error",
Self::CredentialStoreUnavailable => "credential_store_unavailable",
Self::CredentialStoreFailed => "credential_store_failed",
Self::InternalError => "internal_error",
Self::FileIoError => "file_io_error",
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
pub struct ErrorDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub trello_import_result: Option<crate::command_result::TrelloImportResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub batch_result: Option<crate::command_result::CardBatchResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub invalid_label_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_saved: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_error: Option<crate::client::ResponseDecodeKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub http_status: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub http_success: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_limit_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_safe: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mutation_attempted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wekan_status_code: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_error_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_is_client_safe: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub account_created: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_created: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub two_factor_required: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_after_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_expires: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logout_scope: Option<LogoutScope>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remote_logout_completed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credential_stored: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_created: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub local_credential_removed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_deleted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response: Option<RawApiResponseData>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AppError {
code: ErrorCode,
message: String,
exit_code: StableExitCode,
details: Box<ErrorDetails>,
}
impl AppError {
pub fn new(code: ErrorCode, message: impl Into<String>, exit_code: StableExitCode) -> Self {
Self {
code,
message: message.into(),
exit_code,
details: Box::default(),
}
}
pub fn invalid_input(message: impl Into<String>) -> Self {
Self::new(
ErrorCode::InvalidInput,
message,
StableExitCode::InvalidInput,
)
}
pub fn configuration(message: impl Into<String>) -> Self {
Self::new(
ErrorCode::ConfigurationError,
message,
StableExitCode::Configuration,
)
}
pub fn with_details(mut self, details: ErrorDetails) -> Self {
self.details = Box::new(details);
self
}
pub fn with_profile_context(mut self, profile: String) -> Self {
self.details.profile = Some(profile);
self
}
pub fn with_session_created(mut self, created: bool) -> Self {
self.details.session_created = Some(created);
self
}
pub fn with_account_created(mut self, created: bool) -> Self {
self.details.account_created = Some(created);
self
}
pub fn with_profile_state(mut self, created: bool, active: bool) -> Self {
self.details.profile_created = Some(created);
self.details.profile_active = Some(active);
self
}
pub fn with_credential_stored(mut self, stored: bool) -> Self {
self.details.credential_stored = Some(stored);
self
}
pub fn with_user_deleted(mut self, deleted: bool) -> Self {
self.details.user_deleted = Some(deleted);
self
}
pub const fn code(&self) -> ErrorCode {
self.code
}
pub fn message(&self) -> &str {
&self.message
}
pub const fn exit_code(&self) -> StableExitCode {
self.exit_code
}
pub const fn details(&self) -> &ErrorDetails {
&self.details
}
}
impl From<ProfileStoreError> for AppError {
fn from(error: ProfileStoreError) -> Self {
Self::configuration(error.to_string())
}
}
impl std::fmt::Display for AppError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for AppError {}
impl From<ServerUrlError> for AppError {
fn from(error: ServerUrlError) -> Self {
match error {
error @ ServerUrlError::InsecureHttp => Self::new(
ErrorCode::InsecureTransport,
error.to_string(),
StableExitCode::Configuration,
),
error => Self::configuration(error.to_string()),
}
}
}
impl From<WekanClientFactoryError> for AppError {
fn from(error: WekanClientFactoryError) -> Self {
match error {
WekanClientFactoryError::ServerUrl(error) => Self::from(error),
WekanClientFactoryError::Build(_) => Self::new(
ErrorCode::InternalError,
"the HTTP client could not be initialized",
StableExitCode::Internal,
),
}
}
}