use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum Decision {
Allow,
AllowWithCaps,
Deny,
#[serde(other)]
Unknown,
}
impl Decision {
pub fn is_allowed(self) -> bool {
matches!(self, Self::Allow | Self::AllowWithCaps)
}
pub fn is_denied(self) -> bool {
matches!(self, Self::Deny)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum Unit {
UsdMicrocents,
Tokens,
Credits,
RiskPoints,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum CommitOveragePolicy {
Reject,
AllowIfAvailable,
AllowWithOverdraft,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum ReservationStatus {
Active,
Committed,
Released,
Expired,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum CommitStatus {
Committed,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum ReleaseStatus {
Released,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum ExtendStatus {
Active,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum EventStatus {
Applied,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum ErrorCode {
InvalidRequest,
Unauthorized,
Forbidden,
NotFound,
BudgetExceeded,
BudgetFrozen,
BudgetClosed,
ReservationExpired,
ReservationFinalized,
IdempotencyMismatch,
UnitMismatch,
OverdraftLimitExceeded,
DebtOutstanding,
MaxExtensionsExceeded,
InternalError,
#[serde(other)]
Unknown,
}
impl ErrorCode {
pub fn is_retryable(self) -> bool {
matches!(self, Self::InternalError | Self::Unknown)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decision_helpers() {
assert!(Decision::Allow.is_allowed());
assert!(Decision::AllowWithCaps.is_allowed());
assert!(!Decision::Deny.is_allowed());
assert!(!Decision::Unknown.is_allowed());
assert!(Decision::Deny.is_denied());
assert!(!Decision::Allow.is_denied());
assert!(!Decision::Unknown.is_denied());
}
#[test]
fn serde_roundtrip_decision() {
let json = serde_json::to_string(&Decision::AllowWithCaps).unwrap();
assert_eq!(json, "\"ALLOW_WITH_CAPS\"");
let d: Decision = serde_json::from_str(&json).unwrap();
assert_eq!(d, Decision::AllowWithCaps);
}
#[test]
fn serde_unknown_decision_fallback() {
let d: Decision = serde_json::from_str("\"ALLOW_WITH_WARNINGS\"").unwrap();
assert_eq!(d, Decision::Unknown);
}
#[test]
fn serde_roundtrip_unit() {
let json = serde_json::to_string(&Unit::UsdMicrocents).unwrap();
assert_eq!(json, "\"USD_MICROCENTS\"");
let u: Unit = serde_json::from_str(&json).unwrap();
assert_eq!(u, Unit::UsdMicrocents);
}
#[test]
fn serde_unknown_unit_fallback() {
let u: Unit = serde_json::from_str("\"ENERGY_JOULES\"").unwrap();
assert_eq!(u, Unit::Unknown);
}
#[test]
fn serde_roundtrip_all_units() {
for (variant, expected) in [
(Unit::UsdMicrocents, "\"USD_MICROCENTS\""),
(Unit::Tokens, "\"TOKENS\""),
(Unit::Credits, "\"CREDITS\""),
(Unit::RiskPoints, "\"RISK_POINTS\""),
] {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, expected);
let round: Unit = serde_json::from_str(&json).unwrap();
assert_eq!(round, variant);
}
}
#[test]
fn serde_roundtrip_error_code() {
let json = serde_json::to_string(&ErrorCode::BudgetExceeded).unwrap();
assert_eq!(json, "\"BUDGET_EXCEEDED\"");
let ec: ErrorCode = serde_json::from_str(&json).unwrap();
assert_eq!(ec, ErrorCode::BudgetExceeded);
}
#[test]
fn serde_roundtrip_all_error_codes() {
let codes = [
(ErrorCode::InvalidRequest, "\"INVALID_REQUEST\""),
(ErrorCode::Unauthorized, "\"UNAUTHORIZED\""),
(ErrorCode::Forbidden, "\"FORBIDDEN\""),
(ErrorCode::NotFound, "\"NOT_FOUND\""),
(ErrorCode::BudgetExceeded, "\"BUDGET_EXCEEDED\""),
(ErrorCode::BudgetFrozen, "\"BUDGET_FROZEN\""),
(ErrorCode::BudgetClosed, "\"BUDGET_CLOSED\""),
(ErrorCode::ReservationExpired, "\"RESERVATION_EXPIRED\""),
(ErrorCode::ReservationFinalized, "\"RESERVATION_FINALIZED\""),
(ErrorCode::IdempotencyMismatch, "\"IDEMPOTENCY_MISMATCH\""),
(ErrorCode::UnitMismatch, "\"UNIT_MISMATCH\""),
(
ErrorCode::OverdraftLimitExceeded,
"\"OVERDRAFT_LIMIT_EXCEEDED\"",
),
(ErrorCode::DebtOutstanding, "\"DEBT_OUTSTANDING\""),
(
ErrorCode::MaxExtensionsExceeded,
"\"MAX_EXTENSIONS_EXCEEDED\"",
),
(ErrorCode::InternalError, "\"INTERNAL_ERROR\""),
];
for (variant, expected) in codes {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, expected, "failed for {:?}", variant);
let round: ErrorCode = serde_json::from_str(&json).unwrap();
assert_eq!(round, variant);
}
}
#[test]
fn serde_unknown_error_code_fallback() {
let ec: ErrorCode = serde_json::from_str("\"RATE_LIMITED\"").unwrap();
assert_eq!(ec, ErrorCode::Unknown);
}
#[test]
fn error_code_retryable() {
assert!(ErrorCode::InternalError.is_retryable());
assert!(ErrorCode::Unknown.is_retryable());
assert!(!ErrorCode::BudgetExceeded.is_retryable());
assert!(!ErrorCode::Forbidden.is_retryable());
assert!(!ErrorCode::ReservationExpired.is_retryable());
}
#[test]
fn serde_roundtrip_commit_overage_policy() {
let policies = [
(CommitOveragePolicy::Reject, "\"REJECT\""),
(
CommitOveragePolicy::AllowIfAvailable,
"\"ALLOW_IF_AVAILABLE\"",
),
(
CommitOveragePolicy::AllowWithOverdraft,
"\"ALLOW_WITH_OVERDRAFT\"",
),
];
for (variant, expected) in policies {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, expected);
let round: CommitOveragePolicy = serde_json::from_str(&json).unwrap();
assert_eq!(round, variant);
}
}
#[test]
fn serde_roundtrip_reservation_status() {
let statuses = [
(ReservationStatus::Active, "\"ACTIVE\""),
(ReservationStatus::Committed, "\"COMMITTED\""),
(ReservationStatus::Released, "\"RELEASED\""),
(ReservationStatus::Expired, "\"EXPIRED\""),
];
for (variant, expected) in statuses {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, expected);
let round: ReservationStatus = serde_json::from_str(&json).unwrap();
assert_eq!(round, variant);
}
}
#[test]
fn serde_unknown_reservation_status_fallback() {
let s: ReservationStatus = serde_json::from_str("\"PENDING\"").unwrap();
assert_eq!(s, ReservationStatus::Unknown);
}
#[test]
fn serde_roundtrip_single_value_statuses() {
let json = serde_json::to_string(&CommitStatus::Committed).unwrap();
assert_eq!(json, "\"COMMITTED\"");
let cs: CommitStatus = serde_json::from_str(&json).unwrap();
assert_eq!(cs, CommitStatus::Committed);
let json = serde_json::to_string(&ReleaseStatus::Released).unwrap();
assert_eq!(json, "\"RELEASED\"");
let rs: ReleaseStatus = serde_json::from_str(&json).unwrap();
assert_eq!(rs, ReleaseStatus::Released);
let json = serde_json::to_string(&ExtendStatus::Active).unwrap();
assert_eq!(json, "\"ACTIVE\"");
let es: ExtendStatus = serde_json::from_str(&json).unwrap();
assert_eq!(es, ExtendStatus::Active);
let json = serde_json::to_string(&EventStatus::Applied).unwrap();
assert_eq!(json, "\"APPLIED\"");
let evs: EventStatus = serde_json::from_str(&json).unwrap();
assert_eq!(evs, EventStatus::Applied);
}
}