maestro-cli/src/config.rs was two pass-throughs, a newtype error, a two-field projection of MaestroConfigFile, and one four-line or_else with a single caller. `resolve_daemon_url` moves onto MaestroConfigFile, next to the schema it reads, and the module goes; its tests move with it. `[web].dev_proxy_target` is deleted end to end — schema, env var, normalisation, three tests, README line, and the startup warning that admitted the server ignores it. MaestroConfigFile::save had no callers outside its own tests. Deleting it also removes every skip_serializing_if, the five is_empty impls, and the Serialize derives that existed only to make its output tidy. BroadcastTuning was a struct that was never instantiated: all three uses were `::DEFAULT.field` in const position and there is no [broadcast] section. Now two plain consts. $MAESTRO_CONFIG was honoured twice, by resolve_config_path and by clap's `env =`. Every binary already gets it through clap, so resolve_config_path and CONFIG_PATH_ENV go and the TUI falls back to default_config_path like everything else. Also collapses three layers of 1:1 forwarding: the umbrella's cli_dispatch shim, and four near-identical 8-line functions in cli/dispatch.rs that differed only in which run_* they called. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
149 lines
4.4 KiB
Rust
149 lines
4.4 KiB
Rust
//! `maestro-web` view over the unified Maestro config.
|
|
|
|
use std::net::SocketAddr;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use maestro_config::MaestroConfigFile;
|
|
|
|
pub use maestro_config::default_config_path;
|
|
|
|
use maestro_config::WebSection;
|
|
|
|
/// Default bind address when neither env nor file specify one.
|
|
pub const DEFAULT_BIND_ADDR: &str = "127.0.0.1:8080";
|
|
/// Default daemon URL `maestro-web` reverse-proxies to.
|
|
pub const DEFAULT_DAEMON_URL: &str = "http://127.0.0.1:7070";
|
|
|
|
pub const ENV_WEB_BIND_ADDR: &str = "MAESTRO_WEB_BIND_ADDR";
|
|
pub const ENV_WEB_DAEMON_URL: &str = "MAESTRO_WEB_DAEMON_URL";
|
|
|
|
fn default_bind_addr() -> SocketAddr {
|
|
DEFAULT_BIND_ADDR.parse().expect("valid default bind_addr")
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum WebConfigError {
|
|
#[error(transparent)]
|
|
Inner(#[from] maestro_config::ConfigError),
|
|
}
|
|
|
|
/// Resolved runtime configuration for the `maestro-web` binary.
|
|
#[derive(Debug, Clone)]
|
|
pub struct WebConfig {
|
|
pub bind_addr: SocketAddr,
|
|
/// Daemon HTTP base URL — e.g. `http://127.0.0.1:7070`. Always a base
|
|
/// without trailing slash and without a path.
|
|
pub daemon_url: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct EnvOverrides {
|
|
pub bind_addr: Option<String>,
|
|
pub daemon_url: Option<String>,
|
|
}
|
|
|
|
impl EnvOverrides {
|
|
pub fn from_env() -> Self {
|
|
Self {
|
|
bind_addr: std::env::var(ENV_WEB_BIND_ADDR).ok(),
|
|
daemon_url: std::env::var(ENV_WEB_DAEMON_URL).ok(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resolve the runtime configuration. Missing-file is fine: every field has
|
|
/// a default and the binary is happy to run with no config.toml at all.
|
|
pub fn load_config(path: &Path, env: EnvOverrides) -> Result<WebConfig, WebConfigError> {
|
|
let file = MaestroConfigFile::load(path)?;
|
|
resolve_with(file.web, env)
|
|
}
|
|
|
|
fn resolve_with(web: WebSection, env: EnvOverrides) -> Result<WebConfig, WebConfigError> {
|
|
let bind_addr = match env.bind_addr {
|
|
Some(raw) => maestro_config::parse_bind_addr(&raw, ENV_WEB_BIND_ADDR)?,
|
|
None => web.bind_addr.unwrap_or_else(default_bind_addr),
|
|
};
|
|
|
|
let daemon_url = env
|
|
.daemon_url
|
|
.or(web.daemon_url)
|
|
.unwrap_or_else(|| DEFAULT_DAEMON_URL.to_string())
|
|
.trim_end_matches('/')
|
|
.to_string();
|
|
|
|
Ok(WebConfig {
|
|
bind_addr,
|
|
daemon_url,
|
|
})
|
|
}
|
|
|
|
/// Standard state dir (mostly so callers have a place to put logs — the
|
|
/// binary itself is stateless).
|
|
pub fn default_state_dir() -> Result<PathBuf, WebConfigError> {
|
|
Ok(maestro_config::default_state_dir("maestro-web")?)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn defaults_when_file_missing() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("does-not-exist.toml");
|
|
let cfg = load_config(&path, EnvOverrides::default()).unwrap();
|
|
assert_eq!(cfg.bind_addr, default_bind_addr());
|
|
assert_eq!(cfg.daemon_url, "http://127.0.0.1:7070");
|
|
}
|
|
|
|
#[test]
|
|
fn reads_web_section_from_file() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("config.toml");
|
|
std::fs::write(
|
|
&path,
|
|
r#"
|
|
[web]
|
|
bind_addr = "127.0.0.1:9090"
|
|
daemon_url = "http://127.0.0.1:9070"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let cfg = load_config(&path, EnvOverrides::default()).unwrap();
|
|
assert_eq!(cfg.bind_addr, "127.0.0.1:9090".parse().unwrap());
|
|
assert_eq!(cfg.daemon_url, "http://127.0.0.1:9070");
|
|
}
|
|
|
|
#[test]
|
|
fn env_beats_file() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("config.toml");
|
|
std::fs::write(
|
|
&path,
|
|
r#"
|
|
[web]
|
|
bind_addr = "127.0.0.1:9090"
|
|
daemon_url = "http://127.0.0.1:9070"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
let env = EnvOverrides {
|
|
bind_addr: Some("0.0.0.0:8080".into()),
|
|
daemon_url: Some("http://daemon:7070".into()),
|
|
};
|
|
let cfg = load_config(&path, env).unwrap();
|
|
assert_eq!(cfg.bind_addr, "0.0.0.0:8080".parse().unwrap());
|
|
assert_eq!(cfg.daemon_url, "http://daemon:7070");
|
|
}
|
|
|
|
#[test]
|
|
fn daemon_url_strips_trailing_slash() {
|
|
let env = EnvOverrides {
|
|
daemon_url: Some("http://daemon:7070/".into()),
|
|
..Default::default()
|
|
};
|
|
let cfg = resolve_with(WebSection::default(), env).unwrap();
|
|
assert_eq!(cfg.daemon_url, "http://daemon:7070");
|
|
}
|
|
}
|