use std::path::Path;
use crate::config::{validate_config, CameraConfig, Config};
use crate::hassio::{merge, options};
#[allow(clippy::too_many_arguments)]
pub fn run(
options_json: &Path,
overlay: Option<&Path>,
mqtt_host: Option<String>,
mqtt_port: Option<u16>,
mqtt_user: Option<String>,
mqtt_pass: Option<String>,
mqtt_ssl: bool,
output: Option<&Path>,
) -> anyhow::Result<()> {
let opts_src = std::fs::read_to_string(options_json)
.map_err(|e| anyhow::anyhow!("read options.json at {}: {e}", options_json.display()))?;
let opts: options::HassioOptions =
serde_json::from_str(&opts_src).map_err(|e| anyhow::anyhow!("parse options.json: {e}"))?;
let mqtt_flags = options::MqttServiceFlags {
host: mqtt_host.filter(|s| !s.is_empty()),
port: mqtt_port.filter(|&p| p != 0),
username: mqtt_user.filter(|s| !s.is_empty()),
password: mqtt_pass.filter(|s| !s.is_empty()),
ssl: mqtt_ssl,
};
let mut cfg = options::build_base_config(&opts, &mqtt_flags);
if let Some(p) = overlay {
if p.exists() {
let src = std::fs::read_to_string(p)
.map_err(|e| anyhow::anyhow!("read overlay at {}: {e}", p.display()))?;
let overlay_cfg = merge::parse_overlay(&src).map_err(|e| anyhow::anyhow!("{e}"))?;
cfg = merge::merge(cfg, overlay_cfg);
}
}
validate_config(&cfg).map_err(|e| anyhow::anyhow!("merged config invalid: {e}"))?;
let rendered = minimize_render(&cfg)?;
match output {
Some(p) => std::fs::write(p, rendered)
.map_err(|e| anyhow::anyhow!("write {}: {e}", p.display()))?,
None => print!("{rendered}"),
}
Ok(())
}
fn minimize_render(cfg: &Config) -> anyhow::Result<String> {
let mut value =
toml::Value::try_from(cfg).map_err(|e| anyhow::anyhow!("serialise config: {e}"))?;
let cfg_default = toml::Value::try_from(Config::default())
.map_err(|e| anyhow::anyhow!("serialise default config: {e}"))?;
let cam_default = toml::Value::try_from(CameraConfig::default())
.map_err(|e| anyhow::anyhow!("serialise default camera: {e}"))?;
strip_defaults(&mut value, &cfg_default);
if let Some(toml::Value::Array(cameras)) =
value.as_table_mut().and_then(|t| t.get_mut("cameras"))
{
for cam in cameras {
strip_defaults(cam, &cam_default);
}
}
toml::to_string_pretty(&value).map_err(|e| anyhow::anyhow!("serialise stripped config: {e}"))
}
fn strip_defaults(value: &mut toml::Value, default: &toml::Value) {
let (toml::Value::Table(t), toml::Value::Table(d)) = (value, default) else {
return;
};
let keys: Vec<String> = t.keys().cloned().collect();
for k in keys {
let Some(dv) = d.get(&k).cloned() else {
continue;
};
let v = t.get(&k).cloned().expect("key just enumerated");
if v == dv {
t.remove(&k);
continue;
}
if v.is_table() {
let mut nested = v;
strip_defaults(&mut nested, &dv);
match &nested {
toml::Value::Table(nt) if nt.is_empty() => {
t.remove(&k);
}
_ => {
t.insert(k, nested);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minimize_strips_top_level_defaults() {
let mut cfg = Config::default();
cfg.cameras.push(CameraConfig {
name: "cam1".into(),
username: "admin".into(),
password: Some("pw".into()),
uid: Some("UID1".into()),
..CameraConfig::default()
});
let out = minimize_render(&cfg).unwrap();
assert!(!out.contains("bind ="), "stripped: bind\n{out}");
assert!(!out.contains("bind_port"), "stripped: bind_port\n{out}");
assert!(!out.contains("users"), "stripped: users\n{out}");
assert!(!out.contains("tls_client_auth"));
assert!(!out.contains("stream_prune_grace_secs"));
assert!(out.contains(r#"name = "cam1""#));
assert!(out.contains(r#"uid = "UID1""#));
assert!(out.contains(r#"username = "admin""#));
assert!(out.contains(r#"password = "pw""#));
assert!(!out.contains("channel_id"));
assert!(!out.contains("discovery"));
assert!(!out.contains("max_encryption"));
assert!(!out.contains("[cameras.mqtt]"));
assert!(!out.contains("[cameras.pause]"));
assert!(!out.contains("permitted_users"));
}
#[test]
fn minimize_keeps_non_default_overrides() {
let mut cfg = Config::default();
cfg.cameras.push(CameraConfig {
name: "cam1".into(),
username: "admin".into(),
password: Some("pw".into()),
idle_disconnect: true,
..CameraConfig::default()
});
let out = minimize_render(&cfg).unwrap();
assert!(out.contains("idle_disconnect = true"));
}
}