use serde::Deserialize;
use std::path::{Path, PathBuf};
pub const CONFIG_FILE: &str = "banc-rig.toml";
pub const ENV_VAR: &str = "BANC_RIG";
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct RigConfig {
#[serde(default)]
pub rig: RigMeta,
pub target: Option<TargetConfig>,
#[serde(default, rename = "assistant")]
pub assistants: Vec<AssistantConfig>,
#[serde(default, rename = "instrument")]
pub instruments: Vec<InstrumentConfig>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct RigMeta {
pub name: Option<String>,
pub lock_file: Option<PathBuf>,
pub lease: Option<LeaseConfig>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LeaseConfig {
pub addr: String,
pub token_file: PathBuf,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TargetConfig {
pub chip: String,
pub probe: Option<String>,
pub probe_host: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AssistantConfig {
pub name: String,
pub serial: Option<String>,
pub product: Option<String>,
pub addr: Option<String>,
pub token_file: Option<PathBuf>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InstrumentConfig {
pub name: String,
pub kind: String,
pub address: Option<String>,
#[serde(default)]
pub params: toml::Table,
}
impl RigConfig {
pub fn locate() -> anyhow::Result<Option<PathBuf>> {
if let Ok(path) = std::env::var(ENV_VAR) {
let path = PathBuf::from(path);
anyhow::ensure!(
path.is_file(),
"{ENV_VAR} points at {} which does not exist",
path.display()
);
return Ok(Some(path));
}
let mut dir = std::env::current_dir()?;
loop {
let candidate = dir.join(CONFIG_FILE);
if candidate.is_file() {
return Ok(Some(candidate));
}
if !dir.pop() {
return Ok(None);
}
}
}
pub fn load(path: &Path) -> anyhow::Result<Self> {
let text = std::fs::read_to_string(path)?;
let config: RigConfig = toml::from_str(&text)
.map_err(|e| anyhow::anyhow!("parsing {}: {e}", path.display()))?;
Ok(config)
}
pub fn assistant(&self, name: &str) -> Option<&AssistantConfig> {
self.assistants.iter().find(|a| a.name == name)
}
pub fn instrument(&self, name: &str) -> Option<&InstrumentConfig> {
self.instruments.iter().find(|i| i.name == name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_full_config() {
let cfg: RigConfig = toml::from_str(
r#"
[rig]
name = "bench-1"
[target]
chip = "RP2350"
probe = "2e8a:000c"
[[assistant]]
name = "a0"
serial = "0123456789ABCDEF"
[[instrument]]
name = "att0"
kind = "rcdat"
address = "192.168.1.50:23"
params = { max_db = 90.0 }
"#,
)
.unwrap();
assert_eq!(cfg.rig.name.as_deref(), Some("bench-1"));
assert_eq!(cfg.target.as_ref().unwrap().chip, "RP2350");
assert_eq!(cfg.assistant("a0").unwrap().serial.as_deref(), Some("0123456789ABCDEF"));
assert_eq!(cfg.instrument("att0").unwrap().kind, "rcdat");
assert!(cfg.assistant("nope").is_none());
}
#[test]
fn empty_config_is_valid() {
let cfg: RigConfig = toml::from_str("").unwrap();
assert!(cfg.target.is_none());
assert!(cfg.assistants.is_empty());
}
}