#![allow(dead_code)]
#![warn(missing_docs)]
use crate::uci::base::UUID;
use crate::uci::{CalError, CalErrorKind, CalResult};
use std::env;
use std::fmt;
use std::path::Path;
use std::str::FromStr;
use std::sync::Arc;
#[cfg(feature = "zmq")]
pub mod zmq;
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 get_system_uuid(&self) -> UUID;
fn get_service_uuid(&self) -> Option<UUID>;
fn get_subsystem_uuid(&self) -> Option<UUID>;
fn get_component_uuid(&self, name: &str) -> Option<UUID>;
fn get_capability_uuid(&self, name: &str) -> Option<UUID>;
fn oms_schema_version(&self) -> &str;
fn oms_schema_compiler_version(&self) -> &str;
fn get_system_label(&self) -> Option<&str>;
fn get_asb_connection_version(&self) -> &str;
fn get_oms_api_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<()>;
}
#[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)]
pub(crate) static NEXT_TEST_PORT: std::sync::atomic::AtomicU16 =
std::sync::atomic::AtomicU16::new(2000);
#[cfg(test)]
mod trait_object_safety {
use super::*;
#[allow(dead_code)]
type _Asb = Arc<std::sync::Mutex<dyn AbstractServiceBus>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[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());
}
}