mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 11:02:54 +00:00
refactor: Split error handling into separate modules for better code organization
- Move ChargingError enum and IntoResponse implementation to error_types.rs - Move ChargingResult, ErrorContext trait, validation functions, error conversions, and logging to error_helpers.rs - Keep module declarations and re-exports in errors.rs - Add validate_imsi, validate_session_id, and validate_amount validation helpers - Add log_error helper for structured error logging with tracing - Update error variants:
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
use anyhow::Context;
|
||||
|
||||
pub type ChargingResult<T> = Result<T, ChargingError>;
|
||||
|
||||
pub trait ErrorContext<T> {
|
||||
fn with_context(self, msg: &str) -> ChargingResult<T>;
|
||||
}
|
||||
|
||||
impl<T, E> ErrorContext<T> for Result<T, E>
|
||||
where
|
||||
E: Into<ChargingError>,
|
||||
{
|
||||
fn with_context(self, msg: &str) -> ChargingResult<T> {
|
||||
self.map_err(|e| {
|
||||
let charging_err = e.into();
|
||||
match charging_err {
|
||||
ChargingError::InternalError(internal_msg) => {
|
||||
ChargingError::InternalError(format!("{}: {}", msg, internal_msg))
|
||||
}
|
||||
_ => charging_err,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validation helper functions
|
||||
pub fn validate_ip(ip: &str) -> ChargingResult<()> {
|
||||
if ip.is_empty() {
|
||||
return Err(ChargingError::InvalidInput("IP address cannot be empty".to_string()));
|
||||
}
|
||||
|
||||
// Basic IP validation (simplified)
|
||||
let parts: Vec<&str> = ip.split('.').collect();
|
||||
if parts.len() != 4 {
|
||||
return Err(ChargingError::InvalidInput("Invalid IP address format".to_string()));
|
||||
}
|
||||
|
||||
for part in parts {
|
||||
if let Ok(num) = part.parse::<u8>() {
|
||||
if num == 0 {
|
||||
return Err(ChargingError::InvalidInput("IP address cannot be 0.0.0.0".to_string()));
|
||||
}
|
||||
} else {
|
||||
return Err(ChargingError::InvalidInput("Invalid IP address octet".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_bytes(bytes: u64) -> ChargingResult<()> {
|
||||
if bytes == 0 {
|
||||
return Err(ChargingError::InvalidInput("Bytes cannot be zero".to_string()));
|
||||
}
|
||||
|
||||
if bytes > 1_000_000_000_000 { // 1TB limit
|
||||
return Err(ChargingError::InvalidInput("Bytes exceed maximum limit".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_imsi(imsi: &str) -> ChargingResult<()> {
|
||||
if imsi.is_empty() {
|
||||
return Err(ChargingError::InvalidInput("IMSI cannot be empty".to_string()));
|
||||
}
|
||||
|
||||
if imsi.len() < 15 || imsi.len() > 15 {
|
||||
return Err(ChargingError::InvalidInput("IMSI must be exactly 15 digits".to_string()));
|
||||
}
|
||||
|
||||
if !imsi.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Err(ChargingError::InvalidInput("IMSI must contain only digits".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_session_id(session_id: &str) -> ChargingResult<()> {
|
||||
if session_id.is_empty() {
|
||||
return Err(ChargingError::InvalidInput("Session ID cannot be empty".to_string()));
|
||||
}
|
||||
|
||||
if session_id.len() > 64 {
|
||||
return Err(ChargingError::InvalidInput("Session ID too long".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_amount(amount: f64) -> ChargingResult<()> {
|
||||
if amount <= 0.0 {
|
||||
return Err(ChargingError::InvalidInput("Amount must be positive".to_string()));
|
||||
}
|
||||
|
||||
if amount > 10_000.0 {
|
||||
return Err(ChargingError::InvalidInput("Amount exceeds maximum limit".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Error conversion helpers
|
||||
impl From<redis::RedisError> for ChargingError {
|
||||
fn from(err: redis::RedisError) -> Self {
|
||||
ChargingError::RedisConnection(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ChargingError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ChargingError::SerializationError(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::ParseIntError> for ChargingError {
|
||||
fn from(err: std::num::ParseIntError) -> Self {
|
||||
ChargingError::InvalidInput(format!("Parse error: {}", err))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::ParseFloatError> for ChargingError {
|
||||
fn from(err: std::num::ParseFloatError) -> Self {
|
||||
ChargingError::InvalidInput(format!("Parse error: {}", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Error logging helpers
|
||||
pub fn log_error(error: &ChargingError) {
|
||||
match error {
|
||||
ChargingError::RedisConnection(msg) => {
|
||||
tracing::error!("Redis connection error: {}", msg);
|
||||
}
|
||||
ChargingError::RedisOperation(msg) => {
|
||||
tracing::error!("Redis operation error: {}", msg);
|
||||
}
|
||||
ChargingError::SubscriberNotFound(imsi) => {
|
||||
tracing::warn!("Subscriber not found: {}", imsi);
|
||||
}
|
||||
ChargingError::RatingPlanNotFound(plan_id) => {
|
||||
tracing::warn!("Rating plan not found: {}", plan_id);
|
||||
}
|
||||
ChargingError::InsufficientCredit { available, requested } => {
|
||||
tracing::warn!("Insufficient credit: available={}, requested={}", available, requested);
|
||||
}
|
||||
ChargingError::UsageBlocked(reason) => {
|
||||
tracing::warn!("Usage blocked: {}", reason);
|
||||
}
|
||||
ChargingError::InvalidInput(msg) => {
|
||||
tracing::warn!("Invalid input: {}", msg);
|
||||
}
|
||||
ChargingError::SerializationError(msg) => {
|
||||
tracing::error!("Serialization error: {}", msg);
|
||||
}
|
||||
ChargingError::ConfigurationError(msg) => {
|
||||
tracing::error!("Configuration error: {}", msg);
|
||||
}
|
||||
ChargingError::InternalError(msg) => {
|
||||
tracing::error!("Internal error: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::http::StatusCode;
|
||||
use serde_json::json;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChargingError {
|
||||
RedisConnection(String),
|
||||
RedisOperation(String),
|
||||
SubscriberNotFound(String),
|
||||
RatingPlanNotFound(String),
|
||||
InsufficientCredit { available: u64, requested: u64 },
|
||||
UsageBlocked(String),
|
||||
InvalidInput(String),
|
||||
SerializationError(String),
|
||||
ConfigurationError(String),
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for ChargingError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ChargingError::RedisConnection(msg) => write!(f, "Redis connection error: {}", msg),
|
||||
ChargingError::RedisOperation(msg) => write!(f, "Redis operation error: {}", msg),
|
||||
ChargingError::SubscriberNotFound(imsi) => write!(f, "Subscriber not found: {}", imsi),
|
||||
ChargingError::RatingPlanNotFound(plan_id) => write!(f, "Rating plan not found: {}", plan_id),
|
||||
ChargingError::InsufficientCredit { available, requested } => {
|
||||
write!(f, "Insufficient credit: available={}, requested={}", available, requested)
|
||||
}
|
||||
ChargingError::UsageBlocked(reason) => write!(f, "Usage blocked: {}", reason),
|
||||
ChargingError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
|
||||
ChargingError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
|
||||
ChargingError::ConfigurationError(msg) => write!(f, "Configuration error: {}", msg),
|
||||
ChargingError::InternalError(msg) => write!(f, "Internal error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ChargingError {}
|
||||
|
||||
impl IntoResponse for ChargingError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error_message) = match self {
|
||||
ChargingError::RedisConnection(_) => (StatusCode::SERVICE_UNAVAILABLE, "Redis connection error"),
|
||||
ChargingError::RedisOperation(_) => (StatusCode::SERVICE_UNAVAILABLE, "Redis operation error"),
|
||||
ChargingError::SubscriberNotFound(_) => (StatusCode::NOT_FOUND, "Subscriber not found"),
|
||||
ChargingError::RatingPlanNotFound(_) => (StatusCode::NOT_FOUND, "Rating plan not found"),
|
||||
ChargingError::InsufficientCredit { .. } => (StatusCode::PAYMENT_REQUIRED, "Insufficient credit"),
|
||||
ChargingError::UsageBlocked(_) => (StatusCode::FORBIDDEN, "Usage blocked"),
|
||||
ChargingError::InvalidInput(_) => (StatusCode::BAD_REQUEST, "Invalid input"),
|
||||
ChargingError::SerializationError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Serialization error"),
|
||||
ChargingError::ConfigurationError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Configuration error"),
|
||||
ChargingError::InternalError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error"),
|
||||
};
|
||||
|
||||
let body = json!({
|
||||
"error": error_message,
|
||||
"message": self.to_string(),
|
||||
"code": status.as_u16()
|
||||
});
|
||||
|
||||
(status, axum::Json(body)).into_response()
|
||||
}
|
||||
}
|
||||
@@ -1,235 +1,5 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
pub mod error_types;
|
||||
pub mod error_helpers;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ChargingError {
|
||||
#[error("Redis connection failed: {0}")]
|
||||
RedisConnection(String),
|
||||
|
||||
#[error("Redis operation failed: {0}")]
|
||||
RedisOperation(String),
|
||||
|
||||
#[error("Account not found: {0}")]
|
||||
AccountNotFound(String),
|
||||
|
||||
#[error("Insufficient credit: available={available}, requested={requested}")]
|
||||
InsufficientCredit { available: i64, requested: u64 },
|
||||
|
||||
#[error("Invalid request: {0}")]
|
||||
InvalidRequest(String),
|
||||
|
||||
#[error("Rating plan not found: {0}")]
|
||||
RatingPlanNotFound(String),
|
||||
|
||||
#[error("Account operation failed: {0}")]
|
||||
AccountOperation(String),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
#[error("Internal server error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
impl IntoResponse for ChargingError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, error_message) = match self {
|
||||
ChargingError::RedisConnection(ref msg) => {
|
||||
error!("Redis connection error: {}", msg);
|
||||
(StatusCode::SERVICE_UNAVAILABLE, "Redis connection failed")
|
||||
}
|
||||
ChargingError::RedisOperation(ref msg) => {
|
||||
error!("Redis operation error: {}", msg);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Redis operation failed")
|
||||
}
|
||||
ChargingError::AccountNotFound(ref imsi) => {
|
||||
(StatusCode::NOT_FOUND, "Account not found")
|
||||
}
|
||||
ChargingError::InsufficientCredit { available, requested } => {
|
||||
(StatusCode::PAYMENT_REQUIRED, "Insufficient credit")
|
||||
}
|
||||
ChargingError::InvalidRequest(ref msg) => {
|
||||
(StatusCode::BAD_REQUEST, "Invalid request")
|
||||
}
|
||||
ChargingError::RatingPlanNotFound(ref plan_id) => {
|
||||
(StatusCode::NOT_FOUND, "Rating plan not found")
|
||||
}
|
||||
ChargingError::AccountOperation(ref msg) => {
|
||||
error!("Account operation error: {}", msg);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Account operation failed")
|
||||
}
|
||||
ChargingError::Serialization(ref msg) => {
|
||||
error!("Serialization error: {}", msg);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Data serialization failed")
|
||||
}
|
||||
ChargingError::Internal(ref msg) => {
|
||||
error!("Internal error: {}", msg);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
|
||||
}
|
||||
};
|
||||
|
||||
let body = Json(json!({
|
||||
"error": error_message,
|
||||
"details": self.to_string(),
|
||||
"timestamp": chrono::Utc::now().to_rfc3339()
|
||||
}));
|
||||
|
||||
(status, body).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<redis::RedisError> for ChargingError {
|
||||
fn from(err: redis::RedisError) -> Self {
|
||||
match err.kind() {
|
||||
redis::ErrorKind::Io => {
|
||||
ChargingError::RedisConnection(err.to_string())
|
||||
}
|
||||
_ => {
|
||||
ChargingError::RedisOperation(err.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ChargingError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
ChargingError::Serialization(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ChargingError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
ChargingError::Internal(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub type ChargingResult<T> = Result<T, ChargingError>;
|
||||
|
||||
pub trait ErrorContext<T> {
|
||||
fn with_context(self, context: &str) -> ChargingResult<T>;
|
||||
}
|
||||
|
||||
impl<T, E> ErrorContext<T> for Result<T, E>
|
||||
where
|
||||
E: Into<ChargingError>,
|
||||
{
|
||||
fn with_context(self, context: &str) -> ChargingResult<T> {
|
||||
self.map_err(|e| {
|
||||
let charging_err = e.into();
|
||||
match charging_err {
|
||||
ChargingError::RedisOperation(ref msg) => {
|
||||
ChargingError::RedisOperation(format!("{}: {}", context, msg))
|
||||
}
|
||||
ChargingError::AccountOperation(ref msg) => {
|
||||
ChargingError::AccountOperation(format!("{}: {}", context, msg))
|
||||
}
|
||||
ChargingError::Internal(ref msg) => {
|
||||
ChargingError::Internal(format!("{}: {}", context, msg))
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validation errors
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ValidationError {
|
||||
#[error("Invalid IP address: {0}")]
|
||||
InvalidIP(String),
|
||||
|
||||
#[error("Invalid byte value: {0}")]
|
||||
InvalidBytes(String),
|
||||
|
||||
#[error("Missing required field: {0}")]
|
||||
MissingField(String),
|
||||
|
||||
#[error("Field too long: {0} (max {1})")]
|
||||
FieldTooLong(String, usize),
|
||||
|
||||
#[error("Invalid format: {0}")]
|
||||
InvalidFormat(String),
|
||||
}
|
||||
|
||||
impl From<ValidationError> for ChargingError {
|
||||
fn from(err: ValidationError) -> Self {
|
||||
ChargingError::InvalidRequest(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Validation helpers
|
||||
pub fn validate_ip(ip: &str) -> Result<(), ValidationError> {
|
||||
if ip.is_empty() {
|
||||
return Err(ValidationError::MissingField("ip".to_string()));
|
||||
}
|
||||
|
||||
// Basic IP validation - could use std::net::IpAddr for more thorough validation
|
||||
if ip.len() > 45 { // IPv6 max length
|
||||
return Err(ValidationError::FieldTooLong("ip".to_string(), 45));
|
||||
}
|
||||
|
||||
// Simple validation for now - could be enhanced
|
||||
if ip.contains("..") || ip.starts_with('.') || ip.ends_with('.') {
|
||||
return Err(ValidationError::InvalidFormat(ip.to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_bytes(bytes: u64) -> Result<(), ValidationError> {
|
||||
if bytes == 0 {
|
||||
return Err(ValidationError::InvalidBytes("bytes cannot be zero".to_string()));
|
||||
}
|
||||
|
||||
if bytes > 1_000_000_000_000 { // 1TB limit
|
||||
return Err(ValidationError::InvalidBytes("bytes value too large".to_string()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_validate_ip() {
|
||||
assert!(validate_ip("192.168.1.1").is_ok());
|
||||
assert!(validate_ip("10.0.0.1").is_ok());
|
||||
|
||||
assert!(validate_ip("").is_err());
|
||||
assert!(validate_ip("192.168..1.1").is_err());
|
||||
assert!(validate_ip(".192.168.1.1").is_err());
|
||||
assert!(validate_ip("192.168.1.1.").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_bytes() {
|
||||
assert!(validate_bytes(1024).is_ok());
|
||||
assert!(validate_bytes(1_000_000_000).is_ok());
|
||||
|
||||
assert!(validate_bytes(0).is_err());
|
||||
assert!(validate_bytes(2_000_000_000_000).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_conversion() {
|
||||
let redis_err = redis::RedisError::from((
|
||||
redis::ErrorKind::Io,
|
||||
"Connection failed",
|
||||
"Detailed error"
|
||||
));
|
||||
let charging_err: ChargingError = redis_err.into();
|
||||
|
||||
match charging_err {
|
||||
ChargingError::RedisConnection(_) => {}
|
||||
_ => panic!("Expected RedisConnection error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use error_types::{ChargingError};
|
||||
pub use error_helpers::{ChargingResult, ErrorContext, validate_ip, validate_bytes, validate_imsi, validate_session_id, validate_amount, log_error};
|
||||
|
||||
Reference in New Issue
Block a user