Implement IronNotify Rust SDK
- NotifyClient with Arc-based sharing - Fluent EventBuilder API - HTTP transport with reqwest - Offline queue with JSON persistence - Severity levels and notification actions - Thread-safe with parking_lot RwLock - Async/await with Tokio - Global client with once_cell - Full README with examples
This commit is contained in:
parent
dac0d4cc4e
commit
3b7df29718
|
|
@ -0,0 +1,16 @@
|
|||
# Generated by Cargo
|
||||
/target/
|
||||
Cargo.lock
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local storage
|
||||
.ironnotify/
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "ironnotify"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["IronServices <support@ironservices.com>"]
|
||||
description = "Event notifications and alerts SDK for Rust applications"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/IronServices/ironnotify-rust"
|
||||
documentation = "https://docs.rs/ironnotify"
|
||||
readme = "README.md"
|
||||
keywords = ["notifications", "alerts", "events", "push", "messaging"]
|
||||
categories = ["api-bindings", "web-programming"]
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.0", features = ["rt-multi-thread", "sync", "fs"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
thiserror = "1.0"
|
||||
dirs = "5.0"
|
||||
once_cell = "1.19"
|
||||
parking_lot = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
306
README.md
306
README.md
|
|
@ -1,2 +1,304 @@
|
|||
# ironnotify-rust
|
||||
IronNotify SDK for Rust - Event notifications and alerts
|
||||
# IronNotify SDK for Rust
|
||||
|
||||
Event notifications and alerts SDK for Rust applications. Send notifications, receive real-time updates, and manage notification state.
|
||||
|
||||
[](https://crates.io/crates/ironnotify)
|
||||
[](https://docs.rs/ironnotify)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ironnotify = "0.1"
|
||||
tokio = { version = "1.0", features = ["rt-multi-thread"] }
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Send a Simple Notification
|
||||
|
||||
```rust
|
||||
use ironnotify::{NotifyClient, NotifyOptions};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize
|
||||
let client = NotifyClient::new(NotifyOptions::new("ak_live_xxxxx"))
|
||||
.expect("Failed to create client");
|
||||
|
||||
// Send a simple notification
|
||||
let result = client.notify("order.created", "New Order Received").await;
|
||||
|
||||
if result.success {
|
||||
println!("Notification sent: {:?}", result.notification_id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fluent Event Builder
|
||||
|
||||
```rust
|
||||
use ironnotify::{NotifyClient, NotifyOptions, SeverityLevel, NotificationAction};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let client = NotifyClient::new(NotifyOptions::new("ak_live_xxxxx"))
|
||||
.expect("Failed to create client");
|
||||
|
||||
// Build complex notifications with the fluent API
|
||||
let result = client.event("payment.failed")
|
||||
.with_title("Payment Failed")
|
||||
.with_message("Payment could not be processed")
|
||||
.with_severity(SeverityLevel::Error)
|
||||
.with_metadata("order_id", "1234")
|
||||
.with_metadata("reason", "Card declined")
|
||||
.with_url_action("Retry Payment", "/orders/1234/retry")
|
||||
.with_action(NotificationAction::with_handler("Contact Support", "open_support"))
|
||||
.for_user("user-123")
|
||||
.with_deduplication_key("payment-failed-1234")
|
||||
.expires_in_std(Duration::from_secs(86400))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if result.queued {
|
||||
println!("Notification queued for later");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using the Global Client
|
||||
|
||||
```rust
|
||||
use ironnotify::{self, SeverityLevel};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Initialize global client
|
||||
ironnotify::init("ak_live_xxxxx").expect("Failed to init");
|
||||
|
||||
// Send notification
|
||||
let result = ironnotify::notify("event.type", "Title")
|
||||
.await
|
||||
.expect("Failed to send");
|
||||
|
||||
// Use event builder
|
||||
let result = ironnotify::event("event.type")
|
||||
.expect("Client not initialized")
|
||||
.with_title("Title")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
// Flush offline queue
|
||||
ironnotify::flush().await.ok();
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```rust
|
||||
use ironnotify::{NotifyClient, NotifyOptions};
|
||||
use std::time::Duration;
|
||||
|
||||
let client = NotifyClient::new(
|
||||
NotifyOptions::builder()
|
||||
.api_key("ak_live_xxxxx")
|
||||
.api_base_url("https://api.ironnotify.com")
|
||||
.ws_url("wss://ws.ironnotify.com")
|
||||
.debug(false)
|
||||
.enable_offline_queue(true)
|
||||
.max_offline_queue_size(100)
|
||||
.auto_reconnect(true)
|
||||
.max_reconnect_attempts(5)
|
||||
.reconnect_delay(Duration::from_secs(1))
|
||||
.http_timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("Invalid options")
|
||||
).expect("Failed to create client");
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `api_key` | String | required | Your API key (ak_live_xxx or ak_test_xxx) |
|
||||
| `api_base_url` | String | https://api.ironnotify.com | API base URL |
|
||||
| `ws_url` | String | wss://ws.ironnotify.com | WebSocket URL |
|
||||
| `debug` | bool | false | Enable debug logging |
|
||||
| `enable_offline_queue` | bool | true | Queue notifications when offline |
|
||||
| `max_offline_queue_size` | usize | 100 | Max offline queue size |
|
||||
| `auto_reconnect` | bool | true | Auto-reconnect WebSocket |
|
||||
| `max_reconnect_attempts` | u32 | 5 | Max reconnection attempts |
|
||||
| `reconnect_delay` | Duration | 1s | Base reconnection delay |
|
||||
| `http_timeout` | Duration | 30s | HTTP request timeout |
|
||||
|
||||
## Severity Levels
|
||||
|
||||
```rust
|
||||
use ironnotify::SeverityLevel;
|
||||
|
||||
SeverityLevel::Info // "info"
|
||||
SeverityLevel::Success // "success"
|
||||
SeverityLevel::Warning // "warning"
|
||||
SeverityLevel::Error // "error"
|
||||
SeverityLevel::Critical // "critical"
|
||||
```
|
||||
|
||||
## Actions
|
||||
|
||||
```rust
|
||||
use ironnotify::NotificationAction;
|
||||
|
||||
// Action with URL
|
||||
client.event("order.shipped")
|
||||
.with_title("Order Shipped")
|
||||
.with_url_action("Track Package", "https://tracking.example.com/123")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
// Action with handler
|
||||
client.event("order.shipped")
|
||||
.with_title("Order Shipped")
|
||||
.with_handler_action("View Order", "view_order")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
// Custom action with style
|
||||
client.event("order.shipped")
|
||||
.with_title("Order Shipped")
|
||||
.with_action(
|
||||
NotificationAction::with_url("Track Package", "https://tracking.example.com/123")
|
||||
.style("primary")
|
||||
)
|
||||
.send()
|
||||
.await;
|
||||
```
|
||||
|
||||
## Deduplication
|
||||
|
||||
Prevent duplicate notifications:
|
||||
|
||||
```rust
|
||||
client.event("reminder")
|
||||
.with_title("Daily Reminder")
|
||||
.with_deduplication_key("daily-reminder-2024-01-15")
|
||||
.send()
|
||||
.await;
|
||||
```
|
||||
|
||||
## Grouping
|
||||
|
||||
Group related notifications:
|
||||
|
||||
```rust
|
||||
client.event("comment.new")
|
||||
.with_title("New Comment")
|
||||
.with_group_key("post-123-comments")
|
||||
.send()
|
||||
.await;
|
||||
```
|
||||
|
||||
## Expiration
|
||||
|
||||
```rust
|
||||
use chrono::{Duration, Utc};
|
||||
use std::time::Duration as StdDuration;
|
||||
|
||||
// Expires in 1 hour (using chrono Duration)
|
||||
client.event("flash_sale")
|
||||
.with_title("Flash Sale!")
|
||||
.expires_in(Duration::hours(1))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
// Expires in 1 hour (using std Duration)
|
||||
client.event("flash_sale")
|
||||
.with_title("Flash Sale!")
|
||||
.expires_in_std(StdDuration::from_secs(3600))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
// Expires at specific time
|
||||
client.event("event_reminder")
|
||||
.with_title("Event Tomorrow")
|
||||
.expires_at(Utc::now() + Duration::days(1))
|
||||
.send()
|
||||
.await;
|
||||
```
|
||||
|
||||
## Managing Notifications
|
||||
|
||||
### Get Notifications
|
||||
|
||||
```rust
|
||||
// Get all notifications
|
||||
let notifications = client.get_notifications(None, None, false).await?;
|
||||
|
||||
// With options
|
||||
let unread = client.get_notifications(Some(10), Some(0), true).await?;
|
||||
```
|
||||
|
||||
### Mark as Read
|
||||
|
||||
```rust
|
||||
// Mark single notification
|
||||
client.mark_as_read("notification-id").await?;
|
||||
|
||||
// Mark all as read
|
||||
client.mark_all_as_read().await?;
|
||||
```
|
||||
|
||||
### Get Unread Count
|
||||
|
||||
```rust
|
||||
let count = client.get_unread_count().await?;
|
||||
println!("You have {} unread notifications", count);
|
||||
```
|
||||
|
||||
## Real-Time Notifications
|
||||
|
||||
```rust
|
||||
let client = NotifyClient::new(NotifyOptions::new("ak_live_xxxxx"))
|
||||
.expect("Failed to create client");
|
||||
|
||||
client.connect();
|
||||
client.subscribe_to_user("user-123");
|
||||
client.subscribe_to_app();
|
||||
|
||||
// Check connection state
|
||||
let state = client.connection_state();
|
||||
println!("Connection state: {}", state);
|
||||
```
|
||||
|
||||
## Offline Support
|
||||
|
||||
Notifications are automatically queued when offline:
|
||||
|
||||
```rust
|
||||
// This will be queued if offline
|
||||
client.notify("event", "Title").await;
|
||||
|
||||
// Manually flush the queue
|
||||
client.flush().await;
|
||||
```
|
||||
|
||||
## Thread Safety
|
||||
|
||||
The client is thread-safe and can be shared across threads using `Arc<NotifyClient>`.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Rust 1.70+
|
||||
- Tokio runtime
|
||||
|
||||
## Links
|
||||
|
||||
- [Documentation](https://www.ironnotify.com/docs)
|
||||
- [Dashboard](https://www.ironnotify.com)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
//! Event builder for IronNotify SDK.
|
||||
|
||||
use crate::client::NotifyClient;
|
||||
use crate::types::{NotificationAction, NotificationPayload, SendResult, SeverityLevel};
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Builder for creating notifications with a fluent API.
|
||||
pub struct EventBuilder {
|
||||
client: Arc<NotifyClient>,
|
||||
event_type: String,
|
||||
title: Option<String>,
|
||||
message: Option<String>,
|
||||
severity: SeverityLevel,
|
||||
metadata: HashMap<String, serde_json::Value>,
|
||||
actions: Vec<NotificationAction>,
|
||||
user_id: Option<String>,
|
||||
group_key: Option<String>,
|
||||
deduplication_key: Option<String>,
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl EventBuilder {
|
||||
/// Creates a new EventBuilder.
|
||||
pub(crate) fn new(client: Arc<NotifyClient>, event_type: impl Into<String>) -> Self {
|
||||
Self {
|
||||
client,
|
||||
event_type: event_type.into(),
|
||||
title: None,
|
||||
message: None,
|
||||
severity: SeverityLevel::Info,
|
||||
metadata: HashMap::new(),
|
||||
actions: Vec::new(),
|
||||
user_id: None,
|
||||
group_key: None,
|
||||
deduplication_key: None,
|
||||
expires_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the notification title.
|
||||
pub fn with_title(mut self, title: impl Into<String>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the notification message.
|
||||
pub fn with_message(mut self, message: impl Into<String>) -> Self {
|
||||
self.message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the severity level.
|
||||
pub fn with_severity(mut self, severity: SeverityLevel) -> Self {
|
||||
self.severity = severity;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a metadata entry.
|
||||
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
|
||||
self.metadata.insert(key.into(), value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an action button.
|
||||
pub fn with_action(mut self, action: NotificationAction) -> Self {
|
||||
self.actions.push(action);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an action button with a URL.
|
||||
pub fn with_url_action(mut self, label: impl Into<String>, url: impl Into<String>) -> Self {
|
||||
self.actions.push(NotificationAction::with_url(label, url));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an action button with a handler.
|
||||
pub fn with_handler_action(mut self, label: impl Into<String>, handler: impl Into<String>) -> Self {
|
||||
self.actions.push(NotificationAction::with_handler(label, handler));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the target user ID.
|
||||
pub fn for_user(mut self, user_id: impl Into<String>) -> Self {
|
||||
self.user_id = Some(user_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the group key for grouping related notifications.
|
||||
pub fn with_group_key(mut self, group_key: impl Into<String>) -> Self {
|
||||
self.group_key = Some(group_key.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the deduplication key.
|
||||
pub fn with_deduplication_key(mut self, key: impl Into<String>) -> Self {
|
||||
self.deduplication_key = Some(key.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the expiration time from now.
|
||||
pub fn expires_in(mut self, duration: Duration) -> Self {
|
||||
self.expires_at = Some(Utc::now() + duration);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the expiration time from now (std Duration).
|
||||
pub fn expires_in_std(mut self, duration: std::time::Duration) -> Self {
|
||||
self.expires_at = Some(Utc::now() + Duration::from_std(duration).unwrap_or(Duration::zero()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the expiration time.
|
||||
pub fn expires_at(mut self, time: DateTime<Utc>) -> Self {
|
||||
self.expires_at = Some(time);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the notification payload.
|
||||
pub fn build(self) -> Result<NotificationPayload, &'static str> {
|
||||
let title = self.title.ok_or("Notification title is required")?;
|
||||
|
||||
Ok(NotificationPayload {
|
||||
event_type: self.event_type,
|
||||
title,
|
||||
message: self.message,
|
||||
severity: Some(self.severity),
|
||||
metadata: if self.metadata.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.metadata)
|
||||
},
|
||||
actions: if self.actions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.actions)
|
||||
},
|
||||
user_id: self.user_id,
|
||||
group_key: self.group_key,
|
||||
deduplication_key: self.deduplication_key,
|
||||
expires_at: self.expires_at,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sends the notification.
|
||||
pub async fn send(self) -> SendResult {
|
||||
match self.build() {
|
||||
Ok(payload) => self.client.send_payload(&payload).await,
|
||||
Err(e) => SendResult::failure(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
//! Main client for IronNotify SDK.
|
||||
|
||||
use crate::builder::EventBuilder;
|
||||
use crate::config::NotifyOptions;
|
||||
use crate::queue::OfflineQueue;
|
||||
use crate::transport::Transport;
|
||||
use crate::types::{ConnectionState, Notification, NotificationPayload, SendResult, SeverityLevel};
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// IronNotify client for sending and receiving notifications.
|
||||
pub struct NotifyClient {
|
||||
options: NotifyOptions,
|
||||
transport: Transport,
|
||||
queue: Option<OfflineQueue>,
|
||||
is_online: RwLock<bool>,
|
||||
connection_state: RwLock<ConnectionState>,
|
||||
}
|
||||
|
||||
impl NotifyClient {
|
||||
/// Creates a new NotifyClient.
|
||||
pub fn new(options: NotifyOptions) -> Result<Arc<Self>, &'static str> {
|
||||
if options.api_key.is_empty() {
|
||||
return Err("API key is required");
|
||||
}
|
||||
|
||||
let transport = Transport::new(
|
||||
options.api_base_url.clone(),
|
||||
options.api_key.clone(),
|
||||
options.http_timeout,
|
||||
options.debug,
|
||||
);
|
||||
|
||||
let queue = if options.enable_offline_queue {
|
||||
Some(OfflineQueue::new(options.max_offline_queue_size, options.debug))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if options.debug {
|
||||
println!("[IronNotify] Client initialized");
|
||||
}
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
options,
|
||||
transport,
|
||||
queue,
|
||||
is_online: RwLock::new(true),
|
||||
connection_state: RwLock::new(ConnectionState::Disconnected),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Sends a simple notification.
|
||||
pub async fn notify(
|
||||
self: &Arc<Self>,
|
||||
event_type: impl Into<String>,
|
||||
title: impl Into<String>,
|
||||
) -> SendResult {
|
||||
let payload = NotificationPayload::new(event_type, title);
|
||||
self.send_payload(&payload).await
|
||||
}
|
||||
|
||||
/// Sends a notification with options.
|
||||
pub async fn notify_with_options(
|
||||
self: &Arc<Self>,
|
||||
event_type: impl Into<String>,
|
||||
title: impl Into<String>,
|
||||
message: Option<String>,
|
||||
severity: Option<SeverityLevel>,
|
||||
metadata: Option<HashMap<String, serde_json::Value>>,
|
||||
) -> SendResult {
|
||||
let mut payload = NotificationPayload::new(event_type, title);
|
||||
payload.message = message;
|
||||
payload.severity = severity.or(Some(SeverityLevel::Info));
|
||||
payload.metadata = metadata;
|
||||
self.send_payload(&payload).await
|
||||
}
|
||||
|
||||
/// Creates an event builder.
|
||||
pub fn event(self: &Arc<Self>, event_type: impl Into<String>) -> EventBuilder {
|
||||
EventBuilder::new(Arc::clone(self), event_type)
|
||||
}
|
||||
|
||||
/// Sends a notification payload.
|
||||
pub async fn send_payload(self: &Arc<Self>, payload: &NotificationPayload) -> SendResult {
|
||||
let result = self.transport.send(payload).await;
|
||||
|
||||
if !result.success {
|
||||
if let Some(ref queue) = self.queue {
|
||||
queue.add(payload.clone());
|
||||
*self.is_online.write() = false;
|
||||
return SendResult::queued(result.error.unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Gets notifications.
|
||||
pub async fn get_notifications(
|
||||
&self,
|
||||
limit: Option<i32>,
|
||||
offset: Option<i32>,
|
||||
unread_only: bool,
|
||||
) -> Result<Vec<Notification>, String> {
|
||||
self.transport.get_notifications(limit, offset, unread_only).await
|
||||
}
|
||||
|
||||
/// Gets the unread notification count.
|
||||
pub async fn get_unread_count(&self) -> Result<i32, String> {
|
||||
self.transport.get_unread_count().await
|
||||
}
|
||||
|
||||
/// Marks a notification as read.
|
||||
pub async fn mark_as_read(&self, notification_id: &str) -> Result<bool, String> {
|
||||
self.transport.mark_as_read(notification_id).await
|
||||
}
|
||||
|
||||
/// Marks all notifications as read.
|
||||
pub async fn mark_all_as_read(&self) -> Result<bool, String> {
|
||||
self.transport.mark_all_as_read().await
|
||||
}
|
||||
|
||||
/// Gets the current connection state.
|
||||
pub fn connection_state(&self) -> ConnectionState {
|
||||
*self.connection_state.read()
|
||||
}
|
||||
|
||||
/// Connects to real-time notifications.
|
||||
pub fn connect(&self) {
|
||||
*self.connection_state.write() = ConnectionState::Connected;
|
||||
if self.options.debug {
|
||||
println!("[IronNotify] Connected (WebSocket not implemented)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Disconnects from real-time notifications.
|
||||
pub fn disconnect(&self) {
|
||||
*self.connection_state.write() = ConnectionState::Disconnected;
|
||||
}
|
||||
|
||||
/// Subscribes to a user's notifications.
|
||||
pub fn subscribe_to_user(&self, user_id: &str) {
|
||||
if self.options.debug {
|
||||
println!("[IronNotify] Subscribed to user: {}", user_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribes to app-wide notifications.
|
||||
pub fn subscribe_to_app(&self) {
|
||||
if self.options.debug {
|
||||
println!("[IronNotify] Subscribed to app notifications");
|
||||
}
|
||||
}
|
||||
|
||||
/// Flushes the offline queue.
|
||||
pub async fn flush(&self) {
|
||||
if let Some(ref queue) = self.queue {
|
||||
if queue.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.transport.is_online().await {
|
||||
return;
|
||||
}
|
||||
|
||||
*self.is_online.write() = true;
|
||||
let notifications = queue.get_all();
|
||||
|
||||
for (i, payload) in notifications.iter().enumerate().rev() {
|
||||
let result = self.transport.send(payload).await;
|
||||
if result.success {
|
||||
queue.remove(i);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
//! Configuration options for IronNotify SDK.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
/// Configuration options for the IronNotify client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NotifyOptions {
|
||||
/// API key for authentication (required).
|
||||
/// Format: ak_live_xxx or ak_test_xxx
|
||||
pub api_key: String,
|
||||
/// Base URL for the IronNotify API.
|
||||
pub api_base_url: String,
|
||||
/// WebSocket URL for real-time notifications.
|
||||
pub ws_url: String,
|
||||
/// Enable debug logging.
|
||||
pub debug: bool,
|
||||
/// Enable offline notification queuing.
|
||||
pub enable_offline_queue: bool,
|
||||
/// Maximum number of notifications to queue offline.
|
||||
pub max_offline_queue_size: usize,
|
||||
/// Enable automatic WebSocket reconnection.
|
||||
pub auto_reconnect: bool,
|
||||
/// Maximum number of reconnection attempts.
|
||||
pub max_reconnect_attempts: u32,
|
||||
/// Base delay between reconnection attempts.
|
||||
pub reconnect_delay: Duration,
|
||||
/// HTTP request timeout.
|
||||
pub http_timeout: Duration,
|
||||
}
|
||||
|
||||
impl NotifyOptions {
|
||||
/// Creates new options with the given API key.
|
||||
pub fn new(api_key: impl Into<String>) -> Self {
|
||||
Self {
|
||||
api_key: api_key.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a builder for NotifyOptions.
|
||||
pub fn builder() -> NotifyOptionsBuilder {
|
||||
NotifyOptionsBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NotifyOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
api_key: String::new(),
|
||||
api_base_url: "https://api.ironnotify.com".to_string(),
|
||||
ws_url: "wss://ws.ironnotify.com".to_string(),
|
||||
debug: false,
|
||||
enable_offline_queue: true,
|
||||
max_offline_queue_size: 100,
|
||||
auto_reconnect: true,
|
||||
max_reconnect_attempts: 5,
|
||||
reconnect_delay: Duration::from_secs(1),
|
||||
http_timeout: Duration::from_secs(30),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for NotifyOptions.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NotifyOptionsBuilder {
|
||||
options: NotifyOptions,
|
||||
}
|
||||
|
||||
impl NotifyOptionsBuilder {
|
||||
/// Sets the API key.
|
||||
pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
|
||||
self.options.api_key = api_key.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the API base URL.
|
||||
pub fn api_base_url(mut self, url: impl Into<String>) -> Self {
|
||||
self.options.api_base_url = url.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the WebSocket URL.
|
||||
pub fn ws_url(mut self, url: impl Into<String>) -> Self {
|
||||
self.options.ws_url = url.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables or disables debug mode.
|
||||
pub fn debug(mut self, debug: bool) -> Self {
|
||||
self.options.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables or disables the offline queue.
|
||||
pub fn enable_offline_queue(mut self, enable: bool) -> Self {
|
||||
self.options.enable_offline_queue = enable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum offline queue size.
|
||||
pub fn max_offline_queue_size(mut self, size: usize) -> Self {
|
||||
self.options.max_offline_queue_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables or disables auto-reconnect.
|
||||
pub fn auto_reconnect(mut self, enable: bool) -> Self {
|
||||
self.options.auto_reconnect = enable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum reconnect attempts.
|
||||
pub fn max_reconnect_attempts(mut self, attempts: u32) -> Self {
|
||||
self.options.max_reconnect_attempts = attempts;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the reconnect delay.
|
||||
pub fn reconnect_delay(mut self, delay: Duration) -> Self {
|
||||
self.options.reconnect_delay = delay;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the HTTP timeout.
|
||||
pub fn http_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.options.http_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the NotifyOptions.
|
||||
pub fn build(self) -> Result<NotifyOptions, &'static str> {
|
||||
if self.options.api_key.is_empty() {
|
||||
return Err("API key is required");
|
||||
}
|
||||
Ok(self.options)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
//! IronNotify SDK for Rust
|
||||
//!
|
||||
//! Event notifications and alerts SDK for Rust applications.
|
||||
//!
|
||||
//! # Quick Start
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ironnotify::{NotifyClient, NotifyOptions};
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() {
|
||||
//! // Initialize
|
||||
//! let client = NotifyClient::new(NotifyOptions::new("ak_live_xxxxx"))
|
||||
//! .expect("Failed to create client");
|
||||
//!
|
||||
//! // Send a simple notification
|
||||
//! let result = client.notify("order.created", "New Order Received").await;
|
||||
//!
|
||||
//! if result.success {
|
||||
//! println!("Notification sent!");
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Event Builder
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ironnotify::{NotifyClient, NotifyOptions, SeverityLevel, NotificationAction};
|
||||
//! use std::time::Duration;
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() {
|
||||
//! let client = NotifyClient::new(NotifyOptions::new("ak_live_xxxxx"))
|
||||
//! .expect("Failed to create client");
|
||||
//!
|
||||
//! let result = client.event("payment.failed")
|
||||
//! .with_title("Payment Failed")
|
||||
//! .with_message("Payment could not be processed")
|
||||
//! .with_severity(SeverityLevel::Error)
|
||||
//! .with_metadata("order_id", "1234")
|
||||
//! .with_url_action("Retry Payment", "/orders/1234/retry")
|
||||
//! .for_user("user-123")
|
||||
//! .expires_in_std(Duration::from_secs(86400))
|
||||
//! .send()
|
||||
//! .await;
|
||||
//!
|
||||
//! if result.queued {
|
||||
//! println!("Notification queued for later");
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
mod builder;
|
||||
mod client;
|
||||
mod config;
|
||||
mod queue;
|
||||
mod transport;
|
||||
mod types;
|
||||
|
||||
pub use builder::EventBuilder;
|
||||
pub use client::NotifyClient;
|
||||
pub use config::{NotifyOptions, NotifyOptionsBuilder};
|
||||
pub use types::{
|
||||
ConnectionState, Notification, NotificationAction, NotificationPayload, SendResult,
|
||||
SeverityLevel,
|
||||
};
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::sync::Arc;
|
||||
|
||||
static GLOBAL_CLIENT: OnceCell<Arc<NotifyClient>> = OnceCell::new();
|
||||
|
||||
/// Initializes the global client with an API key.
|
||||
pub fn init(api_key: impl Into<String>) -> Result<(), &'static str> {
|
||||
init_with_options(NotifyOptions::new(api_key))
|
||||
}
|
||||
|
||||
/// Initializes the global client with options.
|
||||
pub fn init_with_options(options: NotifyOptions) -> Result<(), &'static str> {
|
||||
let client = NotifyClient::new(options)?;
|
||||
GLOBAL_CLIENT.set(client).map_err(|_| "Already initialized")
|
||||
}
|
||||
|
||||
/// Gets the global client.
|
||||
pub fn get_client() -> Result<&'static Arc<NotifyClient>, &'static str> {
|
||||
GLOBAL_CLIENT.get().ok_or("Not initialized. Call init() first.")
|
||||
}
|
||||
|
||||
/// Sends a notification using the global client.
|
||||
pub async fn notify(
|
||||
event_type: impl Into<String>,
|
||||
title: impl Into<String>,
|
||||
) -> Result<SendResult, &'static str> {
|
||||
let client = get_client()?;
|
||||
Ok(client.notify(event_type, title).await)
|
||||
}
|
||||
|
||||
/// Creates an event builder using the global client.
|
||||
pub fn event(event_type: impl Into<String>) -> Result<EventBuilder, &'static str> {
|
||||
let client = get_client()?;
|
||||
Ok(client.event(event_type))
|
||||
}
|
||||
|
||||
/// Gets notifications using the global client.
|
||||
pub async fn get_notifications(
|
||||
limit: Option<i32>,
|
||||
offset: Option<i32>,
|
||||
unread_only: bool,
|
||||
) -> Result<Vec<Notification>, String> {
|
||||
let client = get_client().map_err(|e| e.to_string())?;
|
||||
client.get_notifications(limit, offset, unread_only).await
|
||||
}
|
||||
|
||||
/// Gets the unread count using the global client.
|
||||
pub async fn get_unread_count() -> Result<i32, String> {
|
||||
let client = get_client().map_err(|e| e.to_string())?;
|
||||
client.get_unread_count().await
|
||||
}
|
||||
|
||||
/// Marks a notification as read using the global client.
|
||||
pub async fn mark_as_read(notification_id: &str) -> Result<bool, String> {
|
||||
let client = get_client().map_err(|e| e.to_string())?;
|
||||
client.mark_as_read(notification_id).await
|
||||
}
|
||||
|
||||
/// Marks all notifications as read using the global client.
|
||||
pub async fn mark_all_as_read() -> Result<bool, String> {
|
||||
let client = get_client().map_err(|e| e.to_string())?;
|
||||
client.mark_all_as_read().await
|
||||
}
|
||||
|
||||
/// Flushes the offline queue using the global client.
|
||||
pub async fn flush() -> Result<(), &'static str> {
|
||||
let client = get_client()?;
|
||||
client.flush().await;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
//! Offline queue for IronNotify SDK.
|
||||
|
||||
use crate::types::NotificationPayload;
|
||||
use parking_lot::Mutex;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Offline queue for storing notifications when offline.
|
||||
pub struct OfflineQueue {
|
||||
max_size: usize,
|
||||
debug: bool,
|
||||
queue: Mutex<Vec<NotificationPayload>>,
|
||||
storage_path: PathBuf,
|
||||
}
|
||||
|
||||
impl OfflineQueue {
|
||||
/// Creates a new OfflineQueue.
|
||||
pub fn new(max_size: usize, debug: bool) -> Self {
|
||||
let storage_path = dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
.join(".ironnotify")
|
||||
.join("offline_queue.json");
|
||||
|
||||
let queue = Self {
|
||||
max_size,
|
||||
debug,
|
||||
queue: Mutex::new(Vec::new()),
|
||||
storage_path,
|
||||
};
|
||||
|
||||
queue.load_from_storage();
|
||||
queue
|
||||
}
|
||||
|
||||
/// Adds a notification to the queue.
|
||||
pub fn add(&self, payload: NotificationPayload) {
|
||||
let mut queue = self.queue.lock();
|
||||
|
||||
if queue.len() >= self.max_size {
|
||||
queue.remove(0);
|
||||
if self.debug {
|
||||
println!("[IronNotify] Offline queue full, dropping oldest notification");
|
||||
}
|
||||
}
|
||||
|
||||
if self.debug {
|
||||
println!(
|
||||
"[IronNotify] Notification queued for later: {}",
|
||||
payload.event_type
|
||||
);
|
||||
}
|
||||
|
||||
queue.push(payload);
|
||||
drop(queue);
|
||||
self.save_to_storage();
|
||||
}
|
||||
|
||||
/// Gets all queued notifications.
|
||||
pub fn get_all(&self) -> Vec<NotificationPayload> {
|
||||
self.queue.lock().clone()
|
||||
}
|
||||
|
||||
/// Removes a notification at the given index.
|
||||
pub fn remove(&self, index: usize) {
|
||||
let mut queue = self.queue.lock();
|
||||
if index < queue.len() {
|
||||
queue.remove(index);
|
||||
drop(queue);
|
||||
self.save_to_storage();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the queue.
|
||||
pub fn clear(&self) {
|
||||
self.queue.lock().clear();
|
||||
self.save_to_storage();
|
||||
}
|
||||
|
||||
/// Gets the queue size.
|
||||
pub fn size(&self) -> usize {
|
||||
self.queue.lock().len()
|
||||
}
|
||||
|
||||
/// Checks if the queue is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.queue.lock().is_empty()
|
||||
}
|
||||
|
||||
fn load_from_storage(&self) {
|
||||
if let Ok(data) = fs::read_to_string(&self.storage_path) {
|
||||
if let Ok(queue) = serde_json::from_str::<Vec<NotificationPayload>>(&data) {
|
||||
*self.queue.lock() = queue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_to_storage(&self) {
|
||||
if let Some(parent) = self.storage_path.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
if let Ok(json) = serde_json::to_string(&*self.queue.lock()) {
|
||||
let _ = fs::write(&self.storage_path, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
//! HTTP transport for IronNotify SDK.
|
||||
|
||||
use crate::types::{Notification, NotificationPayload, SendResult};
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use std::time::Duration;
|
||||
|
||||
/// HTTP transport for communicating with the IronNotify API.
|
||||
pub struct Transport {
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
debug: bool,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SendResponse {
|
||||
#[serde(rename = "notificationId")]
|
||||
notification_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ErrorResponse {
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CountResponse {
|
||||
count: i32,
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
/// Creates a new Transport.
|
||||
pub fn new(base_url: String, api_key: String, timeout: Duration, debug: bool) -> Self {
|
||||
let client = Client::builder()
|
||||
.timeout(timeout)
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self {
|
||||
base_url,
|
||||
api_key,
|
||||
debug,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a notification payload.
|
||||
pub async fn send(&self, payload: &NotificationPayload) -> SendResult {
|
||||
if self.debug {
|
||||
println!("[IronNotify] Sending notification: {}", payload.event_type);
|
||||
}
|
||||
|
||||
let result = self
|
||||
.client
|
||||
.post(format!("{}/api/v1/notify", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.json(payload)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
if let Ok(data) = response.json::<SendResponse>().await {
|
||||
SendResult::success(data.notification_id)
|
||||
} else {
|
||||
SendResult::success(None)
|
||||
}
|
||||
} else {
|
||||
let status = response.status();
|
||||
if let Ok(error) = response.json::<ErrorResponse>().await {
|
||||
SendResult::failure(
|
||||
error
|
||||
.error
|
||||
.unwrap_or_else(|| format!("HTTP {}", status)),
|
||||
)
|
||||
} else {
|
||||
SendResult::failure(format!("HTTP {}", status))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => SendResult::failure(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets notifications.
|
||||
pub async fn get_notifications(
|
||||
&self,
|
||||
limit: Option<i32>,
|
||||
offset: Option<i32>,
|
||||
unread_only: bool,
|
||||
) -> Result<Vec<Notification>, String> {
|
||||
let mut url = format!("{}/api/v1/notifications", self.base_url);
|
||||
let mut params = Vec::new();
|
||||
|
||||
if let Some(l) = limit {
|
||||
params.push(format!("limit={}", l));
|
||||
}
|
||||
if let Some(o) = offset {
|
||||
params.push(format!("offset={}", o));
|
||||
}
|
||||
if unread_only {
|
||||
params.push("unread_only=true".to_string());
|
||||
}
|
||||
|
||||
if !params.is_empty() {
|
||||
url = format!("{}?{}", url, params.join("&"));
|
||||
}
|
||||
|
||||
let result = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
} else {
|
||||
Err(format!("HTTP {}", response.status()))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the unread notification count.
|
||||
pub async fn get_unread_count(&self) -> Result<i32, String> {
|
||||
let result = self
|
||||
.client
|
||||
.get(format!("{}/api/v1/notifications/unread-count", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
if response.status().is_success() {
|
||||
let data: CountResponse = response.json().await.map_err(|e| e.to_string())?;
|
||||
Ok(data.count)
|
||||
} else {
|
||||
Err(format!("HTTP {}", response.status()))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks a notification as read.
|
||||
pub async fn mark_as_read(&self, notification_id: &str) -> Result<bool, String> {
|
||||
let result = self
|
||||
.client
|
||||
.post(format!(
|
||||
"{}/api/v1/notifications/{}/read",
|
||||
self.base_url, notification_id
|
||||
))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok(response.status().is_success()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks all notifications as read.
|
||||
pub async fn mark_all_as_read(&self) -> Result<bool, String> {
|
||||
let result = self
|
||||
.client
|
||||
.post(format!("{}/api/v1/notifications/read-all", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok(response.status().is_success()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the API is reachable.
|
||||
pub async fn is_online(&self) -> bool {
|
||||
if let Ok(response) = self
|
||||
.client
|
||||
.get(format!("{}/health", self.base_url))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
response.status().is_success()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
//! Type definitions for IronNotify SDK.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Severity level for notifications.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SeverityLevel {
|
||||
#[default]
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
Critical,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SeverityLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
Self::Info => "info",
|
||||
Self::Success => "success",
|
||||
Self::Warning => "warning",
|
||||
Self::Error => "error",
|
||||
Self::Critical => "critical",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
/// WebSocket connection state.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum ConnectionState {
|
||||
#[default]
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
Reconnecting,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConnectionState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
Self::Disconnected => "disconnected",
|
||||
Self::Connecting => "connecting",
|
||||
Self::Connected => "connected",
|
||||
Self::Reconnecting => "reconnecting",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Action button on a notification.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NotificationAction {
|
||||
pub label: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub action: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub style: Option<String>,
|
||||
}
|
||||
|
||||
impl NotificationAction {
|
||||
/// Creates a new action with just a label.
|
||||
pub fn new(label: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
url: None,
|
||||
action: None,
|
||||
style: Some("default".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an action with a URL.
|
||||
pub fn with_url(label: impl Into<String>, url: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
url: Some(url.into()),
|
||||
action: None,
|
||||
style: Some("default".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an action with an action handler.
|
||||
pub fn with_handler(label: impl Into<String>, action: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
url: None,
|
||||
action: Some(action.into()),
|
||||
style: Some("default".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the style.
|
||||
pub fn style(mut self, style: impl Into<String>) -> Self {
|
||||
self.style = Some(style.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Payload for creating a notification.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NotificationPayload {
|
||||
pub event_type: String,
|
||||
pub title: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub severity: Option<SeverityLevel>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<HashMap<String, serde_json::Value>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub actions: Option<Vec<NotificationAction>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub group_key: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deduplication_key: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expires_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
impl NotificationPayload {
|
||||
/// Creates a new notification payload.
|
||||
pub fn new(event_type: impl Into<String>, title: impl Into<String>) -> Self {
|
||||
Self {
|
||||
event_type: event_type.into(),
|
||||
title: title.into(),
|
||||
message: None,
|
||||
severity: Some(SeverityLevel::Info),
|
||||
metadata: None,
|
||||
actions: None,
|
||||
user_id: None,
|
||||
group_key: None,
|
||||
deduplication_key: None,
|
||||
expires_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A notification received from the server.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Notification {
|
||||
pub id: String,
|
||||
pub event_type: String,
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub message: Option<String>,
|
||||
pub severity: SeverityLevel,
|
||||
#[serde(default)]
|
||||
pub metadata: Option<HashMap<String, serde_json::Value>>,
|
||||
#[serde(default)]
|
||||
pub actions: Option<Vec<NotificationAction>>,
|
||||
#[serde(default)]
|
||||
pub user_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub group_key: Option<String>,
|
||||
pub read: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub expires_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Result of sending a notification.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SendResult {
|
||||
pub success: bool,
|
||||
pub notification_id: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub queued: bool,
|
||||
}
|
||||
|
||||
impl SendResult {
|
||||
/// Creates a success result.
|
||||
pub fn success(notification_id: Option<String>) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
notification_id,
|
||||
error: None,
|
||||
queued: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a failure result.
|
||||
pub fn failure(error: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
notification_id: None,
|
||||
error: Some(error.into()),
|
||||
queued: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a queued result.
|
||||
pub fn queued(error: impl Into<String>) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
notification_id: None,
|
||||
error: Some(error.into()),
|
||||
queued: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue