mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-20 11:54:25 +00:00
feat: Add Rust SDK with tokio async/await support and comprehensive Telecom Platform API integration
- Add Cargo.toml with tokio 1.52.1 (full features), reqwest 0.13.2 (json), tokio-tungstenite 0.29.0, serde 1.0.228, chrono 0.4.44 - Add TelecomClient with reqwest Client and TelecomConfig with 30s timeout, 3 retries - Add subscriber management: get_subscriber, list_subscribers with pagination/status filter, create_subscriber, update_subscriber, suspend_subscriber, activate_subscriber, terminate_subscriber
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "telecom-sdk"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
authors = ["nutcas3 <[email protected]>"]
|
||||
description = "Rust SDK for Telecom Platform"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/nutcas3/telecom-platform"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.52.1", features = ["full"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
reqwest = { version = "0.13.2", features = ["json"] }
|
||||
tokio-tungstenite = "0.29.0"
|
||||
futures-util = "0.3.32"
|
||||
uuid = { version = "1.23.1", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4.44", features = ["serde"] }
|
||||
thiserror = "2.0.18"
|
||||
url = "2.5.8"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.5"
|
||||
@@ -0,0 +1,525 @@
|
||||
//! Telecom Platform Rust SDK
|
||||
//!
|
||||
//! Provides async/await support with tokio for the Telecom Platform API.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use reqwest::{Client, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TelecomError {
|
||||
#[error("Authentication failed")]
|
||||
AuthenticationError,
|
||||
#[error("API error: {0}")]
|
||||
APIError(String),
|
||||
#[error("Network error: {0}")]
|
||||
NetworkError(#[from] reqwest::Error),
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(String),
|
||||
#[error("Rate limit exceeded")]
|
||||
RateLimitError,
|
||||
#[error("Server error: {0}")]
|
||||
ServerError(StatusCode),
|
||||
#[error("WebSocket error: {0}")]
|
||||
WebSocketError(String),
|
||||
#[error("JSON error: {0}")]
|
||||
JsonError(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SubscriberStatus {
|
||||
Active,
|
||||
Suspended,
|
||||
Terminated,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UsageType {
|
||||
Data,
|
||||
Voice,
|
||||
Sms,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PaymentStatus {
|
||||
Pending,
|
||||
Completed,
|
||||
Failed,
|
||||
Refunded,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Subscriber {
|
||||
pub id: i64,
|
||||
pub imsi: String,
|
||||
pub msisdn: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub email: String,
|
||||
pub organization_id: Option<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,
|
||||
}
|
||||
|
||||
/// Telecom Platform SDK client
|
||||
#[derive(Clone)]
|
||||
pub struct TelecomClient {
|
||||
client: Client,
|
||||
config: TelecomConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TelecomConfig {
|
||||
pub api_url: String,
|
||||
pub api_key: Option<String>,
|
||||
pub timeout: Duration,
|
||||
pub max_retries: u32,
|
||||
}
|
||||
|
||||
impl Default for TelecomConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
api_url: "http://localhost:8000".to_string(),
|
||||
api_key: None,
|
||||
timeout: Duration::from_secs(30),
|
||||
max_retries: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TelecomClient {
|
||||
/// Create a new Telecom client
|
||||
pub fn new(config: TelecomConfig) -> Self {
|
||||
let client = Client::builder()
|
||||
.timeout(config.timeout)
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self { client, config }
|
||||
}
|
||||
|
||||
async fn handle_response(&self, response: reqwest::Response) -> Result<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),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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?)
|
||||
}
|
||||
|
||||
/// 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?)
|
||||
}
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user