#![allow(dead_code)]
#![warn(missing_docs)]
use crate::calconfig::CalConfig;
use crate::uci::CalMessage;
use crate::uci::base::UUID;
use crate::uci::types::{
ClassificationEnum, ID_Type as _, MessageModeEnum, OwnerProducerChoiceType_,
};
use crate::uci::{CalError, CalErrorKind, CalResult};
use chrono::Utc;
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::env;
use std::fmt;
use std::path::Path;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::Duration;
pub mod zmq;
use zmq::{ZMQ_ASB_ID, ZmqAsb};
pub fn get_asb_config_location(path: Option<String>) -> CalResult<String> {
let config_file = path.unwrap_or_else(|| {
env::var("RCAL_CONFIG").unwrap_or_else(|_| String::from_str("./CALConfig.toml").unwrap())
});
if Path::new(&config_file).exists() {
Ok(config_file)
} else {
Err(CalError::new(
CalErrorKind::InitializationFailure,
format!("Config file '{}' does not exist.", config_file),
))
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AsbConnectionState {
Initializing,
Normal,
Degraded,
Inoperable,
Failed,
}
impl AsbConnectionState {
pub fn allows_write(&self) -> bool {
matches!(self, Self::Normal | Self::Degraded)
}
pub fn allows_read_no_wait(&self) -> bool {
matches!(self, Self::Normal | Self::Degraded)
}
pub fn allows_add_listener(&self) -> bool {
!matches!(self, Self::Failed)
}
pub fn is_operational(&self) -> bool {
matches!(self, Self::Normal | Self::Degraded)
}
pub fn validate_transition(&self, next: AsbConnectionState) -> CalResult<()> {
let allowed = match (self, next) {
(Self::Initializing, Self::Normal) => true,
(Self::Initializing, Self::Degraded) => true,
(Self::Initializing, Self::Inoperable) => true,
(Self::Initializing, Self::Failed) => true,
(Self::Normal, Self::Degraded) => true,
(Self::Normal, Self::Inoperable) => true,
(Self::Normal, Self::Failed) => true,
(Self::Degraded, Self::Normal) => true,
(Self::Degraded, Self::Inoperable) => true,
(Self::Degraded, Self::Failed) => true,
(Self::Inoperable, Self::Initializing) => true,
(Self::Inoperable, Self::Normal) => true,
(Self::Inoperable, Self::Degraded) => true,
(Self::Inoperable, Self::Failed) => true,
(Self::Failed, _) => false,
_ => false,
};
if allowed {
Ok(())
} else {
Err(CalError::new(
CalErrorKind::InvalidState { current: *self },
format!(
"ASB state transition {self:?} → {next:?} is not permitted \
(Figure 5.9-2, OMSC-SPC-001 Rev L)",
),
))
}
}
}
impl fmt::Display for AsbConnectionState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Initializing => write!(f, "Initializing"),
Self::Normal => write!(f, "Normal"),
Self::Degraded => write!(f, "Degraded"),
Self::Inoperable => write!(f, "Inoperable"),
Self::Failed => write!(f, "Failed"),
}
}
}
#[derive(Debug, Clone)]
pub struct AsbStatus {
pub state: AsbConnectionState,
pub description: String,
}
impl AsbStatus {
pub fn new(state: AsbConnectionState, description: impl Into<String>) -> Self {
Self {
state,
description: description.into(),
}
}
}
pub trait AsbStatusListener: Send + Sync {
fn on_status_change(&self, status: &AsbStatus);
}
pub trait AbstractServiceBus: Send + Sync {
fn get_logger(&self) -> &slog::Logger;
fn service_identifier(&self) -> &str;
fn asb_identifier(&self) -> &str;
fn oms_schema_version(&self) -> &str;
fn oms_schema_compiler_version(&self) -> &str;
fn connection_status(&self) -> &AsbStatus;
fn register_status_listener(&mut self, listener: Arc<dyn AsbStatusListener>) -> CalResult<()>;
fn unregister_status_listener(
&mut self,
listener: &Arc<dyn AsbStatusListener>,
) -> CalResult<()>;
fn close(&mut self) -> CalResult<()>;
fn message_header_defaults(&self) -> MessageHeaderDefaults;
}
#[derive(Debug, Clone)]
pub struct MessageHeaderDefaults {
pub system_id: UUID,
pub service_id: Option<UUID>,
pub mission_id: Option<UUID>,
pub schema_version: String,
pub mode: MessageModeEnum,
pub classification: ClassificationEnum,
pub owner_producer: Vec<OwnerProducerChoiceType_>,
}
pub trait MessageListener<M: CalMessage>: Send + Sync {
fn on_message(&self, message: &Arc<M>);
}
pub trait AbstractWriter<M: CalMessage>: Send + Sync {
fn topic(&self) -> &str;
fn write(&mut self, message: &M) -> CalResult<()>;
fn close(self: Box<Self>) -> CalResult<()>;
}
pub trait AbstractReader<M: CalMessage>: Send + Sync {
fn topic(&self) -> &str;
fn add_listener(&mut self, listener: Arc<dyn MessageListener<M>>) -> CalResult<()>;
fn remove_listener(&mut self, listener: &Arc<dyn MessageListener<M>>) -> CalResult<()>;
fn read(&mut self, timeout: Option<Duration>) -> CalResult<Option<Arc<M>>>;
fn read_no_wait(&mut self) -> CalResult<Option<Arc<M>>>;
fn close(self: Box<Self>) -> CalResult<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Reliability {
#[default]
BestEffort,
Reliable,
}
#[derive(Debug, Clone)]
pub struct TimeBasedFilter {
pub min_separation: Duration,
}
#[derive(Debug, Clone)]
pub struct Expiration {
pub max_age: Duration,
}
#[derive(Debug, Clone)]
pub struct MessageBuffer {
pub max_messages: usize,
}
#[derive(Debug, Clone, Default)]
pub struct TopicQos {
pub reliability: Reliability,
pub time_based_filter: Option<TimeBasedFilter>,
pub expiration: Option<Expiration>,
pub writer_buffer: Option<MessageBuffer>,
pub reader_buffer: Option<MessageBuffer>,
}
pub trait AbstractServiceBusExt<M: CalMessage>: AbstractServiceBus {
fn create_writer(
&mut self,
topic: &str,
qos: TopicQos,
) -> CalResult<Box<dyn AbstractWriter<M>>>;
fn create_reader(
&mut self,
topic: &str,
qos: TopicQos,
) -> CalResult<Box<dyn AbstractReader<M>>>;
}
pub trait AbstractServiceBusCreateMessage {
fn create_message<M: CalMessage>(&self) -> CalResult<M>;
}
impl<T: AbstractServiceBus> AbstractServiceBusCreateMessage for T {
fn create_message<M: CalMessage>(&self) -> CalResult<M> {
let mut msg = M::cal_create();
if let Some(mt) = msg.as_message_type_mut() {
let defaults = self.message_header_defaults();
let hdr = mt.message_header_mut();
*hdr.system_id_mut().uuid_mut() = defaults.system_id;
*hdr.schema_version_mut() = defaults.schema_version;
*hdr.mode_mut() = defaults.mode;
*hdr.timestamp_mut() = Utc::now().into();
if let (Some(sid), Some(sfield)) = (defaults.service_id, hdr.service_id_mut()) {
*sfield.uuid_mut() = sid;
}
if let (Some(mid), Some(mfield)) = (defaults.mission_id, hdr.mission_id_mut()) {
*mfield.uuid_mut() = mid;
}
let sec = mt.security_information_mut();
*sec.classification_mut() = defaults.classification;
let op = sec.owner_producer_mut();
op.clear();
op.extend(defaults.owner_producer);
}
Ok(msg)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct AsbKey {
service_identifier: String,
asb_identifier: String,
}
type AsbInstance = Arc<Mutex<dyn AbstractServiceBus>>;
type AsbFactoryMap = HashMap<AsbKey, AsbInstance>;
lazy_static! {
static ref ASB_FACTORY: Mutex<AsbFactoryMap> = Mutex::new(AsbFactoryMap::new());
}
pub async fn get_asb(
service_identifier: impl Into<String>,
asb_identifier: impl Into<String>,
config: Arc<CalConfig>,
logger: slog::Logger,
) -> CalResult<AsbInstance> {
let key = AsbKey {
service_identifier: service_identifier.into(),
asb_identifier: asb_identifier.into(),
};
let transport = {
let map = ASB_FACTORY.lock().unwrap();
if let Some(existing) = map.get(&key) {
return Ok(Arc::clone(existing));
}
config
.get_transport(&key.asb_identifier)
.or_else(|| {
config
.system
.default_transport
.as_ref()
.and_then(|def| config.get_transport(def))
})
.ok_or_else(|| {
CalError::new(
CalErrorKind::InitializationFailure,
format!(
"No transport configured for '{}' and no default_transport available.",
key.asb_identifier
),
)
})?
};
let instance: AsbInstance = match transport.type_.as_str() {
ZMQ_ASB_ID => Arc::new(Mutex::new(
ZmqAsb::new(
key.service_identifier.clone(),
key.asb_identifier.clone(),
logger,
Arc::clone(&config),
transport,
)
.await?,
)),
other => {
return Err(CalError::new(
CalErrorKind::InitializationFailure,
format!("Unknown ASB transport type: '{other}'."),
));
}
};
let mut map = ASB_FACTORY.lock().unwrap();
Ok(Arc::clone(map.entry(key).or_insert(instance)))
}
#[macro_export]
macro_rules! update_message_header {
($msg:expr) => {
if let Some(mt) = $crate::uci::CalMessage::as_message_type_mut(&mut $msg) {
*mt.message_header_mut().timestamp_mut() = chrono::Utc::now().into();
}
};
}
#[cfg(test)]
mod trait_object_safety {
use super::*;
use crate::uci::CalMessage;
struct Ping;
impl CalMessage for Ping {
fn message_type_name() -> crate::QName {
"test.Ping".into()
}
fn cal_create() -> Self {
Self
}
}
#[allow(dead_code)]
type _W = Box<dyn AbstractWriter<Ping>>;
#[allow(dead_code)]
type _R = Box<dyn AbstractReader<Ping>>;
#[allow(dead_code)]
type _L = Arc<dyn MessageListener<Ping>>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::calconfig::{get_test_config_path, parse_config_from_file};
use rcal_macros::init_test_logger;
use std::sync::atomic::{AtomicU16, Ordering};
static NEXT_PORT: AtomicU16 = AtomicU16::new(55700);
#[init_test_logger]
#[tokio::test]
async fn test_asb_factory_same_key_returns_same_instance() {
let p1 = NEXT_PORT.fetch_add(1, Ordering::SeqCst);
let p2 = NEXT_PORT.fetch_add(1, Ordering::SeqCst);
let config = zmq::test_config_on_ports(&[p1, p2]);
let a = get_asb("test_svc", "TestZmq", Arc::clone(&config), logger.clone())
.await
.expect("first get_asb must succeed");
let b = get_asb("test_svc", "TestZmq", Arc::clone(&config), logger.clone())
.await
.expect("second get_asb must succeed");
let c = get_asb(
"test_svc_2",
"TestZmq2",
Arc::clone(&config),
logger.clone(),
)
.await
.expect("different service must succeed");
assert!(Arc::ptr_eq(&a, &b), "same key should return the same Arc");
assert!(
!Arc::ptr_eq(&a, &c),
"different service key must be a distinct instance"
);
}
#[init_test_logger]
#[tokio::test]
async fn test_asb_factory_unknown_transport_returns_err() {
let no_default_config = Arc::new(
parse_config_from_file(&get_test_config_path("calconfig_no_default.toml")).unwrap(),
);
assert!(
get_asb("svc", "dummy", no_default_config, logger)
.await
.is_err()
);
}
#[tokio::test]
async fn test_state_display_does_not_recurse() {
assert_eq!(AsbConnectionState::Initializing.to_string(), "Initializing");
assert_eq!(AsbConnectionState::Normal.to_string(), "Normal");
assert_eq!(AsbConnectionState::Degraded.to_string(), "Degraded");
assert_eq!(AsbConnectionState::Inoperable.to_string(), "Inoperable");
assert_eq!(AsbConnectionState::Failed.to_string(), "Failed");
}
#[tokio::test]
async fn test_valid_transitions() {
use AsbConnectionState::*;
let cases = [
(Initializing, Normal),
(Initializing, Degraded),
(Initializing, Inoperable),
(Initializing, Failed),
(Normal, Degraded),
(Normal, Inoperable),
(Normal, Failed),
(Degraded, Normal),
(Degraded, Inoperable),
(Degraded, Failed),
(Inoperable, Initializing),
(Inoperable, Normal),
(Inoperable, Degraded),
(Inoperable, Failed),
];
for (from, to) in cases {
assert!(
from.validate_transition(to).is_ok(),
"{from:?} → {to:?} should be allowed"
);
}
}
#[tokio::test]
async fn test_invalid_transitions() {
use AsbConnectionState::*;
for to in [Initializing, Normal, Degraded, Inoperable] {
assert!(
Failed.validate_transition(to).is_err(),
"Failed → {to:?} must be rejected"
);
}
assert!(Normal.validate_transition(Initializing).is_err());
}
}