Skip to content
Closed
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
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "onesignal-rust-api"
version = "5.6.0"
version = "5.7.0"
authors = ["devrel@onesignal.com"]
edition = "2018"
description = "A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com"
Expand All @@ -11,6 +11,9 @@ serde = "^1.0"
serde_derive = "^1.0"
serde_json = "^1.0"
url = "^2.2"
uuid = { version = "^1.0", features = ["v4"] }
tokio = { version = "^1.0", features = ["time"] }

[dependencies.reqwest]
version = "^0.11"
features = ["json", "multipart"]
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ A powerful way to send personalized messages at scale and build effective custom

For more information, please visit [https://onesignal.com](https://onesignal.com)

- API version: 5.6.0
- Package version: 5.6.0
- API version: 5.7.0
- Package version: 5.7.0

## Installation

Add to `Cargo.toml` under `[dependencies]`:

```toml
onesignal-rust-api = "5.6.0"
onesignal-rust-api = "5.7.0"
```

## Configuration
Expand Down
354 changes: 224 additions & 130 deletions docs/DefaultApi.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/apis/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com
*
* The version of the OpenAPI document: 5.6.0
* The version of the OpenAPI document: 5.7.0
* Contact: devrel@onesignal.com
* Generated by: https://openapi-generator.tech
*/
Expand Down Expand Up @@ -45,7 +45,7 @@ impl Default for Configuration {
fn default() -> Self {
Configuration {
base_path: "https://api.onesignal.com".to_owned(),
user_agent: Some("OpenAPI-Generator/5.6.0/rust".to_owned()),
user_agent: Some("OpenAPI-Generator/5.7.0/rust".to_owned()),
client: reqwest::Client::new(),
basic_auth: None,
oauth_access_token: None,
Expand Down
90 changes: 45 additions & 45 deletions src/apis/default_api.rs

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions src/apis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,63 @@ pub enum Error<T> {
ResponseError(ResponseContent<T>),
}

impl <T> Error<T> {
/// The error messages carried by the response body, normalized to a flat
/// `Vec<String>` regardless of which envelope shape the API returned
/// (`{"errors": "..."}`, `{"errors": ["..."]}`,
/// `{"errors": [{"code": ..., "title": ...}]}`, or an object map such as
/// `{"errors": {"invalid_aliases": {...}}}`, surfaced as `"<key>: <value>"`
/// entries). Returns an empty `Vec` for non-response errors (network, serde,
/// IO) or when the body is not a recognizable error envelope. The raw
/// response remains available on the `ResponseError` variant.
pub fn error_messages(&self) -> Vec<String> {
let content = match self {
Error::ResponseError(response) => &response.content,
_ => return Vec::new(),
};

let parsed: serde_json::Value = match serde_json::from_str(content) {
Ok(value) => value,
Err(_) => return Vec::new(),
};

match parsed.get("errors") {
Some(serde_json::Value::String(message)) => vec![message.clone()],
Some(serde_json::Value::Array(items)) => items
.iter()
.filter_map(|item| match item {
serde_json::Value::String(message) => Some(message.clone()),
serde_json::Value::Object(object) => object
.get("title")
.filter(|value| !value.is_null() && value.as_str() != Some(""))
.or_else(|| object.get("code"))
.and_then(|value| match value {
serde_json::Value::String(message) => Some(message.clone()),
serde_json::Value::Null => None,
other => Some(other.to_string()),
}),
_ => None,
})
.collect(),
Some(serde_json::Value::Object(map)) => {
// Object-shaped envelopes (e.g. {"invalid_aliases": {...}}) carry
// data under arbitrary keys; surface each so it isn't silently
// dropped. Key order is unspecified, so sort for deterministic output.
let mut messages: Vec<String> = map
.iter()
.map(|(key, value)| match value {
serde_json::Value::String(message) => format!("{}: {}", key, message),
other => format!("{}: {}", key, other),
})
.collect();
messages.sort();
messages
}
_ => Vec::new(),
}
}
}

impl <T> fmt::Display for Error<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (module, e) = match self {
Expand Down
20 changes: 20 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Generated from inputs/api/error-catalog.json. Do not edit by hand.
//
// Sentinel error message strings the OneSignal API can return. Each
// constant equals the literal message the server emits, so you can test
// membership against Error::error_messages() (e.g. NO_TARGETING_SPECIFIED).
//
// Note: 200-status sentinels such as NO_SUBSCRIBERS arrive on a successful
// response, not via the exception accessor — read the response's errors
// field for those.

/// HTTP 403 | retryable: no | emitted by: any API-key-authenticated endpoint (REST or Organization key) | note: Generic auth-failure message the public api.onesignal.com edge returns for any invalid or mismatched key — REST or Organization — so a single sentinel covers both. Supersedes the Rails-monolith INVALID_REST_API_KEY / INVALID_USER_AUTH_KEY strings, which the public host no longer returns verbatim. Note the double space after 'denied.'
pub const INVALID_API_KEY: &str = "Access denied. Please include an 'Authorization: ...' header with a valid API key (https://documentation.onesignal.com/docs/en/keys-and-ids#api-keys).";
/// HTTP 400, 404 | retryable: no | emitted by: POST /notifications/{id}/history, POST /notifications/{id}/messages, GET /notifications/{id} (export) | note: Verified live 2026-06-16: GET /notifications/{bogus-uuid} returns 404 with this exact message.
pub const NOTIFICATION_NOT_FOUND: &str = "Notification not found";
/// HTTP 200 | retryable: no | emitted by: POST /notifications | note: Returned with HTTP 200 OK (id is empty), not an error status. The flagship case for the errorMessages accessor — lets callers distinguish a sent notification from a no-op without parsing the polymorphic 200 body.
pub const NO_SUBSCRIBERS: &str = "All included players are not subscribed";
/// HTTP 400 | retryable: no | emitted by: POST /notifications | note: Verified live 2026-06-16: a no-targeting POST /notifications returns 400 with this exact message.
pub const NO_TARGETING_SPECIFIED: &str = "You must include which players, segments, or tags you wish to send this notification to.";
/// HTTP 503 | retryable: yes | emitted by: any endpoint (pgbouncer rejection) | note: Transient DB/pgbouncer failure — the canonical retryable sentinel.
pub const SERVICE_UNAVAILABLE: &str = "Service temporarily unavailable";
178 changes: 178 additions & 0 deletions src/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! Helpers for common OneSignal API usage patterns.

use std::time::Duration;

use crate::apis::configuration;
use crate::apis::default_api::CreateNotificationError;
use crate::apis::{Error, ResponseContent};
use crate::models;

const RETRYABLE_STATUSES: [u16; 2] = [429, 503];
const MIN_BASE_DELAY: Duration = Duration::from_secs(1);
const MAX_BASE_DELAY: Duration = Duration::from_secs(60);

/// Options for [`create_notification_with_retry`].
#[derive(Debug, Clone)]
pub struct CreateNotificationWithRetryOptions {
/// Retries after the initial attempt. Default 3.
pub max_retries: u32,
/// Backoff base used when the response carries no `Retry-After` header.
/// Clamped to [1s, 60s]. Default 1s.
pub base_delay: Duration,
}

impl Default for CreateNotificationWithRetryOptions {
fn default() -> Self {
Self {
max_retries: 3,
base_delay: Duration::from_secs(1),
}
}
}

/// Result of [`create_notification_with_retry`]: the create response plus
/// whether the server replayed a previously completed request
/// (`Idempotent-Replayed` response header).
#[derive(Debug, Clone)]
pub struct CreateNotificationWithRetryResult {
pub response: models::CreateNotificationSuccessResponse,
pub was_replayed: bool,
}

enum AttemptOutcome {
Fatal(Error<CreateNotificationError>),
Retryable(Error<CreateNotificationError>, Option<Duration>),
}

/// Create a notification with safe, idempotent retries.
///
/// Ensures `notification.idempotency_key` is set (generating a UUIDv4 when
/// absent) so the server can deduplicate, then posts to `/notifications`.
/// Transient failures (HTTP 429, HTTP 503, or connect/timeout errors) are
/// retried with the SAME idempotency key, honoring the `Retry-After` response
/// header when present and falling back to exponential backoff
/// (`base_delay * 2^attempt`) otherwise. Other errors are returned
/// immediately. An existing idempotency key is respected, never overwritten.
///
/// NOTE: the request logic below mirrors `apis::default_api::create_notification`
/// (which does not expose response headers); keep the two in sync when the
/// generated operation changes.
pub async fn create_notification_with_retry(
configuration: &configuration::Configuration,
notification: &mut models::Notification,
options: Option<CreateNotificationWithRetryOptions>,
) -> Result<CreateNotificationWithRetryResult, Error<CreateNotificationError>> {
let opts = options.unwrap_or_default();
// Clamp the backoff base so a stray value can neither hammer the API (too
// small) nor stall the caller for an unbounded stretch (too large).
let base_delay = opts.base_delay.clamp(MIN_BASE_DELAY, MAX_BASE_DELAY);

if notification
.idempotency_key
.as_deref()
.map_or(true, |key| key.is_empty())
{
notification.idempotency_key = Some(uuid::Uuid::new_v4().to_string());
}

let mut attempt: u32 = 0;
loop {
match send_once(configuration, notification).await {
Ok(result) => return Ok(result),
Err(AttemptOutcome::Fatal(error)) => return Err(error),
Err(AttemptOutcome::Retryable(error, retry_after)) => {
if attempt >= opts.max_retries {
return Err(error);
}
let delay =
retry_after.unwrap_or_else(|| base_delay * 2u32.saturating_pow(attempt));
if !delay.is_zero() {
tokio::time::sleep(delay).await;
}
attempt += 1;
}
}
}
}

async fn send_once(
configuration: &configuration::Configuration,
notification: &models::Notification,
) -> Result<CreateNotificationWithRetryResult, AttemptOutcome> {
let client = &configuration.client;

let uri_str = format!("{}/notifications", configuration.base_path);
let mut req_builder = client.request(reqwest::Method::POST, uri_str.as_str());

if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}

// Adds a telemetry header
req_builder = req_builder.header(
"OS-Usage-Data",
concat!(
"kind=sdk, sdk-name=onesignal-rust, version=",
env!("CARGO_PKG_VERSION")
),
);

if let Some(ref token) = configuration.rest_api_key_token {
req_builder = req_builder.header("Authorization", format!("Key {}", token.to_owned()));
}
req_builder = req_builder.json(notification);

let req = req_builder
.build()
.map_err(|e| AttemptOutcome::Fatal(Error::Reqwest(e)))?;
let resp = match client.execute(req).await {
Ok(resp) => resp,
Err(e) => {
return Err(if e.is_timeout() || e.is_connect() {
AttemptOutcome::Retryable(Error::Reqwest(e), None)
} else {
AttemptOutcome::Fatal(Error::Reqwest(e))
});
}
};

let status = resp.status();
let was_replayed = header_value(&resp, "idempotent-replayed")
.map_or(false, |value| value.trim().eq_ignore_ascii_case("true"));
let retry_after = header_value(&resp, "retry-after")
.and_then(|value| value.trim().parse::<u64>().ok())
.map(Duration::from_secs);

let content = resp
.text()
.await
.map_err(|e| AttemptOutcome::Fatal(Error::Reqwest(e)))?;

if !status.is_client_error() && !status.is_server_error() {
let response = serde_json::from_str(&content)
.map_err(|e| AttemptOutcome::Fatal(Error::Serde(e)))?;
Ok(CreateNotificationWithRetryResult {
response,
was_replayed,
})
} else {
let entity: Option<CreateNotificationError> = serde_json::from_str(&content).ok();
let error = Error::ResponseError(ResponseContent {
status,
content,
entity,
});
Err(if RETRYABLE_STATUSES.contains(&status.as_u16()) {
AttemptOutcome::Retryable(error, retry_after)
} else {
AttemptOutcome::Fatal(error)
})
}
}

fn header_value(resp: &reqwest::Response, name: &str) -> Option<String> {
resp.headers()
.get(name)
.and_then(|value| value.to_str().ok())
.map(|value| value.to_owned())
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ extern crate url;
extern crate reqwest;

pub mod apis;
pub mod helpers;
pub mod models;
pub mod errors;
2 changes: 1 addition & 1 deletion src/models/api_key_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com
*
* The version of the OpenAPI document: 5.6.0
* The version of the OpenAPI document: 5.7.0
* Contact: devrel@onesignal.com
* Generated by: https://openapi-generator.tech
*/
Expand Down
2 changes: 1 addition & 1 deletion src/models/api_key_tokens_list_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com
*
* The version of the OpenAPI document: 5.6.0
* The version of the OpenAPI document: 5.7.0
* Contact: devrel@onesignal.com
* Generated by: https://openapi-generator.tech
*/
Expand Down
2 changes: 1 addition & 1 deletion src/models/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com
*
* The version of the OpenAPI document: 5.6.0
* The version of the OpenAPI document: 5.7.0
* Contact: devrel@onesignal.com
* Generated by: https://openapi-generator.tech
*/
Expand Down
2 changes: 1 addition & 1 deletion src/models/basic_notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com
*
* The version of the OpenAPI document: 5.6.0
* The version of the OpenAPI document: 5.7.0
* Contact: devrel@onesignal.com
* Generated by: https://openapi-generator.tech
*/
Expand Down
2 changes: 1 addition & 1 deletion src/models/basic_notification_all_of.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com
*
* The version of the OpenAPI document: 5.6.0
* The version of the OpenAPI document: 5.7.0
* Contact: devrel@onesignal.com
* Generated by: https://openapi-generator.tech
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com
*
* The version of the OpenAPI document: 5.6.0
* The version of the OpenAPI document: 5.7.0
* Contact: devrel@onesignal.com
* Generated by: https://openapi-generator.tech
*/
Expand Down
2 changes: 1 addition & 1 deletion src/models/button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com
*
* The version of the OpenAPI document: 5.6.0
* The version of the OpenAPI document: 5.7.0
* Contact: devrel@onesignal.com
* Generated by: https://openapi-generator.tech
*/
Expand Down
2 changes: 1 addition & 1 deletion src/models/copy_template_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* A powerful way to send personalized messages at scale and build effective customer engagement strategies. Learn more at onesignal.com
*
* The version of the OpenAPI document: 5.6.0
* The version of the OpenAPI document: 5.7.0
* Contact: devrel@onesignal.com
* Generated by: https://openapi-generator.tech
*/
Expand Down
Loading