use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApiErrorCode {
FewMemory,
BadMemory,
MissingConfig,
InvalidDependency,
MissingMain,
InvalidMain,
InvalidDisplayName,
MissingDisplayName,
InvalidMemory,
MissingMemory,
InvalidVersion,
MissingVersion,
InvalidAccessToken,
RegexValidation,
InvalidStart,
InvalidSubdomain,
RateLimit,
NotFound,
AppNotFound,
InvalidFile,
KeepCalm,
ContainerAlreadyStarted,
InvalidTimeRange,
NoCustomDomain,
InvalidVersionId,
DatabaseTypeInvalid,
DatabaseVersionInvalid,
RestoreInProgress,
DailySnapshotsLimitReached,
InvalidScope,
UpgradeRequired,
Unknown(String),
}
impl ApiErrorCode {
fn as_str(&self) -> &str {
match self {
Self::FewMemory => "FEW_MEMORY",
Self::BadMemory => "BAD_MEMORY",
Self::MissingConfig => "MISSING_CONFIG",
Self::InvalidDependency => "INVALID_DEPENDENCY",
Self::MissingMain => "MISSING_MAIN",
Self::InvalidMain => "INVALID_MAIN",
Self::InvalidDisplayName => "INVALID_DISPLAY_NAME",
Self::MissingDisplayName => "MISSING_DISPLAY_NAME",
Self::InvalidMemory => "INVALID_MEMORY",
Self::MissingMemory => "MISSING_MEMORY",
Self::InvalidVersion => "INVALID_VERSION",
Self::MissingVersion => "MISSING_VERSION",
Self::InvalidAccessToken => "INVALID_ACCESS_TOKEN",
Self::RegexValidation => "REGEX_VALIDATION",
Self::InvalidStart => "INVALID_START",
Self::InvalidSubdomain => "INVALID_SUBDOMAIN",
Self::RateLimit => "RATE_LIMIT",
Self::NotFound => "NOT_FOUND",
Self::AppNotFound => "APP_NOT_FOUND",
Self::InvalidFile => "INVALID_FILE",
Self::KeepCalm => "KEEP_CALM",
Self::ContainerAlreadyStarted => "CONTAINER_ALREADY_STARTED",
Self::InvalidTimeRange => "INVALID_TIME_RANGE",
Self::NoCustomDomain => "NO_CUSTOM_DOMAIN",
Self::InvalidVersionId => "INVALID_VERSION_ID",
Self::DatabaseTypeInvalid => "DATABASE_TYPE_INVALID",
Self::DatabaseVersionInvalid => "DATABASE_VERSION_INVALID",
Self::RestoreInProgress => "RESTORE_IN_PROGRESS",
Self::DailySnapshotsLimitReached => {
"DAILY_SNAPSHOTS_LIMIT_REACHED"
}
Self::InvalidScope => "INVALID_SCOPE",
Self::UpgradeRequired => "UPGRADE_REQUIRED",
Self::Unknown(s) => s.as_str(),
}
}
}
impl Serialize for ApiErrorCode {
fn serialize<S: serde::Serializer>(
&self,
s: S,
) -> Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ApiErrorCode {
fn deserialize<D: serde::Deserializer<'de>>(
d: D,
) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Ok(match s.as_str() {
"FEW_MEMORY" => Self::FewMemory,
"BAD_MEMORY" => Self::BadMemory,
"MISSING_CONFIG" => Self::MissingConfig,
"INVALID_DEPENDENCY" => Self::InvalidDependency,
"MISSING_MAIN" => Self::MissingMain,
"INVALID_MAIN" => Self::InvalidMain,
"INVALID_DISPLAY_NAME" => Self::InvalidDisplayName,
"MISSING_DISPLAY_NAME" => Self::MissingDisplayName,
"INVALID_MEMORY" => Self::InvalidMemory,
"MISSING_MEMORY" => Self::MissingMemory,
"INVALID_VERSION" => Self::InvalidVersion,
"MISSING_VERSION" => Self::MissingVersion,
"INVALID_ACCESS_TOKEN" => Self::InvalidAccessToken,
"REGEX_VALIDATION" => Self::RegexValidation,
"INVALID_START" => Self::InvalidStart,
"INVALID_SUBDOMAIN" => Self::InvalidSubdomain,
"RATE_LIMIT" => Self::RateLimit,
"NOT_FOUND" => Self::NotFound,
"APP_NOT_FOUND" => Self::AppNotFound,
"INVALID_FILE" => Self::InvalidFile,
"KEEP_CALM" => Self::KeepCalm,
"CONTAINER_ALREADY_STARTED" => Self::ContainerAlreadyStarted,
"INVALID_TIME_RANGE" => Self::InvalidTimeRange,
"NO_CUSTOM_DOMAIN" => Self::NoCustomDomain,
"INVALID_VERSION_ID" => Self::InvalidVersionId,
"DATABASE_TYPE_INVALID" => Self::DatabaseTypeInvalid,
"DATABASE_VERSION_INVALID" => Self::DatabaseVersionInvalid,
"RESTORE_IN_PROGRESS" => Self::RestoreInProgress,
"DAILY_SNAPSHOTS_LIMIT_REACHED" => {
Self::DailySnapshotsLimitReached
}
"INVALID_SCOPE" => Self::InvalidScope,
"UPGRADE_REQUIRED" => Self::UpgradeRequired,
_ => Self::Unknown(s),
})
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use super::{ApiError, ApiErrorCode, CommitError};
const ALL_CODES: &[(&str, ApiErrorCode)] = &[
("FEW_MEMORY", ApiErrorCode::FewMemory),
("BAD_MEMORY", ApiErrorCode::BadMemory),
("MISSING_CONFIG", ApiErrorCode::MissingConfig),
("INVALID_DEPENDENCY", ApiErrorCode::InvalidDependency),
("MISSING_MAIN", ApiErrorCode::MissingMain),
("INVALID_MAIN", ApiErrorCode::InvalidMain),
("INVALID_DISPLAY_NAME", ApiErrorCode::InvalidDisplayName),
("MISSING_DISPLAY_NAME", ApiErrorCode::MissingDisplayName),
("INVALID_MEMORY", ApiErrorCode::InvalidMemory),
("MISSING_MEMORY", ApiErrorCode::MissingMemory),
("INVALID_VERSION", ApiErrorCode::InvalidVersion),
("MISSING_VERSION", ApiErrorCode::MissingVersion),
("INVALID_ACCESS_TOKEN", ApiErrorCode::InvalidAccessToken),
("REGEX_VALIDATION", ApiErrorCode::RegexValidation),
("INVALID_START", ApiErrorCode::InvalidStart),
("INVALID_SUBDOMAIN", ApiErrorCode::InvalidSubdomain),
("RATE_LIMIT", ApiErrorCode::RateLimit),
("NOT_FOUND", ApiErrorCode::NotFound),
("APP_NOT_FOUND", ApiErrorCode::AppNotFound),
("INVALID_FILE", ApiErrorCode::InvalidFile),
("KEEP_CALM", ApiErrorCode::KeepCalm),
(
"CONTAINER_ALREADY_STARTED",
ApiErrorCode::ContainerAlreadyStarted,
),
("INVALID_TIME_RANGE", ApiErrorCode::InvalidTimeRange),
("NO_CUSTOM_DOMAIN", ApiErrorCode::NoCustomDomain),
("INVALID_VERSION_ID", ApiErrorCode::InvalidVersionId),
("DATABASE_TYPE_INVALID", ApiErrorCode::DatabaseTypeInvalid),
(
"DATABASE_VERSION_INVALID",
ApiErrorCode::DatabaseVersionInvalid,
),
("RESTORE_IN_PROGRESS", ApiErrorCode::RestoreInProgress),
(
"DAILY_SNAPSHOTS_LIMIT_REACHED",
ApiErrorCode::DailySnapshotsLimitReached,
),
("INVALID_SCOPE", ApiErrorCode::InvalidScope),
("UPGRADE_REQUIRED", ApiErrorCode::UpgradeRequired),
];
#[test]
fn all_known_codes_roundtrip() {
for (wire, variant) in ALL_CODES {
let json = format!(r#""{wire}""#);
let got: ApiErrorCode = serde_json::from_str(&json).unwrap();
assert_eq!(&got, variant, "deserialize failed for {wire}");
let serialized = serde_json::to_string(variant).unwrap();
assert_eq!(serialized, json, "serialize failed for {wire}");
}
}
#[test]
fn unknown_code_captures_raw_string() {
let got: ApiErrorCode =
serde_json::from_str(r#""APPLICATION_STOPPING""#).unwrap();
assert_eq!(got, ApiErrorCode::Unknown("APPLICATION_STOPPING".into()));
assert_eq!(
serde_json::to_string(&got).unwrap(),
r#""APPLICATION_STOPPING""#
);
}
#[tokio::test]
async fn api_error_display_transport() {
let reqwest_err = reqwest::get("http://0.0.0.0:1").await.unwrap_err();
let err = ApiError::Transport(reqwest_err);
assert!(err.to_string().starts_with("transport error:"));
}
#[test]
fn api_error_display_api() {
let err = ApiError::Api {
code: ApiErrorCode::RateLimit,
};
assert!(err.to_string().starts_with("api error:"));
}
#[tokio::test]
async fn api_error_source_transport_is_some() {
let reqwest_err = reqwest::get("http://0.0.0.0:1").await.unwrap_err();
let err = ApiError::Transport(reqwest_err);
assert!(err.source().is_some());
}
#[test]
fn api_error_source_api_is_none() {
let err = ApiError::Api {
code: ApiErrorCode::NotFound,
};
assert!(err.source().is_none());
}
#[test]
fn commit_error_from_io_error() {
let io = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
let err = CommitError::from(io);
assert!(matches!(err, CommitError::Io(_)));
}
}
#[derive(Debug)]
pub enum ApiError {
Transport(reqwest::Error),
Api {
code: ApiErrorCode,
},
}
#[derive(Debug)]
pub enum CommitError {
Api(ApiError),
Io(std::io::Error),
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ApiError::Transport(e) => write!(f, "transport error: {e}"),
ApiError::Api { code } => write!(f, "api error: {code:?}"),
}
}
}
impl std::error::Error for ApiError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ApiError::Transport(e) => Some(e),
ApiError::Api { .. } => None,
}
}
}
impl From<reqwest::Error> for ApiError {
fn from(err: reqwest::Error) -> Self {
ApiError::Transport(err)
}
}
impl From<std::io::Error> for CommitError {
fn from(err: std::io::Error) -> Self {
CommitError::Io(err)
}
}
impl From<reqwest::Error> for CommitError {
fn from(err: reqwest::Error) -> Self {
CommitError::Api(ApiError::from(err))
}
}