onesignal_rust_api/
helpers.rs1use std::time::Duration;
4
5use crate::apis::configuration;
6use crate::apis::default_api::CreateNotificationError;
7use crate::apis::{Error, ResponseContent};
8use crate::models;
9
10const RETRYABLE_STATUSES: [u16; 2] = [429, 503];
11const MIN_BASE_DELAY: Duration = Duration::from_secs(1);
12const MAX_BASE_DELAY: Duration = Duration::from_secs(60);
13
14#[derive(Debug, Clone)]
16pub struct CreateNotificationWithRetryOptions {
17 pub max_retries: u32,
19 pub base_delay: Duration,
22}
23
24impl Default for CreateNotificationWithRetryOptions {
25 fn default() -> Self {
26 Self {
27 max_retries: 3,
28 base_delay: Duration::from_secs(1),
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
37pub struct CreateNotificationWithRetryResult {
38 pub response: models::CreateNotificationSuccessResponse,
39 pub was_replayed: bool,
40}
41
42enum AttemptOutcome {
43 Fatal(Error<CreateNotificationError>),
44 Retryable(Error<CreateNotificationError>, Option<Duration>),
45}
46
47pub async fn create_notification_with_retry(
61 configuration: &configuration::Configuration,
62 notification: &mut models::Notification,
63 options: Option<CreateNotificationWithRetryOptions>,
64) -> Result<CreateNotificationWithRetryResult, Error<CreateNotificationError>> {
65 let opts = options.unwrap_or_default();
66 let base_delay = opts.base_delay.clamp(MIN_BASE_DELAY, MAX_BASE_DELAY);
69
70 if notification
71 .idempotency_key
72 .as_deref()
73 .map_or(true, |key| key.is_empty())
74 {
75 notification.idempotency_key = Some(uuid::Uuid::new_v4().to_string());
76 }
77
78 let mut attempt: u32 = 0;
79 loop {
80 match send_once(configuration, notification).await {
81 Ok(result) => return Ok(result),
82 Err(AttemptOutcome::Fatal(error)) => return Err(error),
83 Err(AttemptOutcome::Retryable(error, retry_after)) => {
84 if attempt >= opts.max_retries {
85 return Err(error);
86 }
87 let delay =
88 retry_after.unwrap_or_else(|| base_delay * 2u32.saturating_pow(attempt));
89 if !delay.is_zero() {
90 tokio::time::sleep(delay).await;
91 }
92 attempt += 1;
93 }
94 }
95 }
96}
97
98async fn send_once(
99 configuration: &configuration::Configuration,
100 notification: &models::Notification,
101) -> Result<CreateNotificationWithRetryResult, AttemptOutcome> {
102 let client = &configuration.client;
103
104 let uri_str = format!("{}/notifications", configuration.base_path);
105 let mut req_builder = client.request(reqwest::Method::POST, uri_str.as_str());
106
107 if let Some(ref user_agent) = configuration.user_agent {
108 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
109 }
110
111 req_builder = req_builder.header(
113 "OS-Usage-Data",
114 concat!(
115 "kind=sdk, sdk-name=onesignal-rust, version=",
116 env!("CARGO_PKG_VERSION")
117 ),
118 );
119
120 if let Some(ref token) = configuration.rest_api_key_token {
121 req_builder = req_builder.header("Authorization", format!("Key {}", token.to_owned()));
122 }
123 req_builder = req_builder.json(notification);
124
125 let req = req_builder
126 .build()
127 .map_err(|e| AttemptOutcome::Fatal(Error::Reqwest(e)))?;
128 let resp = match client.execute(req).await {
129 Ok(resp) => resp,
130 Err(e) => {
131 return Err(if e.is_timeout() || e.is_connect() {
132 AttemptOutcome::Retryable(Error::Reqwest(e), None)
133 } else {
134 AttemptOutcome::Fatal(Error::Reqwest(e))
135 });
136 }
137 };
138
139 let status = resp.status();
140 let was_replayed = header_value(&resp, "idempotent-replayed")
141 .map_or(false, |value| value.trim().eq_ignore_ascii_case("true"));
142 let retry_after = header_value(&resp, "retry-after")
143 .and_then(|value| value.trim().parse::<u64>().ok())
144 .map(Duration::from_secs);
145
146 let content = resp
147 .text()
148 .await
149 .map_err(|e| AttemptOutcome::Fatal(Error::Reqwest(e)))?;
150
151 if !status.is_client_error() && !status.is_server_error() {
152 let response = serde_json::from_str(&content)
153 .map_err(|e| AttemptOutcome::Fatal(Error::Serde(e)))?;
154 Ok(CreateNotificationWithRetryResult {
155 response,
156 was_replayed,
157 })
158 } else {
159 let entity: Option<CreateNotificationError> = serde_json::from_str(&content).ok();
160 let error = Error::ResponseError(ResponseContent {
161 status,
162 content,
163 entity,
164 });
165 Err(if RETRYABLE_STATUSES.contains(&status.as_u16()) {
166 AttemptOutcome::Retryable(error, retry_after)
167 } else {
168 AttemptOutcome::Fatal(error)
169 })
170 }
171}
172
173pub type MessageSent = models::CreateNotificationSuccessResponse;
178
179pub type MessageNotSent = models::CreateNotificationSuccessResponse;
184
185impl models::CreateNotificationSuccessResponse {
186 pub fn is_message_sent(&self) -> bool {
190 self.id.as_deref().map_or(false, |id| !id.is_empty())
191 }
192
193 pub fn is_message_not_sent(&self) -> bool {
196 !self.is_message_sent()
197 }
198
199 pub fn as_sent(&self) -> Option<&MessageSent> {
202 if self.is_message_sent() {
203 Some(self)
204 } else {
205 None
206 }
207 }
208
209 pub fn as_not_sent(&self) -> Option<&MessageNotSent> {
212 if self.is_message_not_sent() {
213 Some(self)
214 } else {
215 None
216 }
217 }
218}
219
220fn header_value(resp: &reqwest::Response, name: &str) -> Option<String> {
221 resp.headers()
222 .get(name)
223 .and_then(|value| value.to_str().ok())
224 .map(|value| value.to_owned())
225}