pub mod ffi;
use std::{fmt, time::Duration};
#[derive(Debug, thiserror::Error)]
pub enum CarlaError {
#[error(transparent)]
Connection(#[from] ConnectionError),
#[error(transparent)]
Resource(#[from] ResourceError),
#[error(transparent)]
Operation(#[from] OperationError),
#[error(transparent)]
Validation(#[from] ValidationError),
#[error(transparent)]
Map(#[from] MapError),
#[error(transparent)]
Sensor(#[from] SensorError),
#[error(transparent)]
Internal(#[from] InternalError),
#[error(transparent)]
Io(#[from] std::io::Error),
}
#[derive(Debug, thiserror::Error)]
pub enum ConnectionError {
#[error("Operation '{operation}' timed out after {duration:?}")]
Timeout {
operation: String,
duration: Duration,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Disconnected from CARLA server: {reason}")]
Disconnected {
reason: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Invalid server endpoint: {host}:{port}")]
InvalidEndpoint {
host: String,
port: u16,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
}
#[derive(Debug, thiserror::Error)]
pub enum ResourceError {
#[error("{resource_type} not found: {identifier}{}", context.as_ref().map(|c| format!(" ({})", c)).unwrap_or_default())]
NotFound {
resource_type: ResourceType,
identifier: String,
context: Option<String>,
},
#[error("{resource_type} already exists: {identifier}")]
AlreadyExists {
resource_type: ResourceType,
identifier: String,
},
#[error("{resource_type} was destroyed: {identifier}")]
Destroyed {
resource_type: ResourceType,
identifier: String,
},
#[error("{resource_type} unavailable: {reason}")]
Unavailable {
resource_type: ResourceType,
reason: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceType {
Actor,
Blueprint,
Map,
Sensor,
TrafficLight,
Waypoint,
Episode,
Recording,
}
#[derive(Debug, thiserror::Error)]
pub enum OperationError {
#[error("Failed to spawn actor '{blueprint}' at {transform}: {reason}")]
SpawnFailed {
blueprint: String,
transform: String,
reason: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Invalid transform {transform}: {reason}")]
InvalidTransform {
transform: String,
reason: String,
},
#[error("Operation '{operation}' requires physics for actor {actor_id}")]
PhysicsDisabled {
actor_id: u32,
operation: String,
},
#[error("Simulation error: {message}")]
SimulationError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
}
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("Invalid blueprint '{blueprint_id}': {reason}")]
InvalidBlueprint {
blueprint_id: String,
reason: String,
},
#[error("Invalid attribute '{name}' = '{value}': {reason}")]
InvalidAttribute {
name: String,
value: String,
reason: String,
},
#[error("Value {value} out of bounds [{min}, {max}] for field '{field}'")]
OutOfBounds {
field: String,
value: String,
min: String,
max: String,
},
#[error("Invalid configuration for '{setting}': {reason}")]
InvalidConfiguration {
setting: String,
reason: String,
},
}
#[derive(Debug, thiserror::Error)]
pub enum MapError {
#[error("Failed to load map '{map_name}': {reason}")]
LoadFailed {
map_name: String,
reason: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Invalid waypoint at {location}: {reason}")]
InvalidWaypoint {
location: String,
reason: String,
},
#[error("Map topology error: {message}")]
TopologyError {
message: String,
},
}
#[derive(Debug, thiserror::Error)]
pub enum SensorError {
#[error("Sensor data unavailable for sensor {sensor_id}: {reason}")]
DataUnavailable {
sensor_id: u32,
reason: String,
},
#[error("Invalid sensor configuration for '{sensor_type}': {reason}")]
InvalidConfiguration {
sensor_type: String,
reason: String,
},
#[error("Failed to listen to sensor {sensor_id}: {reason}")]
ListenFailed {
sensor_id: u32,
reason: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
}
#[derive(Debug, thiserror::Error)]
pub enum InternalError {
#[error("FFI error: {message}")]
FfiError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Serialization error in {context}")]
SerializationError {
context: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("Unexpected state: {description}")]
UnexpectedState {
description: String,
},
}
pub type Result<T> = std::result::Result<T, CarlaError>;
impl ResourceType {
pub fn as_str(&self) -> &'static str {
match self {
ResourceType::Actor => "Actor",
ResourceType::Blueprint => "Blueprint",
ResourceType::Map => "Map",
ResourceType::Sensor => "Sensor",
ResourceType::TrafficLight => "TrafficLight",
ResourceType::Waypoint => "Waypoint",
ResourceType::Episode => "Episode",
ResourceType::Recording => "Recording",
}
}
}
impl fmt::Display for ResourceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl CarlaError {
pub fn is_connection_error(&self) -> bool {
matches!(self, CarlaError::Connection(_))
}
pub fn is_timeout(&self) -> bool {
matches!(
self,
CarlaError::Connection(ConnectionError::Timeout { .. })
)
}
pub fn is_not_found(&self) -> bool {
matches!(self, CarlaError::Resource(ResourceError::NotFound { .. }))
}
pub fn is_validation_error(&self) -> bool {
matches!(self, CarlaError::Validation(_))
}
pub fn is_operation_error(&self) -> bool {
matches!(self, CarlaError::Operation(_))
}
pub fn is_map_error(&self) -> bool {
matches!(self, CarlaError::Map(_))
}
pub fn is_sensor_error(&self) -> bool {
matches!(self, CarlaError::Sensor(_))
}
pub fn is_internal_error(&self) -> bool {
matches!(self, CarlaError::Internal(_))
}
pub fn is_retriable(&self) -> bool {
matches!(
self,
CarlaError::Connection(ConnectionError::Timeout { .. })
| CarlaError::Connection(ConnectionError::Disconnected { .. })
| CarlaError::Operation(OperationError::SpawnFailed { .. })
)
}
}
pub trait ResultExt<T> {
fn context(self, msg: &str) -> Result<T>;
fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T>;
}
impl<T> ResultExt<T> for Option<T> {
fn context(self, msg: &str) -> Result<T> {
self.ok_or_else(|| {
InternalError::UnexpectedState {
description: msg.to_string(),
}
.into()
})
}
fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T> {
self.ok_or_else(|| InternalError::UnexpectedState { description: f() }.into())
}
}
impl<T, E> ResultExt<T> for std::result::Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
fn context(self, msg: &str) -> Result<T> {
self.map_err(|e| {
InternalError::FfiError {
message: format!("{}: {}", msg, e),
source: Some(Box::new(e)),
}
.into()
})
}
fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T> {
self.map_err(|e| {
InternalError::FfiError {
message: format!("{}: {}", f(), e),
source: Some(Box::new(e)),
}
.into()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connection_timeout_display() {
let error = ConnectionError::Timeout {
operation: "spawn_actor".to_string(),
duration: Duration::from_secs(5),
source: None,
};
assert_eq!(
format!("{}", error),
"Operation 'spawn_actor' timed out after 5s"
);
}
#[test]
fn test_resource_not_found_display() {
let error = ResourceError::NotFound {
resource_type: ResourceType::Blueprint,
identifier: "vehicle.tesla.model3".to_string(),
context: Some("BlueprintLibrary::find".to_string()),
};
assert_eq!(
format!("{}", error),
"Blueprint not found: vehicle.tesla.model3 (BlueprintLibrary::find)"
);
}
#[test]
fn test_error_classification() {
let error: CarlaError = ResourceError::NotFound {
resource_type: ResourceType::Blueprint,
identifier: "test".to_string(),
context: None,
}
.into();
assert!(error.is_not_found());
assert!(!error.is_timeout());
assert!(!error.is_connection_error());
assert!(!error.is_retriable());
}
#[test]
fn test_timeout_is_retriable() {
let error: CarlaError = ConnectionError::Timeout {
operation: "spawn".to_string(),
duration: Duration::from_secs(5),
source: None,
}
.into();
assert!(error.is_timeout());
assert!(error.is_connection_error());
assert!(error.is_retriable());
}
#[test]
fn test_spawn_failed_is_retriable() {
let error: CarlaError = OperationError::SpawnFailed {
blueprint: "vehicle.tesla.model3".to_string(),
transform: "(0, 0, 0)".to_string(),
reason: "collision".to_string(),
source: None,
}
.into();
assert!(error.is_retriable());
assert!(!error.is_connection_error());
}
#[test]
fn test_resource_type_display() {
assert_eq!(ResourceType::Actor.as_str(), "Actor");
assert_eq!(ResourceType::Blueprint.as_str(), "Blueprint");
assert_eq!(format!("{}", ResourceType::Map), "Map");
}
#[test]
fn test_validation_error_display() {
let error = ValidationError::OutOfBounds {
field: "throttle".to_string(),
value: "1.5".to_string(),
min: "0.0".to_string(),
max: "1.0".to_string(),
};
assert_eq!(
format!("{}", error),
"Value 1.5 out of bounds [0.0, 1.0] for field 'throttle'"
);
}
#[test]
fn test_io_error_conversion() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let error: CarlaError = io_error.into();
assert!(matches!(error, CarlaError::Io(_)));
}
#[test]
fn test_is_validation_error() {
let error: CarlaError = ValidationError::InvalidConfiguration {
setting: "test".to_string(),
reason: "bad".to_string(),
}
.into();
assert!(error.is_validation_error());
assert!(!error.is_operation_error());
}
#[test]
fn test_is_operation_error() {
let error: CarlaError = OperationError::SimulationError {
message: "test".to_string(),
source: None,
}
.into();
assert!(error.is_operation_error());
assert!(!error.is_validation_error());
}
#[test]
fn test_is_map_error() {
let error: CarlaError = MapError::TopologyError {
message: "test".to_string(),
}
.into();
assert!(error.is_map_error());
}
#[test]
fn test_is_sensor_error() {
let error: CarlaError = SensorError::DataUnavailable {
sensor_id: 1,
reason: "test".to_string(),
}
.into();
assert!(error.is_sensor_error());
}
#[test]
fn test_is_internal_error() {
let error: CarlaError = InternalError::UnexpectedState {
description: "test".to_string(),
}
.into();
assert!(error.is_internal_error());
}
#[test]
fn test_result_ext_option_context() {
let opt: Option<i32> = None;
let err = opt.context("missing value").unwrap_err();
assert!(err.is_internal_error());
assert!(format!("{}", err).contains("missing value"));
}
#[test]
fn test_result_ext_option_some() {
let opt: Option<i32> = Some(42);
assert_eq!(opt.context("should not fail").unwrap(), 42);
}
#[test]
fn test_result_ext_option_with_context() {
let opt: Option<i32> = None;
let err = opt
.with_context(|| format!("value {} missing", 42))
.unwrap_err();
assert!(err.is_internal_error());
assert!(format!("{}", err).contains("value 42 missing"));
}
#[test]
fn test_result_ext_result_context() {
let res: std::result::Result<i32, std::io::Error> = Err(std::io::Error::other("io fail"));
let err = res.context("operation failed").unwrap_err();
assert!(err.is_internal_error());
assert!(format!("{}", err).contains("operation failed"));
}
#[test]
fn test_result_ext_result_ok() {
let res: std::result::Result<i32, std::io::Error> = Ok(42);
assert_eq!(res.context("should not fail").unwrap(), 42);
}
}