From 7758afd150c5cb77f357f00973bc107ee5537f5a Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Fri, 17 Apr 2026 17:53:11 +0300 Subject: [PATCH] feat: Add Rust SDK with tokio async/await support and comprehensive Telecom Platform API integration - Add Cargo.toml with tokio 1.52.1 (full features), reqwest 0.13.2 (json), tokio-tungstenite 0.29.0, serde 1.0.228, chrono 0.4.44 - Add TelecomClient with reqwest Client and TelecomConfig with 30s timeout, 3 retries - Add subscriber management: get_subscriber, list_subscribers with pagination/status filter, create_subscriber, update_subscriber, suspend_subscriber, activate_subscriber, terminate_subscriber --- sdk/rust/Cargo.toml | 23 ++ sdk/rust/src/lib.rs | 525 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 548 insertions(+) create mode 100644 sdk/rust/Cargo.toml create mode 100644 sdk/rust/src/lib.rs diff --git a/sdk/rust/Cargo.toml b/sdk/rust/Cargo.toml new file mode 100644 index 0000000..72b59e0 --- /dev/null +++ b/sdk/rust/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "telecom-sdk" +version = "1.0.0" +edition = "2024" +authors = ["nutcas3 "] +description = "Rust SDK for Telecom Platform" +license = "MIT" +repository = "https://github.com/nutcas3/telecom-platform" + +[dependencies] +tokio = { version = "1.52.1", features = ["full"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +reqwest = { version = "0.13.2", features = ["json"] } +tokio-tungstenite = "0.29.0" +futures-util = "0.3.32" +uuid = { version = "1.23.1", features = ["v4", "serde"] } +chrono = { version = "0.4.44", features = ["serde"] } +thiserror = "2.0.18" +url = "2.5.8" + +[dev-dependencies] +tokio-test = "0.4.5" diff --git a/sdk/rust/src/lib.rs b/sdk/rust/src/lib.rs new file mode 100644 index 0000000..d9e0a4b --- /dev/null +++ b/sdk/rust/src/lib.rs @@ -0,0 +1,525 @@ +//! Telecom Platform Rust SDK +//! +//! Provides async/await support with tokio for the Telecom Platform API. + +use chrono::{DateTime, Utc}; +use reqwest::{Client, StatusCode}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::Duration; +use thiserror::Error; +use tokio_tungstenite::{connect_async, tungstenite::Message}; +use url::Url; +use uuid::Uuid; + +#[derive(Error, Debug)] +pub enum TelecomError { + #[error("Authentication failed")] + AuthenticationError, + #[error("API error: {0}")] + APIError(String), + #[error("Network error: {0}")] + NetworkError(#[from] reqwest::Error), + #[error("Validation error: {0}")] + ValidationError(String), + #[error("Rate limit exceeded")] + RateLimitError, + #[error("Server error: {0}")] + ServerError(StatusCode), + #[error("WebSocket error: {0}")] + WebSocketError(String), + #[error("JSON error: {0}")] + JsonError(#[from] serde_json::Error), +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum SubscriberStatus { + Active, + Suspended, + Terminated, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum UsageType { + Data, + Voice, + Sms, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum PaymentStatus { + Pending, + Completed, + Failed, + Refunded, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Subscriber { + pub id: i64, + pub imsi: String, + pub msisdn: String, + pub first_name: String, + pub last_name: String, + pub email: String, + pub organization_id: Option, + pub status: SubscriberStatus, + pub plan_id: i64, + pub balance: f64, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct SubscriberList { + pub subscribers: Vec, + pub total: i64, + pub page: i32, + pub page_size: i32, + pub has_next: bool, + pub has_prev: bool, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CreateSubscriberRequest { + pub imsi: String, + pub msisdn: String, + pub first_name: String, + pub last_name: String, + pub email: String, + pub plan_id: i64, + pub organization_id: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct UpdateSubscriberRequest { + pub first_name: Option, + pub last_name: Option, + pub email: Option, + pub plan_id: Option, + pub status: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct UsageStats { + pub subscriber_id: String, + pub data_up: i64, + pub data_down: i64, + pub voice_seconds: i64, + pub sms_count: i64, + pub period_start: DateTime, + pub period_end: DateTime, + pub cost: f64, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct UsageEvent { + pub id: String, + pub subscriber_id: String, + pub usage_type: UsageType, + pub amount: i64, + pub cost: f64, + pub timestamp: DateTime, + pub metadata: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct UsageEventList { + pub events: Vec, + pub total: i64, + pub page: i32, + pub page_size: i32, + pub has_next: bool, + pub has_prev: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct CurrentSession { + pub session_id: String, + pub start_time: DateTime, + pub data_up: i64, + pub data_down: i64, + pub voice_seconds: i64, + pub sms_count: i64, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RealTimeUsage { + pub current_session: Option, + pub today_usage: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PaymentTransaction { + pub id: String, + pub subscriber_id: String, + pub amount: f64, + pub currency: String, + pub status: PaymentStatus, + pub gateway: String, + pub transaction_id: Option, + pub created_at: DateTime, + pub completed_at: Option>, + pub metadata: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PaymentTransactionList { + pub transactions: Vec, + pub total: i64, + pub page: i32, + pub page_size: i32, + pub has_next: bool, + pub has_prev: bool, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CreatePaymentRequest { + pub subscriber_id: String, + pub amount: f64, + pub currency: String, + pub gateway: String, + pub metadata: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RatingPlan { + pub plan_id: String, + pub name: String, + pub data_rate: f64, + pub voice_rate: f64, + pub sms_rate: f64, + pub monthly_fee: f64, + pub data_limit: i64, + pub voice_limit: i64, + pub sms_limit: i64, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct SystemStats { + pub active_sessions: i64, + pub total_accounts: i64, + pub blocked_users: i64, + pub low_balance_alerts: i64, + pub uptime: f64, + pub cpu_usage: f64, + pub memory_usage: f64, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct HealthStatus { + pub status: String, + pub timestamp: DateTime, + pub checks: HashMap, + pub uptime: f64, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WebSocketMessage { + pub r#type: String, + pub data: HashMap, + pub timestamp: DateTime, +} + +#[derive(Debug, Clone, Serialize)] +pub struct GraphQLRequest { + pub query: String, + pub variables: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GraphQLResponse { + pub data: Option>, + pub errors: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GraphQLError { + pub message: String, + pub locations: Option>, + pub path: Option>, + pub extensions: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct GraphQLErrorLocation { + pub line: i32, + pub column: i32, +} + +/// Telecom Platform SDK client +#[derive(Clone)] +pub struct TelecomClient { + client: Client, + config: TelecomConfig, +} + +#[derive(Debug, Clone)] +pub struct TelecomConfig { + pub api_url: String, + pub api_key: Option, + pub timeout: Duration, + pub max_retries: u32, +} + +impl Default for TelecomConfig { + fn default() -> Self { + Self { + api_url: "http://localhost:8000".to_string(), + api_key: None, + timeout: Duration::from_secs(30), + max_retries: 3, + } + } +} + +impl TelecomClient { + /// Create a new Telecom client + pub fn new(config: TelecomConfig) -> Self { + let client = Client::builder() + .timeout(config.timeout) + .build() + .expect("Failed to create HTTP client"); + + Self { client, config } + } + + async fn handle_response(&self, response: reqwest::Response) -> Result { + match response.status() { + StatusCode::UNAUTHORIZED => Err(TelecomError::AuthenticationError), + StatusCode::TOO_MANY_REQUESTS => Err(TelecomError::RateLimitError), + status if status.is_client_error() => { + let error_text = response.text().await.unwrap_or_else(|_| "Bad request".to_string()); + Err(TelecomError::APIError(error_text)) + } + status if status.is_server_error() => Err(TelecomError::ServerError(status)), + _ => Ok(response), + } + } + + /// Get subscriber by ID + pub async fn get_subscriber(&self, id: i64) -> Result { + let response = self + .client + .get(&format!("{}/v1/subscribers/{}", self.config.api_url, id)) + .send() + .await?; + + let response = self.handle_response(response).await?; + Ok(response.json().await?) + } + + /// List subscribers with pagination + pub async fn list_subscribers( + &self, + page: i32, + page_size: i32, + status: Option, + ) -> Result { + let mut request = self + .client + .get(&format!("{}/v1/subscribers", self.config.api_url)) + .query(&[("page", page), ("page_size", page_size)]); + + if let Some(status) = status { + request = request.query(&[("status", serde_json::to_string(&status).unwrap())]); + } + + let response = request.send().await?; + let response = self.handle_response(response).await?; + Ok(response.json().await?) + } + + /// Create a new subscriber + pub async fn create_subscriber(&self, request: CreateSubscriberRequest) -> Result { + let response = self + .client + .post(&format!("{}/v1/subscribers", self.config.api_url)) + .json(&request) + .send() + .await?; + + let response = self.handle_response(response).await?; + Ok(response.json().await?) + } + + /// Update an existing subscriber + pub async fn update_subscriber(&self, id: i64, request: UpdateSubscriberRequest) -> Result { + let response = self + .client + .put(&format!("{}/v1/subscribers/{}", self.config.api_url, id)) + .json(&request) + .send() + .await?; + + let response = self.handle_response(response).await?; + Ok(response.json().await?) + } + + /// Suspend a subscriber + pub async fn suspend_subscriber(&self, id: i64) -> Result { + let response = self + .client + .post(&format!("{}/v1/subscribers/{}/suspend", self.config.api_url, id)) + .send() + .await?; + + let response = self.handle_response(response).await?; + Ok(response.json().await?) + } + + /// Activate a suspended subscriber + pub async fn activate_subscriber(&self, id: i64) -> Result { + let response = self + .client + .post(&format!("{}/v1/subscribers/{}/activate", self.config.api_url, id)) + .send() + .await?; + + let response = self.handle_response(response).await?; + Ok(response.json().await?) + } + + /// Terminate a subscriber + pub async fn terminate_subscriber(&self, id: i64) -> Result { + let response = self + .client + .delete(&format!("{}/v1/subscribers/{}", self.config.api_url, id)) + .send() + .await?; + + let response = self.handle_response(response).await?; + let result: serde_json::Value = response.json().await?; + Ok(result["success"].as_bool().unwrap_or(false)) + } + + /// Get usage statistics for a subscriber + pub async fn get_usage_stats( + &self, + subscriber_id: i64, + start_date: DateTime, + end_date: DateTime, + ) -> Result { + let response = self + .client + .get(&format!("{}/v1/subscribers/{}/usage", self.config.api_url, subscriber_id)) + .query(&[ + ("start_date", start_date.to_rfc3339()), + ("end_date", end_date.to_rfc3339()), + ]) + .send() + .await?; + + let response = self.handle_response(response).await?; + Ok(response.json().await?) + } + + /// Create a payment transaction + pub async fn create_payment_transaction(&self, request: CreatePaymentRequest) -> Result { + let response = self + .client + .post(&format!("{}/v1/payments/transactions", self.config.api_url)) + .json(&request) + .send() + .await?; + + let response = self.handle_response(response).await?; + Ok(response.json().await?) + } + + /// Get system statistics + pub async fn get_system_stats(&self) -> Result { + let response = self + .client + .get(&format!("{}/v1/system/stats", self.config.api_url)) + .send() + .await?; + + let response = self.handle_response(response).await?; + Ok(response.json().await?) + } + + /// Get system health status + pub async fn get_health_status(&self) -> Result { + let response = self + .client + .get(&format!("{}/v1/health", self.config.api_url)) + .send() + .await?; + + let response = self.handle_response(response).await?; + Ok(response.json().await?) + } + + /// Execute a GraphQL query + pub async fn execute_graphql_query( + &self, + query: String, + variables: Option>, + ) -> Result { + let request = GraphQLRequest { query, variables }; + let response = self + .client + .post(&format!("{}/graphql", self.config.api_url)) + .json(&request) + .send() + .await?; + + let response = self.handle_response(response).await?; + Ok(response.json().await?) + } + + /// Connect to WebSocket for real-time updates + pub async fn connect_websocket(&self, message_handler: F) -> Result<(), TelecomError> + where + F: Fn(WebSocketMessage) + Send + Sync + 'static, + { + let ws_url = self.config.api_url.replace("http://", "ws://") + "/ws"; + let (ws_stream, _) = connect_async(&ws_url).await.map_err(|e| TelecomError::WebSocketError(e.to_string()))?; + + println!("WebSocket connected to {}", ws_url); + + let (mut write, mut read) = ws_stream.split(); + let handler = Arc::new(message_handler); + + // Spawn task to handle incoming messages + let handler_clone = handler.clone(); + let read_task = tokio::spawn(async move { + while let Some(msg) = read.next().await { + match msg { + Ok(tungstenite::Message::Text(text)) => { + if let Ok(ws_msg) = serde_json::from_str::(&text) { + handler_clone(ws_msg); + } + } + Ok(tungstenite::Message::Binary(data)) => { + if let Ok(text) = String::from_utf8(data) { + if let Ok(ws_msg) = serde_json::from_str::(&text) { + handler_clone(ws_msg); + } + } + } + Ok(tungstenite::Message::Close(_)) => { + println!("WebSocket connection closed"); + break; + } + Err(e) => { + eprintln!("WebSocket error: {}", e); + break; + } + _ => {} + } + } + }); + + // Return a handle that can be used to manage the connection + tokio::spawn(async move { + let _ = read_task.await; + }); + + Ok(()) + } +}