From 808afbc2be87b28a4d6e54fc67ea4c94b3afd43e Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Sat, 25 Apr 2026 04:54:13 +0300 Subject: [PATCH] feat: Add comprehensive API client modules and JWT authentication to Rust SDK - Add hmac, sha2, and base64 dependencies to Cargo.toml for JWT support - Add README.md with installation instructions, architecture overview, and API examples for all SDK modules - Add AuthProvider class with API key and JWT token support (generate_jwt_token, validate_jwt_token, get_headers, clear_token_cache methods) - Add HTTPClient base class with retry logic, error handling, and timeout management - Add SubscriberAPI module --- sdk/rust/Cargo.toml | 3 + sdk/rust/README.md | 238 ++++++++++++++ sdk/rust/src/api/graphql.rs | 23 ++ sdk/rust/src/api/mod.rs | 13 + sdk/rust/src/api/payments.rs | 45 +++ sdk/rust/src/api/rating_plans.rs | 23 ++ sdk/rust/src/api/subscribers.rs | 67 ++++ sdk/rust/src/api/system.rs | 21 ++ sdk/rust/src/api/usage.rs | 65 ++++ sdk/rust/src/auth.rs | 143 ++++++++ sdk/rust/src/client.rs | 127 ++++++++ sdk/rust/src/error.rs | 22 ++ sdk/rust/src/lib.rs | 544 ++++--------------------------- sdk/rust/src/types.rs | 221 +++++++++++++ 14 files changed, 1075 insertions(+), 480 deletions(-) create mode 100644 sdk/rust/README.md create mode 100644 sdk/rust/src/api/graphql.rs create mode 100644 sdk/rust/src/api/mod.rs create mode 100644 sdk/rust/src/api/payments.rs create mode 100644 sdk/rust/src/api/rating_plans.rs create mode 100644 sdk/rust/src/api/subscribers.rs create mode 100644 sdk/rust/src/api/system.rs create mode 100644 sdk/rust/src/api/usage.rs create mode 100644 sdk/rust/src/auth.rs create mode 100644 sdk/rust/src/client.rs create mode 100644 sdk/rust/src/error.rs create mode 100644 sdk/rust/src/types.rs diff --git a/sdk/rust/Cargo.toml b/sdk/rust/Cargo.toml index 23878a6..9ff3a5e 100644 --- a/sdk/rust/Cargo.toml +++ b/sdk/rust/Cargo.toml @@ -18,6 +18,9 @@ uuid = { version = "1.11", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } thiserror = "2.0" url = "2.5" +hmac = "0.12" +sha2 = "0.10" +base64 = "0.22" [dev-dependencies] tokio-test = "0.4" diff --git a/sdk/rust/README.md b/sdk/rust/README.md new file mode 100644 index 0000000..d429fd7 --- /dev/null +++ b/sdk/rust/README.md @@ -0,0 +1,238 @@ +# Telecom Platform Rust SDK + +Async Rust SDK for the Telecom Platform with full type safety and modern async/await support. + +## Features + +- **Modular Architecture**: Separate modules for authentication, HTTP client, and API services +- **Async/Await Support**: Full async/await support with tokio +- **Type Safety**: Strong typing with Rust structs and enums +- **Error Handling**: Comprehensive error handling with custom error types +- **Retry Logic**: Built-in retry mechanism with exponential backoff +- **Context Support**: Full context.Context support for cancellation + +## Installation + +Add to your `Cargo.toml`: + +```toml +[dependencies] +telecom-sdk = { path = "../sdk/rust" } +``` + +Or install from source: + +```bash +cd sdk/rust +cargo build +``` + +## Architecture + +The SDK is organized into modular components: + +- `auth.rs` - Authentication provider handling API keys and JWT tokens +- `client.rs` - HTTP client with retry logic and authentication +- `error.rs` - Error types for the SDK +- `types.rs` - Data structures and type definitions +- `api/` - Directory containing API modules: + - `subscribers.rs` - Subscriber management API + - `usage.rs` - Usage tracking API + - `payments.rs` - Payment processing API + - `rating_plans.rs` - Rating plan management API + - `system.rs` - System monitoring API + - `graphql.rs` - GraphQL query execution + - `mod.rs` - Module exports + +## Quick Start + +```rust +use telecom_sdk::{TelecomClient, Config}; +use tokio; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let config = Config { + api_url: "http://localhost:8000".to_string(), + api_key: Some("your-api-key".to_string()), + ..Default::default() + }; + + let client = TelecomClient::new(config).await?; + + // Get subscriber + let subscriber = client.subscribers.get(1).await?; + println!("Subscriber: {} {}", subscriber.first_name, subscriber.last_name); + + // List subscribers + let list = client.subscribers.list(1, 50, None).await?; + println!("Total subscribers: {}", list.total); + + Ok(()) +} +``` + +## API Reference + +### Configuration + +```rust +let config = Config { + api_url: "http://localhost:8000".to_string(), + api_key: Some("your-api-key".to_string()), + jwt_secret: Some("your-jwt-secret".to_string()), + timeout: Some(Duration::from_secs(30)), + max_retries: Some(3), + retry_delay: Some(Duration::from_secs(1)), +}; +``` + +### Subscriber Management + +```rust +// Get subscriber +let subscriber = client.subscribers.get(1).await?; + +// List subscribers +let list = client.subscribers.list(1, 50, Some("active".to_string())).await?; + +// Create subscriber +let request = CreateSubscriberRequest { + imsi: "208930000000001".to_string(), + msisdn: "+1234567890".to_string(), + first_name: "John".to_string(), + last_name: "Doe".to_string(), + email: "john.doe@example.com".to_string(), + plan_id: 1, +}; +let subscriber = client.subscribers.create(request).await?; + +// Update subscriber +let update = UpdateSubscriberRequest { + first_name: Some("Jane".to_string()), + ..Default::default() +}; +let subscriber = client.subscribers.update(1, update).await?; + +// Delete subscriber +client.subscribers.delete(1).await?; + +// Suspend subscriber +client.subscribers.suspend(1).await?; + +// Activate subscriber +client.subscribers.activate(1).await?; +``` + +### Usage Management + +```rust +use std::time::{Duration, SystemTime}; + +// Get usage stats +let start_date = SystemTime::now() - Duration::from_secs(30 * 24 * 60 * 60); +let end_date = SystemTime::now(); +let stats = client.usage.get_stats(1, start_date, end_date).await?; + +// List usage events +let events = client.usage.list_events(1, Some("data".to_string()), 1, 50).await?; + +// Get real-time usage +let realtime = client.usage.get_real_time(1).await?; +``` + +### Payment Management + +```rust +// Create payment transaction +let transaction = client.payments.create_transaction(CreatePaymentTransactionRequest { + subscriber_id: "1".to_string(), + amount: 25.00, + currency: "USD".to_string(), + gateway: "stripe".to_string(), + metadata: None, +}).await?; + +// Get transaction +let transaction = client.payments.get_transaction("txn-123".to_string()).await?; + +// List transactions +let transactions = client.payments.list_transactions(1, Some("completed".to_string()), 1, 50).await?; +``` + +### Rating Plans + +```rust +// List rating plans +let plans = client.rating_plans.list().await?; + +// Get specific plan +let plan = client.rating_plans.get("plan-123".to_string()).await?; +``` + +### System Management + +```rust +// Get system stats +let stats = client.system.get_stats().await?; + +// Get health status +let health = client.system.get_health().await?; +``` + +### GraphQL + +```rust +// Execute GraphQL query +let query = r#" + query GetSubscribers($first: Int) { + subscribers(first: $first) { + id + firstName + lastName + } + } +"#; + +let variables = serde_json::json!({"first": 10}); +let result = client.graphql.execute(query, Some(variables)).await?; +``` + +## Error Handling + +```rust +use telecom_sdk::TelecomError; + +match client.subscribers.get(1).await { + Ok(subscriber) => println!("Subscriber: {}", subscriber.first_name), + Err(TelecomError::AuthenticationError) => println!("Authentication failed"), + Err(TelecomError::APIError(msg)) => println!("API error: {}", msg), + Err(TelecomError::NetworkError(msg)) => println!("Network error: {}", msg), + Err(e) => println!("Error: {}", e), +} +``` + +## Development + +### Running Tests + +```bash +cargo test +``` + +### Building + +```bash +cargo build --release +``` + +### Code Style + +```bash +cargo fmt +cargo clippy +``` + +## License + +This SDK is licensed under the MIT License. diff --git a/sdk/rust/src/api/graphql.rs b/sdk/rust/src/api/graphql.rs new file mode 100644 index 0000000..ad251d2 --- /dev/null +++ b/sdk/rust/src/api/graphql.rs @@ -0,0 +1,23 @@ +use crate::client::HTTPClient; +use crate::error::TelecomError; +use crate::types::GraphQLRequest; +use std::collections::HashMap; + +pub struct GraphQLAPI { + client: HTTPClient, +} + +impl GraphQLAPI { + pub fn new(client: HTTPClient) -> Self { + Self { client } + } + + pub async fn execute( + &self, + query: String, + variables: Option>, + ) -> Result, TelecomError> { + let req = GraphQLRequest { query, variables }; + self.client.post("/graphql", Some(&req)).await + } +} diff --git a/sdk/rust/src/api/mod.rs b/sdk/rust/src/api/mod.rs new file mode 100644 index 0000000..7221730 --- /dev/null +++ b/sdk/rust/src/api/mod.rs @@ -0,0 +1,13 @@ +pub mod graphql; +pub mod payments; +pub mod rating_plans; +pub mod subscribers; +pub mod system; +pub mod usage; + +pub use graphql::GraphQLAPI; +pub use payments::PaymentAPI; +pub use rating_plans::RatingPlanAPI; +pub use subscribers::SubscriberAPI; +pub use system::SystemAPI; +pub use usage::UsageAPI; diff --git a/sdk/rust/src/api/payments.rs b/sdk/rust/src/api/payments.rs new file mode 100644 index 0000000..854f153 --- /dev/null +++ b/sdk/rust/src/api/payments.rs @@ -0,0 +1,45 @@ +use crate::client::HTTPClient; +use crate::error::TelecomError; +use crate::types::{CreatePaymentRequest, PaymentTransaction, SubscriberList}; +use std::collections::HashMap; + +pub struct PaymentAPI { + client: HTTPClient, +} + +impl PaymentAPI { + pub fn new(client: HTTPClient) -> Self { + Self { client } + } + + pub async fn create_transaction(&self, req: &CreatePaymentRequest) -> Result { + self.client.post("/v1/payments/transactions", Some(req)).await + } + + pub async fn get_transaction(&self, transaction_id: &str) -> Result { + self.client + .get(&format!("/v1/payments/transactions/{}", transaction_id), None) + .await + } + + pub async fn list_transactions( + &self, + subscriber_id: Option, + status: Option, + page: i32, + page_size: i32, + ) -> Result { + let mut params = HashMap::new(); + params.insert("page".to_string(), page.to_string()); + params.insert("page_size".to_string(), page_size.to_string()); + + if let Some(subscriber_id) = subscriber_id { + params.insert("subscriber_id".to_string(), subscriber_id.to_string()); + } + if let Some(status) = status { + params.insert("status".to_string(), status); + } + + self.client.get("/v1/payments/transactions", Some(¶ms)).await + } +} diff --git a/sdk/rust/src/api/rating_plans.rs b/sdk/rust/src/api/rating_plans.rs new file mode 100644 index 0000000..ee0d3e6 --- /dev/null +++ b/sdk/rust/src/api/rating_plans.rs @@ -0,0 +1,23 @@ +use crate::client::HTTPClient; +use crate::error::TelecomError; +use crate::types::RatingPlan; + +pub struct RatingPlanAPI { + client: HTTPClient, +} + +impl RatingPlanAPI { + pub fn new(client: HTTPClient) -> Self { + Self { client } + } + + pub async fn list(&self) -> Result, TelecomError> { + self.client.get("/v1/rating-plans", None).await + } + + pub async fn get(&self, plan_id: &str) -> Result { + self.client + .get(&format!("/v1/rating-plans/{}", plan_id), None) + .await + } +} diff --git a/sdk/rust/src/api/subscribers.rs b/sdk/rust/src/api/subscribers.rs new file mode 100644 index 0000000..031973d --- /dev/null +++ b/sdk/rust/src/api/subscribers.rs @@ -0,0 +1,67 @@ +use crate::client::HTTPClient; +use crate::error::TelecomError; +use crate::types::{ + CreateSubscriberRequest, Subscriber, SubscriberList, UpdateSubscriberRequest, +}; +use std::collections::HashMap; + +pub struct SubscriberAPI { + client: HTTPClient, +} + +impl SubscriberAPI { + pub fn new(client: HTTPClient) -> Self { + Self { client } + } + + pub async fn get(&self, id: i64) -> Result { + self.client.get(&format!("/v1/subscribers/{}", id), None).await + } + + pub async fn list( + &self, + page: i32, + page_size: i32, + status: Option, + ) -> Result { + let mut params = HashMap::new(); + params.insert("page".to_string(), page.to_string()); + params.insert("page_size".to_string(), page_size.to_string()); + + if let Some(status) = status { + params.insert("status".to_string(), status); + } + + self.client.get("/v1/subscribers", Some(¶ms)).await + } + + pub async fn create(&self, req: &CreateSubscriberRequest) -> Result { + self.client.post("/v1/subscribers", Some(req)).await + } + + pub async fn update( + &self, + id: i64, + req: &UpdateSubscriberRequest, + ) -> Result { + self.client + .put(&format!("/v1/subscribers/{}", id), Some(req)) + .await + } + + pub async fn delete(&self, id: i64) -> Result<(), TelecomError> { + self.client.delete(&format!("/v1/subscribers/{}", id)).await + } + + pub async fn suspend(&self, id: i64) -> Result { + self.client + .post(&format!("/v1/subscribers/{}/suspend", id), None::<()>) + .await + } + + pub async fn activate(&self, id: i64) -> Result { + self.client + .post(&format!("/v1/subscribers/{}/activate", id), None::<()>) + .await + } +} diff --git a/sdk/rust/src/api/system.rs b/sdk/rust/src/api/system.rs new file mode 100644 index 0000000..8e48775 --- /dev/null +++ b/sdk/rust/src/api/system.rs @@ -0,0 +1,21 @@ +use crate::client::HTTPClient; +use crate::error::TelecomError; +use crate::types::{HealthStatus, SystemStats}; + +pub struct SystemAPI { + client: HTTPClient, +} + +impl SystemAPI { + pub fn new(client: HTTPClient) -> Self { + Self { client } + } + + pub async fn get_stats(&self) -> Result { + self.client.get("/v1/system/stats", None).await + } + + pub async fn get_health(&self) -> Result { + self.client.get("/v1/health", None).await + } +} diff --git a/sdk/rust/src/api/usage.rs b/sdk/rust/src/api/usage.rs new file mode 100644 index 0000000..6a2719b --- /dev/null +++ b/sdk/rust/src/api/usage.rs @@ -0,0 +1,65 @@ +use crate::client::HTTPClient; +use crate::error::TelecomError; +use crate::types::{RealTimeUsage, UsageEventList, UsageStats}; +use chrono::{DateTime, Utc}; +use std::collections::HashMap; + +pub struct UsageAPI { + client: HTTPClient, +} + +impl UsageAPI { + pub fn new(client: HTTPClient) -> Self { + Self { client } + } + + pub async fn get_stats( + &self, + subscriber_id: i64, + start_date: DateTime, + end_date: DateTime, + ) -> Result { + let mut params = HashMap::new(); + params.insert("start_date".to_string(), start_date.to_rfc3339()); + params.insert("end_date".to_string(), end_date.to_rfc3339()); + + self.client + .get(&format!("/v1/subscribers/{}/usage", subscriber_id), Some(¶ms)) + .await + } + + pub async fn list_events( + &self, + subscriber_id: Option, + usage_type: Option, + start_date: Option>, + end_date: Option>, + page: i32, + page_size: i32, + ) -> Result { + let mut params = HashMap::new(); + params.insert("page".to_string(), page.to_string()); + params.insert("page_size".to_string(), page_size.to_string()); + + if let Some(subscriber_id) = subscriber_id { + params.insert("subscriber_id".to_string(), subscriber_id.to_string()); + } + if let Some(usage_type) = usage_type { + params.insert("usage_type".to_string(), usage_type); + } + if let Some(start_date) = start_date { + params.insert("start_date".to_string(), start_date.to_rfc3339()); + } + if let Some(end_date) = end_date { + params.insert("end_date".to_string(), end_date.to_rfc3339()); + } + + self.client.get("/v1/usage/events", Some(¶ms)).await + } + + pub async fn get_realtime(&self, subscriber_id: i64) -> Result { + self.client + .get(&format!("/v1/subscribers/{}/realtime", subscriber_id), None) + .await + } +} diff --git a/sdk/rust/src/auth.rs b/sdk/rust/src/auth.rs new file mode 100644 index 0000000..2cabe63 --- /dev/null +++ b/sdk/rust/src/auth.rs @@ -0,0 +1,143 @@ +use chrono::{DateTime, Utc}; +use hmac::{Hmac, Mac}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use std::collections::HashMap; + +type HmacSha256 = Hmac; + +/// Authentication provider for the Telecom SDK +#[derive(Clone)] +pub struct AuthProvider { + api_key: Option, + jwt_secret: Option, + token_cache: Option, + token_expiry: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JWTClaims { + pub sub: String, + pub exp: i64, + pub iat: i64, + #[serde(flatten)] + pub additional: HashMap, +} + +impl AuthProvider { + pub fn new(api_key: Option, jwt_secret: Option) -> Self { + Self { + api_key, + jwt_secret, + token_cache: None, + token_expiry: None, + } + } + + pub fn get_headers(&self) -> HashMap { + let mut headers = HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert("User-Agent".to_string(), "Telecom-Rust-SDK/1.0.0".to_string()); + + if let Some(ref api_key) = self.api_key { + headers.insert("X-API-Key".to_string(), api_key.clone()); + } + + if let Some(ref token) = self.token_cache { + if self.is_token_valid() { + headers.insert("Authorization".to_string(), format!("Bearer {}", token)); + } + } + + headers + } + + pub fn generate_jwt_token( + &mut self, + user_id: String, + expiry_hours: i64, + additional_claims: HashMap, + ) -> Result { + let jwt_secret = self.jwt_secret.as_ref().ok_or("JWT secret not configured")?; + + let now = Utc::now().timestamp(); + let exp = now + (expiry_hours * 3600); + + let mut claims = HashMap::new(); + claims.insert("sub".to_string(), serde_json::to_value(&user_id).unwrap()); + claims.insert("exp".to_string(), serde_json::to_value(&exp).unwrap()); + claims.insert("iat".to_string(), serde_json::to_value(&now).unwrap()); + + for (k, v) in additional_claims { + claims.insert(k, v); + } + + let header = json!({"alg": "HS256", "typ": "JWT"}); + let encoded_header = self.base64_url_encode(&header.to_string()); + let encoded_payload = self.base64_url_encode(&json!(claims).to_string()); + let signature = self.sign(&format!("{}.{}", encoded_header, encoded_payload), jwt_secret); + + let token = format!("{}.{}.{}", encoded_header, encoded_payload, signature); + self.token_cache = Some(token.clone()); + self.token_expiry = Some(DateTime::from_timestamp(exp, 0).unwrap()); + + Ok(token) + } + + pub fn validate_jwt_token(&self, token: &str) -> Result { + let jwt_secret = self.jwt_secret.as_ref().ok_or("JWT secret not configured")?; + + let parts: Vec<&str> = token.split('.').collect(); + if parts.len() != 3 { + return Err("Invalid token format".to_string()); + } + + let (encoded_header, encoded_payload, signature) = (parts[0], parts[1], parts[2]); + let expected_signature = self.sign(&format!("{}.{}", encoded_header, encoded_payload), jwt_secret); + + if signature != expected_signature { + return Err("Invalid token signature".to_string()); + } + + let payload_bytes = self.base64_url_decode(encoded_payload)?; + let claims: JWTClaims = serde_json::from_slice(&payload_bytes) + .map_err(|e| format!("Failed to decode payload: {}", e))?; + + if claims.exp < Utc::now().timestamp() { + return Err("Token has expired".to_string()); + } + + Ok(claims) + } + + pub fn clear_token_cache(&mut self) { + self.token_cache = None; + self.token_expiry = None; + } + + fn is_token_valid(&self) -> bool { + if self.token_cache.is_none() || self.token_expiry.is_none() { + return false; + } + Utc::now() < self.token_expiry.unwrap() + } + + fn base64_url_encode(&self, data: &str) -> String { + use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; + URL_SAFE_NO_PAD.encode(data.as_bytes()) + } + + fn base64_url_decode(&self, data: &str) -> Result, String> { + use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; + URL_SAFE_NO_PAD + .decode(data) + .map_err(|e| format!("Failed to decode: {}", e)) + } + + fn sign(&self, data: &str, secret: &str) -> String { + let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size"); + mac.update(data.as_bytes()); + let result = mac.finalize(); + self.base64_url_encode(&format!("{:x}", result.into_bytes())) + } +} diff --git a/sdk/rust/src/client.rs b/sdk/rust/src/client.rs new file mode 100644 index 0000000..84114d2 --- /dev/null +++ b/sdk/rust/src/client.rs @@ -0,0 +1,127 @@ +use crate::auth::AuthProvider; +use crate::error::TelecomError; +use reqwest::{Client, StatusCode}; +use std::collections::HashMap; +use std::time::Duration; + +/// HTTP client for making API requests +pub struct HTTPClient { + client: Client, + auth_provider: AuthProvider, + base_url: String, + max_retries: u32, + retry_delay: Duration, +} + +impl HTTPClient { + pub fn new( + base_url: String, + auth_provider: AuthProvider, + timeout: Duration, + max_retries: u32, + retry_delay: Duration, + ) -> Self { + let client = Client::builder() + .timeout(timeout) + .build() + .expect("Failed to create HTTP client"); + + Self { + client, + auth_provider, + base_url, + max_retries, + retry_delay, + } + } + + pub async fn get(&self, path: &str, params: Option<&HashMap>) -> Result + where + T: serde::de::DeserializeOwned, + { + self.request("GET", path, None, params).await + } + + pub async fn post(&self, path: &str, body: Option<&impl serde::Serialize>) -> Result + where + T: serde::de::DeserializeOwned, + { + self.request("POST", path, body, None).await + } + + pub async fn put(&self, path: &str, body: Option<&impl serde::Serialize>) -> Result + where + T: serde::de::DeserializeOwned, + { + self.request("PUT", path, body, None).await + } + + pub async fn delete(&self, path: &str) -> Result<(), TelecomError> { + self.request::<()>("DELETE", path, None, None).await + } + + async fn request( + &self, + method: &str, + path: &str, + body: Option<&impl serde::Serialize>, + params: Option<&HashMap>, + ) -> Result + where + T: serde::de::DeserializeOwned, + { + let url = format!("{}{}", self.base_url, path); + let mut request = self.client.request(method, &url); + + // Set headers + let headers = self.auth_provider.get_headers(); + for (key, value) in headers { + request = request.header(&key, &value); + } + + // Add query parameters + if let Some(params) = params { + request = request.query(params); + } + + // Add body + if let Some(body) = body { + request = request.json(body); + } + + // Retry logic + let mut last_error = None; + for attempt in 0..=self.max_retries { + match request.try_clone().unwrap().send().await { + Ok(response) => { + let status = response.status(); + + if status == StatusCode::UNAUTHORIZED { + return Err(TelecomError::AuthenticationError); + } else if status == StatusCode::TOO_MANY_REQUESTS { + return Err(TelecomError::RateLimitError); + } else if status.is_client_error() { + let error_text = response.text().await.unwrap_or_else(|_| "Bad request".to_string()); + return Err(TelecomError::APIError(error_text)); + } else if status.is_server_error() { + return Err(TelecomError::ServerError(status)); + } + + return response.json::().await.map_err(TelecomError::from); + } + Err(e) => { + last_error = Some(TelecomError::NetworkError(e)); + if attempt < self.max_retries { + tokio::time::sleep(self.retry_delay * (1 << attempt)).await; + } + } + } + } + + Err(last_error.unwrap_or_else(|| TelecomError::APIError("Request failed".to_string()))) + } + + pub fn close(&self) { + // HTTP client doesn't need explicit closing + } +} diff --git a/sdk/rust/src/error.rs b/sdk/rust/src/error.rs new file mode 100644 index 0000000..f01cbe7 --- /dev/null +++ b/sdk/rust/src/error.rs @@ -0,0 +1,22 @@ +use reqwest::StatusCode; +use thiserror::Error; + +#[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), +} diff --git a/sdk/rust/src/lib.rs b/sdk/rust/src/lib.rs index d9e0a4b..ec56f49 100644 --- a/sdk/rust/src/lib.rs +++ b/sdk/rust/src/lib.rs @@ -2,267 +2,43 @@ //! //! 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; +pub mod api; +pub mod auth; +pub mod client; +pub mod error; +pub mod types; + +use auth::AuthProvider; +use client::HTTPClient; +use error::TelecomError; use std::time::Duration; -use thiserror::Error; -use tokio_tungstenite::{connect_async, tungstenite::Message}; -use url::Url; -use uuid::Uuid; +use types::*; -#[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, -} +pub use api::{GraphQLAPI, PaymentAPI, RatingPlanAPI, SubscriberAPI, SystemAPI, UsageAPI}; /// Telecom Platform SDK client #[derive(Clone)] pub struct TelecomClient { - client: Client, - config: TelecomConfig, + auth_provider: AuthProvider, + http_client: HTTPClient, + + // API modules + pub subscribers: SubscriberAPI, + pub usage: UsageAPI, + pub payments: PaymentAPI, + pub rating_plans: RatingPlanAPI, + pub system: SystemAPI, + pub graphql: GraphQLAPI, } #[derive(Debug, Clone)] pub struct TelecomConfig { pub api_url: String, pub api_key: Option, + pub jwt_secret: Option, pub timeout: Duration, pub max_retries: u32, + pub retry_delay: Duration, } impl Default for TelecomConfig { @@ -270,8 +46,10 @@ impl Default for TelecomConfig { Self { api_url: "http://localhost:8000".to_string(), api_key: None, + jwt_secret: None, timeout: Duration::from_secs(30), max_retries: 3, + retry_delay: Duration::from_secs(1), } } } @@ -279,247 +57,53 @@ impl Default for TelecomConfig { 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"); + let auth_provider = AuthProvider::new(config.api_key.clone(), config.jwt_secret.clone()); + + let http_client = HTTPClient::new( + config.api_url.clone(), + auth_provider.clone(), + config.timeout, + config.max_retries, + config.retry_delay, + ); - Self { client, config } - } + // Initialize API modules + let subscribers = SubscriberAPI::new(http_client.clone()); + let usage = UsageAPI::new(http_client.clone()); + let payments = PaymentAPI::new(http_client.clone()); + let rating_plans = RatingPlanAPI::new(http_client.clone()); + let system = SystemAPI::new(http_client.clone()); + let graphql = GraphQLAPI::new(http_client.clone()); - 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), + Self { + auth_provider, + http_client, + subscribers, + usage, + payments, + rating_plans, + system, + graphql, } } - /// 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?) + /// Generate a JWT token for authentication + pub fn generate_jwt_token( + &mut self, + user_id: String, + expiry_hours: i64, + additional_claims: std::collections::HashMap, + ) -> Result { + self.auth_provider.generate_jwt_token(user_id, expiry_hours, additional_claims) } - /// 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?) + /// Validate a JWT token + pub fn validate_jwt_token(&self, token: &str) -> Result { + self.auth_provider.validate_jwt_token(token) } - /// 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(()) + /// Close the client and cleanup resources + pub fn close(&self) { + self.http_client.close(); } } diff --git a/sdk/rust/src/types.rs b/sdk/rust/src/types.rs new file mode 100644 index 0000000..79ee8ea --- /dev/null +++ b/sdk/rust/src/types.rs @@ -0,0 +1,221 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[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, +}