use std::path::Path;
use serde::Deserialize;
use tracing::info;
mod dds;
mod engine;
mod loopback;
mod owp;
mod stomp;
mod uci;
pub(crate) use dds::DdsSection;
pub(crate) use engine::EngineSection;
pub(crate) use loopback::LoopbackSection;
pub(crate) use owp::OwpSection;
pub(crate) use stomp::StompSection;
pub(crate) use uci::UciSection;
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct Config {
#[serde(default)]
pub(crate) engine: EngineSection,
#[serde(default)]
pub(crate) uci: UciSection,
#[serde(default)]
pub(crate) loopback: LoopbackSection,
#[serde(default)]
pub(crate) owp: OwpSection,
#[serde(default)]
pub(crate) stomp: StompSection,
#[serde(default)]
pub(crate) dds: DdsSection,
}
pub(crate) fn default_true() -> bool {
true
}
pub(crate) fn load(path: &Path) -> Result<Config, String> {
if !path.exists() {
return Err(format!("config file {} not found", path.display()));
}
let text = std::fs::read_to_string(path)
.map_err(|err| format!("cannot read {}: {err}", path.display()))?;
let config = toml::from_str(&text).map_err(|err| format!("in {}: {err}", path.display()))?;
info!(path = %path.display(), "config loaded");
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn misspelled_config_key_is_rejected() {
let err = toml::from_str::<Config>("[stomp]\ntopic = [\"PositionReport\"]\n").unwrap_err();
assert!(err.to_string().contains("topic"), "{err}");
}
#[test]
fn known_config_keys_still_parse() {
let config: Config = toml::from_str("[stomp]\ntopics = [\"PositionReport\"]\n").unwrap();
assert!(config.stomp.enabled);
assert_eq!(config.stomp.topics, ["PositionReport"]);
}
#[test]
fn a_present_adapter_section_is_on() {
let empty: Config = toml::from_str("").unwrap();
assert!(!empty.loopback.enabled);
assert!(!empty.owp.enabled);
assert!(!empty.stomp.enabled);
assert!(!empty.dds.enabled);
let named: Config =
toml::from_str("[owp]\n[stomp]\n[dds]\nqos = \"config/dds-qos.xml\"\n").unwrap();
assert!(!named.loopback.enabled);
assert!(named.owp.enabled);
assert!(named.stomp.enabled);
assert!(named.dds.enabled);
let held: Config = toml::from_str("[owp]\nenabled = false\n").unwrap();
assert!(!held.owp.enabled);
}
#[test]
fn shipped_configs_parse() {
for name in ["default.toml", "asb.toml", "compose.toml", "dds.toml"] {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../config")
.join(name);
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("{}: {err}", path.display()));
toml::from_str::<Config>(&text).unwrap_or_else(|err| panic!("{name}: {err}"));
}
}
#[test]
fn engine_and_stomp_runtime_knobs_parse() {
let config: Config = toml::from_str(
"[engine]\nstats_interval_secs = 0\n[stomp]\non_panic = \"reconnect\"\nsuppress_echo = false\n",
)
.unwrap();
assert_eq!(config.engine.stats_interval_secs, 0);
assert!(!config.stomp.suppress_echo);
assert_eq!(
config.stomp.on_panic_mode().unwrap(),
oa_gateway_stomp::OnPanic::Reconnect
);
let config: Config = toml::from_str("[stomp]\non_panic = \"die\"\n").unwrap();
let err = config.stomp.on_panic_mode().unwrap_err();
assert!(err.contains("stomp.on_panic"), "{err}");
}
#[test]
fn a_debug_of_the_config_does_not_leak_the_stomp_passcode() {
let config: Config =
toml::from_str("[stomp]\nlogin = \"user\"\npasscode = \"s3cr3t-passphrase\"\n")
.unwrap();
assert_eq!(config.stomp.passcode.expose(), "s3cr3t-passphrase");
let shown = format!("{config:?}");
assert!(!shown.contains("s3cr3t-passphrase"), "{shown}");
assert!(shown.contains("redacted"), "{shown}");
}
#[test]
fn dds_provider_and_qos_are_checked() {
let config: Config =
toml::from_str("[dds]\nprovider = \"rustdds\"\nqos = \"config/dds-qos.xml\"\n")
.unwrap();
assert!(config.dds.enabled);
assert_eq!(
config.dds.provider_kind().unwrap(),
oa_gateway_dds::DdsProviderKind::Rustdds
);
let config: Config = toml::from_str("[dds]\nprovider = \"cyclone\"\n").unwrap();
let err = config.dds.provider_kind().unwrap_err();
assert!(err.contains("dds.provider"), "{err}");
}
#[test]
fn a_missing_config_is_an_error() {
let err = load(Path::new("definitely/not/here.toml")).unwrap_err();
assert!(err.contains("not found"), "{err}");
}
}