use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ClientErrorCode {
LicenseNotFound,
LicenseExpired,
LicenseRevoked,
LicenseSuspended,
LicenseBlacklisted,
LicenseInactive,
AlreadyBound,
NotBound,
HardwareMismatch,
FeatureNotIncluded,
QuotaExceeded,
GracePeriodExpired,
InternalError,
#[serde(other)]
Unknown,
}
impl ClientErrorCode {
pub fn default_message(&self) -> &'static str {
match self {
ClientErrorCode::LicenseNotFound => "License not found",
ClientErrorCode::LicenseExpired => "License has expired",
ClientErrorCode::LicenseRevoked => "License has been revoked",
ClientErrorCode::LicenseSuspended => "License is suspended",
ClientErrorCode::LicenseBlacklisted => "License has been blacklisted",
ClientErrorCode::LicenseInactive => "License is not active",
ClientErrorCode::AlreadyBound => "License is already bound to another device",
ClientErrorCode::NotBound => "License is not bound to any device",
ClientErrorCode::HardwareMismatch => "Hardware ID does not match",
ClientErrorCode::FeatureNotIncluded => "Feature not included in license",
ClientErrorCode::QuotaExceeded => "Usage quota exceeded",
ClientErrorCode::GracePeriodExpired => {
"Grace period expired - please connect to license server"
}
ClientErrorCode::InternalError => "Internal server error",
ClientErrorCode::Unknown => "Unknown error",
}
}
pub fn is_license_invalid(&self) -> bool {
matches!(
self,
ClientErrorCode::LicenseNotFound
| ClientErrorCode::LicenseExpired
| ClientErrorCode::LicenseRevoked
| ClientErrorCode::LicenseBlacklisted
| ClientErrorCode::GracePeriodExpired
)
}
pub fn requires_online(&self) -> bool {
matches!(
self,
ClientErrorCode::GracePeriodExpired | ClientErrorCode::LicenseSuspended
)
}
}
impl fmt::Display for ClientErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.default_message())
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ServerErrorBody {
pub code: ClientErrorCode,
pub message: String,
#[serde(default)]
pub details: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ServerErrorResponse {
pub error: ServerErrorBody,
}
#[derive(Debug, Clone)]
pub struct ClientApiError {
pub code: ClientErrorCode,
pub message: String,
pub details: Option<serde_json::Value>,
}
impl ClientApiError {
pub fn new(code: ClientErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
details: None,
}
}
pub fn with_details(
code: ClientErrorCode,
message: impl Into<String>,
details: serde_json::Value,
) -> Self {
Self {
code,
message: message.into(),
details: Some(details),
}
}
pub fn grace_period_expired() -> Self {
Self::new(
ClientErrorCode::GracePeriodExpired,
"Offline grace period has expired. Please connect to the license server.",
)
}
pub fn is_license_invalid(&self) -> bool {
self.code.is_license_invalid()
}
pub fn requires_online(&self) -> bool {
self.code.requires_online()
}
}
impl fmt::Display for ClientApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for ClientApiError {}
impl From<ServerErrorResponse> for ClientApiError {
fn from(resp: ServerErrorResponse) -> Self {
Self {
code: resp.error.code,
message: resp.error.message,
details: resp.error.details,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_server_error_response() {
let json = r#"{
"error": {
"code": "LICENSE_NOT_FOUND",
"message": "The requested license does not exist",
"details": null
}
}"#;
let resp: ServerErrorResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.error.code, ClientErrorCode::LicenseNotFound);
assert_eq!(resp.error.message, "The requested license does not exist");
assert!(resp.error.details.is_none());
}
#[test]
fn parse_already_bound_error() {
let json = r#"{
"error": {
"code": "ALREADY_BOUND",
"message": "License is already bound to device 'Work Laptop'",
"details": {"device_name": "Work Laptop"}
}
}"#;
let resp: ServerErrorResponse = serde_json::from_str(json).unwrap();
let err: ClientApiError = resp.into();
assert_eq!(err.code, ClientErrorCode::AlreadyBound);
assert!(err.message.contains("Work Laptop"));
assert!(err.details.is_some());
}
#[test]
fn parse_unknown_error_code() {
let json = r#"{
"error": {
"code": "SOME_FUTURE_ERROR",
"message": "Some new error type",
"details": null
}
}"#;
let resp: ServerErrorResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.error.code, ClientErrorCode::Unknown);
}
#[test]
fn error_code_is_license_invalid() {
assert!(ClientErrorCode::LicenseNotFound.is_license_invalid());
assert!(ClientErrorCode::LicenseExpired.is_license_invalid());
assert!(ClientErrorCode::LicenseRevoked.is_license_invalid());
assert!(ClientErrorCode::GracePeriodExpired.is_license_invalid());
assert!(!ClientErrorCode::AlreadyBound.is_license_invalid());
assert!(!ClientErrorCode::NotBound.is_license_invalid());
assert!(!ClientErrorCode::LicenseSuspended.is_license_invalid());
}
#[test]
fn error_code_requires_online() {
assert!(ClientErrorCode::GracePeriodExpired.requires_online());
assert!(ClientErrorCode::LicenseSuspended.requires_online());
assert!(!ClientErrorCode::LicenseExpired.requires_online());
assert!(!ClientErrorCode::AlreadyBound.requires_online());
}
#[test]
fn client_api_error_display() {
let err = ClientApiError::new(
ClientErrorCode::LicenseExpired,
"Your license expired on 2024-01-01",
);
let display = format!("{}", err);
assert!(display.contains("expired"));
}
}