Skip to content
Merged

237 #309

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions src/turnkey/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,33 @@
//
// SPDX-License-Identifier: MIT

//! Turnkey-specific domain errors and error kind mappings.
//!
//! This module provides stable, high-level error categories for Turnkey
//! operations and their mapping to canonical [`AppErrorKind`] values.
//!
//! # Error Categories
//!
//! - [`TurnkeyErrorKind::UniqueLabel`] - Unique constraint violations
//! - [`TurnkeyErrorKind::RateLimited`] - Throttling or quota exceeded
//! - [`TurnkeyErrorKind::Timeout`] - Operation timeouts
//! - [`TurnkeyErrorKind::Auth`] - Authentication/authorization failures
//! - [`TurnkeyErrorKind::Network`] - Network-level errors
//! - [`TurnkeyErrorKind::Service`] - Generic Turnkey service errors
//!
//! # Mapping to AppErrorKind
//!
//! The mapping is intentionally conservative to maintain stability:
//!
//! | TurnkeyErrorKind | AppErrorKind |
//! |------------------|--------------|
//! | UniqueLabel | Conflict |
//! | RateLimited | RateLimited |
//! | Timeout | Timeout |
//! | Auth | Unauthorized |
//! | Network | Network |
//! | Service | Turnkey |

use crate::{AppErrorKind, Error};

/// High-level, stable Turnkey error categories.
Expand All @@ -16,30 +43,100 @@ use crate::{AppErrorKind, Error};
/// - `Auth` → `Unauthorized`
/// - `Network` → `Network`
/// - `Service` → `Turnkey`
///
/// # Examples
///
/// ```rust
/// use masterror::turnkey::TurnkeyErrorKind;
///
/// let kind = TurnkeyErrorKind::Timeout;
/// assert_eq!(kind.to_string(), "request timed out");
/// ```
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TurnkeyErrorKind {
/// Unique label violation or duplicate resource.
///
/// ```rust
/// use masterror::turnkey::TurnkeyErrorKind;
///
/// let kind = TurnkeyErrorKind::UniqueLabel;
/// assert_eq!(kind.to_string(), "label already exists");
/// ```
#[error("label already exists")]
UniqueLabel,

/// Throttling or quota exceeded.
///
/// ```rust
/// use masterror::turnkey::TurnkeyErrorKind;
///
/// let kind = TurnkeyErrorKind::RateLimited;
/// assert_eq!(kind.to_string(), "rate limited or throttled");
/// ```
#[error("rate limited or throttled")]
RateLimited,

/// Operation exceeded allowed time.
///
/// ```rust
/// use masterror::turnkey::TurnkeyErrorKind;
///
/// let kind = TurnkeyErrorKind::Timeout;
/// assert_eq!(kind.to_string(), "request timed out");
/// ```
#[error("request timed out")]
Timeout,

/// Authentication/authorization failure.
///
/// ```rust
/// use masterror::turnkey::TurnkeyErrorKind;
///
/// let kind = TurnkeyErrorKind::Auth;
/// assert_eq!(kind.to_string(), "authentication/authorization failed");
/// ```
#[error("authentication/authorization failed")]
Auth,

/// Network-level error (DNS/connect/TLS/build).
///
/// ```rust
/// use masterror::turnkey::TurnkeyErrorKind;
///
/// let kind = TurnkeyErrorKind::Network;
/// assert_eq!(kind.to_string(), "network error");
/// ```
#[error("network error")]
Network,

/// Generic service error in the Turnkey subsystem.
///
/// ```rust
/// use masterror::turnkey::TurnkeyErrorKind;
///
/// let kind = TurnkeyErrorKind::Service;
/// assert_eq!(kind.to_string(), "service error");
/// ```
#[error("service error")]
Service
}

/// Turnkey domain error with stable kind and safe, human-readable message.
///
/// Combines a [`TurnkeyErrorKind`] with a human-readable message.
/// Display format: `"{kind}: {msg}"`.
///
/// # Examples
///
/// ```rust
/// use masterror::turnkey::{TurnkeyError, TurnkeyErrorKind};
///
/// let err = TurnkeyError::new(TurnkeyErrorKind::RateLimited, "quota exceeded");
/// assert_eq!(err.kind, TurnkeyErrorKind::RateLimited);
/// assert_eq!(err.msg, "quota exceeded");
/// assert_eq!(err.to_string(), "rate limited or throttled: quota exceeded");
/// ```
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[error("{kind}: {msg}")]
pub struct TurnkeyError {
Expand Down Expand Up @@ -70,6 +167,28 @@ impl TurnkeyError {
/// Map [`TurnkeyErrorKind`] into the canonical [`AppErrorKind`].
///
/// Keep mappings conservative and stable. See enum docs for rationale.
///
/// # Examples
///
/// ```rust
/// use masterror::{
/// AppErrorKind,
/// turnkey::{TurnkeyErrorKind, map_turnkey_kind}
/// };
///
/// assert_eq!(
/// map_turnkey_kind(TurnkeyErrorKind::Timeout),
/// AppErrorKind::Timeout
/// );
/// assert_eq!(
/// map_turnkey_kind(TurnkeyErrorKind::Auth),
/// AppErrorKind::Unauthorized
/// );
/// assert_eq!(
/// map_turnkey_kind(TurnkeyErrorKind::UniqueLabel),
/// AppErrorKind::Conflict
/// );
/// ```
#[must_use]
#[inline]
pub fn map_turnkey_kind(kind: TurnkeyErrorKind) -> AppErrorKind {
Expand Down
135 changes: 134 additions & 1 deletion src/turnkey/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,138 @@ fn from_turnkey_error_into_app_error() {
let e = TurnkeyError::new(TurnkeyErrorKind::RateLimited, "try later");
let a: AppError = e.into();
assert_eq!(a.kind, AppErrorKind::RateLimited);
// message plumbing is AppError-specific; sanity-check only kind here.
}

#[test]
fn turnkey_error_kind_display_unique_label() {
let kind = TurnkeyErrorKind::UniqueLabel;
assert_eq!(kind.to_string(), "label already exists");
}

#[test]
fn turnkey_error_kind_display_rate_limited() {
let kind = TurnkeyErrorKind::RateLimited;
assert_eq!(kind.to_string(), "rate limited or throttled");
}

#[test]
fn turnkey_error_kind_display_timeout() {
let kind = TurnkeyErrorKind::Timeout;
assert_eq!(kind.to_string(), "request timed out");
}

#[test]
fn turnkey_error_kind_display_auth() {
let kind = TurnkeyErrorKind::Auth;
assert_eq!(kind.to_string(), "authentication/authorization failed");
}

#[test]
fn turnkey_error_kind_display_network() {
let kind = TurnkeyErrorKind::Network;
assert_eq!(kind.to_string(), "network error");
}

#[test]
fn turnkey_error_kind_display_service() {
let kind = TurnkeyErrorKind::Service;
assert_eq!(kind.to_string(), "service error");
}

#[test]
fn turnkey_error_new_creates_error_with_kind_and_message() {
let err = TurnkeyError::new(TurnkeyErrorKind::Timeout, "operation timeout");
assert_eq!(err.kind, TurnkeyErrorKind::Timeout);
assert_eq!(err.msg, "operation timeout");
}

#[test]
fn turnkey_error_new_accepts_string() {
let err = TurnkeyError::new(TurnkeyErrorKind::Network, "test".to_string());
assert_eq!(err.msg, "test");
}

#[test]
fn turnkey_error_new_accepts_str() {
let err = TurnkeyError::new(TurnkeyErrorKind::Auth, "auth failed");
assert_eq!(err.msg, "auth failed");
}

#[test]
fn turnkey_error_new_accepts_empty_string() {
let err = TurnkeyError::new(TurnkeyErrorKind::Service, "");
assert_eq!(err.msg, "");
}

#[test]
fn turnkey_error_new_accepts_unicode() {
let err = TurnkeyError::new(TurnkeyErrorKind::UniqueLabel, "ラベルが存在します");
assert_eq!(err.msg, "ラベルが存在します");
}

#[test]
fn turnkey_error_display_formats_kind_and_message() {
let err = TurnkeyError::new(TurnkeyErrorKind::RateLimited, "quota exceeded");
let display = err.to_string();
assert!(display.contains("rate limited or throttled"));
assert!(display.contains("quota exceeded"));
assert_eq!(display, "rate limited or throttled: quota exceeded");
}

#[test]
fn turnkey_error_display_with_empty_message() {
let err = TurnkeyError::new(TurnkeyErrorKind::Timeout, "");
let display = err.to_string();
assert_eq!(display, "request timed out: ");
}

#[test]
fn turnkey_error_clone_creates_identical_copy() {
let err1 = TurnkeyError::new(TurnkeyErrorKind::Network, "connection lost");
let err2 = err1.clone();
assert_eq!(err1.kind, err2.kind);
assert_eq!(err1.msg, err2.msg);
assert_eq!(err1, err2);
}

#[test]
fn turnkey_error_partial_eq_compares_kind_and_message() {
let err1 = TurnkeyError::new(TurnkeyErrorKind::Auth, "invalid token");
let err2 = TurnkeyError::new(TurnkeyErrorKind::Auth, "invalid token");
let err3 = TurnkeyError::new(TurnkeyErrorKind::Auth, "different message");
let err4 = TurnkeyError::new(TurnkeyErrorKind::Service, "invalid token");

assert_eq!(err1, err2);
assert_ne!(err1, err3);
assert_ne!(err1, err4);
}

#[test]
fn turnkey_error_kind_clone_creates_identical_copy() {
let kind1 = TurnkeyErrorKind::Timeout;
let kind2 = kind1;
assert_eq!(kind1, kind2);
}

#[test]
fn turnkey_error_kind_partial_eq_works() {
assert_eq!(TurnkeyErrorKind::UniqueLabel, TurnkeyErrorKind::UniqueLabel);
assert_eq!(TurnkeyErrorKind::RateLimited, TurnkeyErrorKind::RateLimited);
assert_ne!(TurnkeyErrorKind::Timeout, TurnkeyErrorKind::Network);
}

#[test]
fn map_turnkey_kind_is_inline() {
let kind = TurnkeyErrorKind::Timeout;
let mapped = map_turnkey_kind(kind);
assert_eq!(mapped, AppErrorKind::Timeout);
}

#[test]
fn turnkey_error_debug_format() {
let err = TurnkeyError::new(TurnkeyErrorKind::UniqueLabel, "duplicate key");
let debug = format!("{:?}", err);
assert!(debug.contains("TurnkeyError"));
assert!(debug.contains("UniqueLabel"));
assert!(debug.contains("duplicate key"));
}
Loading