1use serde::{Deserialize, Serialize};
4
5use crate::{FailureClass, RetryClass, SCHEMA_VERSION, ValidationError, require_non_empty};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum RenewalAutomationState {
10 NotAttempted,
11 NoRenewal,
12 PrepareStarted,
13 ReceiptWritten,
14 LeaseCreated,
15 PendingContinuation,
16 Fulfilled,
17 Failed,
18 Expired,
19}
20
21impl RenewalAutomationState {
22 pub const ALL: &'static [Self] = &[
23 Self::NotAttempted,
24 Self::NoRenewal,
25 Self::PrepareStarted,
26 Self::ReceiptWritten,
27 Self::LeaseCreated,
28 Self::PendingContinuation,
29 Self::Fulfilled,
30 Self::Failed,
31 Self::Expired,
32 ];
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct RenewalAutomationStatus {
38 pub schema_version: String,
39 pub state: RenewalAutomationState,
40 pub client_id: String,
41 pub adapter_id: String,
42 pub updated_at_epoch_s: u64,
43 pub pending_token_present: bool,
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub reset_path: Option<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub thread_id: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub renewal_lease_id: Option<String>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub prepared_at_epoch_s: Option<u64>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub fulfilled_at_epoch_s: Option<u64>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub pending_path: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub reset_prepare_receipt_path: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub failure_class: Option<FailureClass>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub retry_class: Option<RetryClass>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub message: Option<String>,
64}
65
66impl RenewalAutomationStatus {
67 pub fn validate(&self) -> Result<(), ValidationError> {
68 if self.schema_version != SCHEMA_VERSION {
69 return Err(ValidationError::SchemaVersionMismatch {
70 expected: SCHEMA_VERSION.to_string(),
71 found: self.schema_version.clone(),
72 });
73 }
74 require_non_empty(&self.client_id, "renewal_status.client_id")?;
75 require_non_empty(&self.adapter_id, "renewal_status.adapter_id")?;
76 for (field, value) in [
77 ("renewal_status.reset_path", &self.reset_path),
78 ("renewal_status.thread_id", &self.thread_id),
79 ("renewal_status.renewal_lease_id", &self.renewal_lease_id),
80 ("renewal_status.pending_path", &self.pending_path),
81 (
82 "renewal_status.reset_prepare_receipt_path",
83 &self.reset_prepare_receipt_path,
84 ),
85 ("renewal_status.message", &self.message),
86 ] {
87 if let Some(value) = value {
88 require_non_empty(value, field)?;
89 }
90 }
91
92 let failure_like = matches!(
93 self.state,
94 RenewalAutomationState::Failed | RenewalAutomationState::Expired
95 );
96 match (failure_like, self.failure_class, self.retry_class) {
97 (true, Some(_), Some(_)) => {}
98 (true, _, _) => {
99 return Err(ValidationError::InvalidRequest(
100 "renewal_status state=failed|expired requires failure_class and retry_class"
101 .into(),
102 ));
103 }
104 (false, None, None) => {}
105 (false, _, _) => {
106 return Err(ValidationError::InvalidRequest(
107 "failure_class and retry_class are only valid for failed or expired renewal status"
108 .into(),
109 ));
110 }
111 }
112
113 if matches!(self.state, RenewalAutomationState::PendingContinuation)
114 && !self.pending_token_present
115 {
116 return Err(ValidationError::InvalidRequest(
117 "pending_continuation requires pending_token_present=true".into(),
118 ));
119 }
120
121 Ok(())
122 }
123}