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
This commit is contained in:
nutcas3
2026-04-25 04:54:13 +03:00
parent f6590462f5
commit 808afbc2be
14 changed files with 1075 additions and 480 deletions
+3
View File
@@ -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"
+238
View File
@@ -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<dyn std::error::Error>> {
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: "[email protected]".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.
+23
View File
@@ -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<HashMap<String, serde_json::Value>>,
) -> Result<HashMap<String, serde_json::Value>, TelecomError> {
let req = GraphQLRequest { query, variables };
self.client.post("/graphql", Some(&req)).await
}
}
+13
View File
@@ -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;
+45
View File
@@ -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<PaymentTransaction, TelecomError> {
self.client.post("/v1/payments/transactions", Some(req)).await
}
pub async fn get_transaction(&self, transaction_id: &str) -> Result<PaymentTransaction, TelecomError> {
self.client
.get(&format!("/v1/payments/transactions/{}", transaction_id), None)
.await
}
pub async fn list_transactions(
&self,
subscriber_id: Option<i64>,
status: Option<String>,
page: i32,
page_size: i32,
) -> Result<SubscriberList, TelecomError> {
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(&params)).await
}
}
+23
View File
@@ -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<Vec<RatingPlan>, TelecomError> {
self.client.get("/v1/rating-plans", None).await
}
pub async fn get(&self, plan_id: &str) -> Result<RatingPlan, TelecomError> {
self.client
.get(&format!("/v1/rating-plans/{}", plan_id), None)
.await
}
}
+67
View File
@@ -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<Subscriber, TelecomError> {
self.client.get(&format!("/v1/subscribers/{}", id), None).await
}
pub async fn list(
&self,
page: i32,
page_size: i32,
status: Option<String>,
) -> Result<SubscriberList, TelecomError> {
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(&params)).await
}
pub async fn create(&self, req: &CreateSubscriberRequest) -> Result<Subscriber, TelecomError> {
self.client.post("/v1/subscribers", Some(req)).await
}
pub async fn update(
&self,
id: i64,
req: &UpdateSubscriberRequest,
) -> Result<Subscriber, TelecomError> {
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<Subscriber, TelecomError> {
self.client
.post(&format!("/v1/subscribers/{}/suspend", id), None::<()>)
.await
}
pub async fn activate(&self, id: i64) -> Result<Subscriber, TelecomError> {
self.client
.post(&format!("/v1/subscribers/{}/activate", id), None::<()>)
.await
}
}
+21
View File
@@ -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<SystemStats, TelecomError> {
self.client.get("/v1/system/stats", None).await
}
pub async fn get_health(&self) -> Result<HealthStatus, TelecomError> {
self.client.get("/v1/health", None).await
}
}
+65
View File
@@ -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<Utc>,
end_date: DateTime<Utc>,
) -> Result<UsageStats, TelecomError> {
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(&params))
.await
}
pub async fn list_events(
&self,
subscriber_id: Option<i64>,
usage_type: Option<String>,
start_date: Option<DateTime<Utc>>,
end_date: Option<DateTime<Utc>>,
page: i32,
page_size: i32,
) -> Result<UsageEventList, TelecomError> {
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(&params)).await
}
pub async fn get_realtime(&self, subscriber_id: i64) -> Result<RealTimeUsage, TelecomError> {
self.client
.get(&format!("/v1/subscribers/{}/realtime", subscriber_id), None)
.await
}
}
+143
View File
@@ -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<Sha256>;
/// Authentication provider for the Telecom SDK
#[derive(Clone)]
pub struct AuthProvider {
api_key: Option<String>,
jwt_secret: Option<String>,
token_cache: Option<String>,
token_expiry: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JWTClaims {
pub sub: String,
pub exp: i64,
pub iat: i64,
#[serde(flatten)]
pub additional: HashMap<String, serde_json::Value>,
}
impl AuthProvider {
pub fn new(api_key: Option<String>, jwt_secret: Option<String>) -> Self {
Self {
api_key,
jwt_secret,
token_cache: None,
token_expiry: None,
}
}
pub fn get_headers(&self) -> HashMap<String, String> {
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<String, serde_json::Value>,
) -> Result<String, String> {
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<JWTClaims, String> {
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<Vec<u8>, 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()))
}
}
+127
View File
@@ -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<T>(&self, path: &str, params: Option<&HashMap<String, String>>) -> Result<T, TelecomError>
where
T: serde::de::DeserializeOwned,
{
self.request("GET", path, None, params).await
}
pub async fn post<T>(&self, path: &str, body: Option<&impl serde::Serialize>) -> Result<T, TelecomError>
where
T: serde::de::DeserializeOwned,
{
self.request("POST", path, body, None).await
}
pub async fn put<T>(&self, path: &str, body: Option<&impl serde::Serialize>) -> Result<T, TelecomError>
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<T>(
&self,
method: &str,
path: &str,
body: Option<&impl serde::Serialize>,
params: Option<&HashMap<String, String>>,
) -> Result<T, TelecomError>
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::<T>().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
}
}
+22
View File
@@ -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),
}
+64 -480
View File
@@ -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<String>,
pub status: SubscriberStatus,
pub plan_id: i64,
pub balance: f64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SubscriberList {
pub subscribers: Vec<Subscriber>,
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<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct UpdateSubscriberRequest {
pub first_name: Option<String>,
pub last_name: Option<String>,
pub email: Option<String>,
pub plan_id: Option<i64>,
pub status: Option<SubscriberStatus>,
}
#[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<Utc>,
pub period_end: DateTime<Utc>,
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<Utc>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UsageEventList {
pub events: Vec<UsageEvent>,
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<Utc>,
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<CurrentSession>,
pub today_usage: Option<HashMap<String, i64>>,
}
#[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<String>,
pub created_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PaymentTransactionList {
pub transactions: Vec<PaymentTransaction>,
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<HashMap<String, serde_json::Value>>,
}
#[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<Utc>,
pub checks: HashMap<String, serde_json::Value>,
pub uptime: f64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WebSocketMessage {
pub r#type: String,
pub data: HashMap<String, serde_json::Value>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize)]
pub struct GraphQLRequest {
pub query: String,
pub variables: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GraphQLResponse {
pub data: Option<HashMap<String, serde_json::Value>>,
pub errors: Option<Vec<GraphQLError>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GraphQLError {
pub message: String,
pub locations: Option<Vec<GraphQLErrorLocation>>,
pub path: Option<Vec<String>>,
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
#[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<String>,
pub jwt_secret: Option<String>,
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<reqwest::Response, TelecomError> {
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<Subscriber, TelecomError> {
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<String, serde_json::Value>,
) -> Result<String, String> {
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<SubscriberStatus>,
) -> Result<SubscriberList, TelecomError> {
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<JWTClaims, String> {
self.auth_provider.validate_jwt_token(token)
}
/// Create a new subscriber
pub async fn create_subscriber(&self, request: CreateSubscriberRequest) -> Result<Subscriber, TelecomError> {
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<Subscriber, TelecomError> {
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<Subscriber, TelecomError> {
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<Subscriber, TelecomError> {
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<bool, TelecomError> {
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<Utc>,
end_date: DateTime<Utc>,
) -> Result<UsageStats, TelecomError> {
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<PaymentTransaction, TelecomError> {
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<SystemStats, TelecomError> {
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<HealthStatus, TelecomError> {
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<HashMap<String, serde_json::Value>>,
) -> Result<GraphQLResponse, TelecomError> {
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<F>(&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::<WebSocketMessage>(&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::<WebSocketMessage>(&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();
}
}
+221
View File
@@ -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<String>,
pub status: SubscriberStatus,
pub plan_id: i64,
pub balance: f64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SubscriberList {
pub subscribers: Vec<Subscriber>,
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<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct UpdateSubscriberRequest {
pub first_name: Option<String>,
pub last_name: Option<String>,
pub email: Option<String>,
pub plan_id: Option<i64>,
pub status: Option<SubscriberStatus>,
}
#[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<Utc>,
pub period_end: DateTime<Utc>,
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<Utc>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UsageEventList {
pub events: Vec<UsageEvent>,
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<Utc>,
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<CurrentSession>,
pub today_usage: Option<HashMap<String, i64>>,
}
#[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<String>,
pub created_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PaymentTransactionList {
pub transactions: Vec<PaymentTransaction>,
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<HashMap<String, serde_json::Value>>,
}
#[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<Utc>,
pub checks: HashMap<String, serde_json::Value>,
pub uptime: f64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WebSocketMessage {
pub r#type: String,
pub data: HashMap<String, serde_json::Value>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize)]
pub struct GraphQLRequest {
pub query: String,
pub variables: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GraphQLResponse {
pub data: Option<HashMap<String, serde_json::Value>>,
pub errors: Option<Vec<GraphQLError>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GraphQLError {
pub message: String,
pub locations: Option<Vec<GraphQLErrorLocation>>,
pub path: Option<Vec<String>>,
pub extensions: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GraphQLErrorLocation {
pub line: i32,
pub column: i32,
}