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
|
||||||
IronNotify SDK for Rust - Event notifications and alerts
|
|
||||||
|
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