use serde::{Deserialize, Serialize};
use crate::{FailureClass, RetryClass, SCHEMA_VERSION, ValidationError, require_non_empty};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RenewalAutomationState {
NotAttempted,
NoRenewal,
PrepareStarted,
ReceiptWritten,
LeaseCreated,
PendingContinuation,
Fulfilled,
Failed,
Expired,
}
impl RenewalAutomationState {
pub const ALL: &'static [Self] = &[
Self::NotAttempted,
Self::NoRenewal,
Self::PrepareStarted,
Self::ReceiptWritten,
Self::LeaseCreated,
Self::PendingContinuation,
Self::Fulfilled,
Self::Failed,
Self::Expired,
];
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RenewalAutomationStatus {
pub schema_version: String,
pub state: RenewalAutomationState,
pub client_id: String,
pub adapter_id: String,
pub updated_at_epoch_s: u64,
pub pending_token_present: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub reset_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub renewal_lease_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prepared_at_epoch_s: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fulfilled_at_epoch_s: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pending_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reset_prepare_receipt_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failure_class: Option<FailureClass>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_class: Option<RetryClass>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl RenewalAutomationStatus {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.schema_version != SCHEMA_VERSION {
return Err(ValidationError::SchemaVersionMismatch {
expected: SCHEMA_VERSION.to_string(),
found: self.schema_version.clone(),
});
}
require_non_empty(&self.client_id, "renewal_status.client_id")?;
require_non_empty(&self.adapter_id, "renewal_status.adapter_id")?;
for (field, value) in [
("renewal_status.reset_path", &self.reset_path),
("renewal_status.thread_id", &self.thread_id),
("renewal_status.renewal_lease_id", &self.renewal_lease_id),
("renewal_status.pending_path", &self.pending_path),
(
"renewal_status.reset_prepare_receipt_path",
&self.reset_prepare_receipt_path,
),
("renewal_status.message", &self.message),
] {
if let Some(value) = value {
require_non_empty(value, field)?;
}
}
let failure_like = matches!(
self.state,
RenewalAutomationState::Failed | RenewalAutomationState::Expired
);
match (failure_like, self.failure_class, self.retry_class) {
(true, Some(_), Some(_)) => {}
(true, _, _) => {
return Err(ValidationError::InvalidRequest(
"renewal_status state=failed|expired requires failure_class and retry_class"
.into(),
));
}
(false, None, None) => {}
(false, _, _) => {
return Err(ValidationError::InvalidRequest(
"failure_class and retry_class are only valid for failed or expired renewal status"
.into(),
));
}
}
if matches!(self.state, RenewalAutomationState::PendingContinuation)
&& !self.pending_token_present
{
return Err(ValidationError::InvalidRequest(
"pending_continuation requires pending_token_present=true".into(),
));
}
Ok(())
}
}