mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 11:02:54 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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(¶ms)).await
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms)).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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms))
|
||||
.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(¶ms)).await
|
||||
}
|
||||
|
||||
pub async fn get_realtime(&self, subscriber_id: i64) -> Result<RealTimeUsage, TelecomError> {
|
||||
self.client
|
||||
.get(&format!("/v1/subscribers/{}/realtime", subscriber_id), None)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user