#![allow(dead_code)]
use crate::uci::base::UUID;
use crate::uci::{CalError, CalImplementationErrorKind, CalResult};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::fs;
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
#[serde(default)]
pub struct CalConfig {
pub system: System,
#[serde(rename = "uuid-factory")]
pub uuidfactory: UUIDFactory,
pub transport: Vec<Transport>,
pub service: Vec<Service>,
pub externalizer: HashMap<String, ExternalizerConfig>,
}
impl CalConfig {
pub fn get_service(&self, name: &str) -> Option<&Service> {
self.service.iter().find(|item| item.id == name)
}
pub fn get_transport(&self, name: &str) -> Option<&Transport> {
self.transport.iter().find(|item| item.id == name)
}
pub fn get_transport_for_service(&self, name: &str) -> Option<&Transport> {
let service_conf = self.get_service(name)?;
let transport_name = service_conf
.transport
.as_ref()
.or(self.system.default_transport.as_ref())?;
self.get_transport(transport_name)
}
}
impl fmt::Display for CalConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", toml::to_string(self).unwrap())
}
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Trace,
Debug,
#[default]
Info,
Warn,
Error,
}
impl From<LogLevel> for slog::Level {
fn from(l: LogLevel) -> Self {
match l {
LogLevel::Trace => slog::Level::Trace,
LogLevel::Debug => slog::Level::Debug,
LogLevel::Info => slog::Level::Info,
LogLevel::Warn => slog::Level::Warning,
LogLevel::Error => slog::Level::Error,
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum LogFormat {
#[default]
Pretty,
Basic,
Logfmt,
Json,
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase", tag = "type")]
pub enum SinkType {
#[default]
Stdout,
Stderr,
File {
path: String,
},
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(default)]
pub struct SinkConfig {
#[serde(flatten)]
pub sink_type: SinkType,
pub level: LogLevel,
pub format: LogFormat,
pub subsystems: Vec<String>,
}
impl Default for SinkConfig {
fn default() -> Self {
Self {
sink_type: SinkType::Stdout,
level: LogLevel::Warn,
format: LogFormat::Pretty,
subsystems: Vec::new(),
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
#[serde(default)]
pub struct LoggingConfig {
pub default_level: LogLevel,
pub sink: Vec<SinkConfig>,
}
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
#[serde(default)]
pub struct System {
pub id: String,
pub label: Option<String>,
pub uuid: UUID,
pub default_transport: Option<String>,
pub logging: LoggingConfig,
pub mission_id: Option<UUID>,
pub mode: Option<String>,
pub classification: Option<String>,
pub owner_producer: Vec<String>,
}
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
pub enum UUIDFactoryType {
#[default]
Random,
TimeBased,
}
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
#[serde(default)]
pub struct UUIDFactory {
#[serde(rename = "type")]
pub type_: UUIDFactoryType,
pub namespace: Option<UUID>,
pub node: Option<mac_address::MacAddress>,
}
#[derive(Deserialize, Serialize, Default, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SerializationFormat {
#[default]
Xml,
PrettyXml,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ExternalizerConfig {
Xml {
#[serde(default)]
pretty: bool,
},
#[cfg(feature = "compression")]
Compression {
#[serde(default = "default_inner_externalizer")]
inner: String,
#[serde(default)]
compression_type: CompressionType,
#[serde(default)]
options: HashMap<String, toml::Value>,
},
}
#[cfg(feature = "compression")]
impl CompressionType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Gzip => "gzip",
Self::Deflate => "deflate",
Self::Zlib => "zlib",
}
}
}
#[cfg(feature = "compression")]
impl std::str::FromStr for CompressionType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"gzip" => Ok(Self::Gzip),
"deflate" => Ok(Self::Deflate),
"zlib" => Ok(Self::Zlib),
_ => Err(()),
}
}
}
#[cfg(feature = "compression")]
fn default_inner_externalizer() -> String {
"xml".to_string()
}
#[cfg(feature = "compression")]
#[derive(Deserialize, Serialize, Default, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CompressionType {
#[default]
Gzip,
Deflate,
Zlib,
}
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
#[serde(default)]
pub struct Transport {
pub id: String,
#[serde(rename = "type")]
pub type_: String,
pub uri: String,
pub externalizer: Option<String>,
}
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
pub struct NamedUuid {
pub name: String,
pub uuid: UUID,
}
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
#[serde(default)]
pub struct Service {
pub id: String,
pub transport: Option<String>,
pub topic: Vec<Topic>,
pub uuid: Option<UUID>,
pub subsystem_uuid: Option<UUID>,
pub components: Vec<NamedUuid>,
pub capabilities: Vec<NamedUuid>,
pub status_delay: Option<String>,
pub service_status_data_request_enable: bool,
}
impl Service {
pub fn get_component_uuid(&self, name: &str) -> Option<UUID> {
self.components
.iter()
.find(|c| c.name == name)
.map(|c| c.uuid)
}
pub fn get_capability_uuid(&self, name: &str) -> Option<UUID> {
self.capabilities
.iter()
.find(|c| c.name == name)
.map(|c| c.uuid)
}
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReliabilityConfig {
#[default]
BestEffort,
Reliable,
}
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
#[serde(default)]
pub struct TopicQosConfig {
pub reliability: Option<ReliabilityConfig>,
pub time_based_filter_ms: Option<u64>,
pub expiration_ms: Option<u64>,
pub writer_buffer: Option<usize>,
pub reader_buffer: Option<usize>,
}
#[derive(Deserialize, Serialize, Default, Debug, Clone)]
#[serde(default)]
pub struct Topic {
pub id: String,
#[serde(rename = "type")]
pub type_: Option<String>,
pub topic: Option<String>,
pub qos: Option<TopicQosConfig>,
}
pub fn parse_config_from_file(filename: &str) -> CalResult<CalConfig> {
let config_str = fs::read_to_string(filename).map_err(|err| {
CalError::with_impl_source(
CalImplementationErrorKind::ConfigError,
format!("Can't read config file: {}", filename),
err,
)
})?;
parse_config(config_str.as_str())
}
pub fn parse_config(config_str: &str) -> CalResult<CalConfig> {
let config = toml::from_str(config_str).map_err(|err| {
CalError::with_impl_source(
CalImplementationErrorKind::ConfigError,
"Can't parse configuration",
err,
)
})?;
Ok(config)
}
#[cfg(test)]
use std::env;
#[cfg(test)]
use std::path::PathBuf;
#[cfg(test)]
pub fn get_test_config_path(filename: &str) -> String {
let mut file_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
file_path.push("tests");
file_path.push("fixtures");
file_path.push(filename);
file_path.to_string_lossy().into_owned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_file() {
parse_config_from_file(get_test_config_path("calconfig_sample.toml").as_str()).unwrap();
}
#[test]
fn test_uuid_factory() {
parse_config("[system]\nid=\"foo\"\n[uuid-factory]\ntype=\"Random\"\n").unwrap();
parse_config("[system]\nid=\"foo\"\n[uuid-factory]\ntype=\"TimeBased\"\n").unwrap();
parse_config("[system]\nid=\"foo\"\n[uuid-factory]\ntype=\"TimeBased\"\nnode=\"00:11:22:33:44:55\"\n").unwrap();
}
#[test]
fn test_topic_qos_config_parses() {
let toml = r#"
[system]
id = "test"
[[service]]
id = "Svc"
[[service.topic]]
id = "SystemStatus"
[service.topic.qos]
reliability = "best_effort"
time_based_filter_ms = 100
expiration_ms = 5000
reader_buffer = 10
writer_buffer = 5
"#;
let cfg = parse_config(toml).unwrap();
let svc = cfg.get_service("Svc").unwrap();
let topic = svc.topic.iter().find(|t| t.id == "SystemStatus").unwrap();
let qos = topic.qos.as_ref().unwrap();
assert_eq!(qos.reliability, Some(ReliabilityConfig::BestEffort));
assert_eq!(qos.time_based_filter_ms, Some(100));
assert_eq!(qos.expiration_ms, Some(5000));
assert_eq!(qos.reader_buffer, Some(10));
assert_eq!(qos.writer_buffer, Some(5));
}
#[test]
fn test_topic_without_qos_parses() {
let toml = "[system]\nid=\"foo\"\n[[service]]\nid=\"Svc\"\n[[service.topic]]\nid=\"T\"\n";
let cfg = parse_config(toml).unwrap();
let topic = &cfg.get_service("Svc").unwrap().topic[0];
assert!(topic.qos.is_none());
}
}