use std::path::Path;
use std::process::Command;
use anyhow::{anyhow, bail, Context, Result};
use crate::cli::LayerArg;
use crate::config::{Layer, LoadedConfig};
pub fn run(cfg: &LoadedConfig, name: &str, layer: Option<LayerArg>) -> Result<()> {
let path = resolve_path(cfg, name, layer)?;
open_editor(&path)
}
fn resolve_path(
cfg: &LoadedConfig,
name: &str,
layer: Option<LayerArg>,
) -> Result<std::path::PathBuf> {
if let Some(layer_arg) = layer {
let target_layer = layer_arg_to_layer(layer_arg);
let conn = cfg
.all_connections
.iter()
.find(|c| c.name == name && c.layer == target_layer)
.ok_or_else(|| {
anyhow!(
"no connection named '{name}' in the {} layer",
target_layer.label()
)
})?;
Ok(conn.source_path.clone())
} else {
let conn = cfg
.find(name)
.ok_or_else(|| anyhow!("no connection named '{name}'"))?;
Ok(conn.source_path.clone())
}
}
fn layer_arg_to_layer(arg: LayerArg) -> Layer {
match arg {
LayerArg::Project => Layer::Project,
LayerArg::User => Layer::User,
LayerArg::System => Layer::System,
}
}
fn open_editor(path: &Path) -> Result<()> {
let editor = std::env::var("EDITOR")
.or_else(|_| std::env::var("VISUAL"))
.unwrap_or_else(|_| "vi".to_string());
let status = Command::new(&editor)
.arg(path)
.status()
.with_context(|| format!("failed to launch editor '{editor}'"))?;
if !status.success() {
bail!(
"editor '{editor}' exited with status {}",
status.code().unwrap_or(-1)
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
use crate::config;
fn write_yaml(dir: &std::path::Path, name: &str, content: &str) {
fs::write(dir.join(name), content).unwrap();
}
fn simple_conn(name: &str, host: &str) -> String {
format!(
"connections:\n {name}:\n host: {host}\n user: u\n auth:\n type: key\n key: ~/.ssh/id_rsa\n description: d\n"
)
}
fn load(
cwd: &std::path::Path,
user: Option<&std::path::Path>,
sys: &std::path::Path,
) -> config::LoadedConfig {
config::load_impl(cwd, Some("connections"), false, user, sys).unwrap()
}
#[test]
fn test_resolve_path_no_layer_uses_active_source() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(&yconn, "connections.yaml", &simple_conn("srv", "10.0.0.1"));
let empty = TempDir::new().unwrap();
let cfg = load(root.path(), None, empty.path());
let path = resolve_path(&cfg, "srv", None).unwrap();
assert_eq!(path, yconn.join("connections.yaml"));
}
#[test]
fn test_resolve_path_with_layer_flag_finds_shadowed_entry() {
let root = TempDir::new().unwrap();
let yconn = root.path().join(".yconn");
fs::create_dir_all(&yconn).unwrap();
write_yaml(
&yconn,
"connections.yaml",
&simple_conn("srv", "project-host"),
);
let sys = TempDir::new().unwrap();
write_yaml(
sys.path(),
"connections.yaml",
&simple_conn("srv", "system-host"),
);
let cfg = load(root.path(), None, sys.path());
let p = resolve_path(&cfg, "srv", None).unwrap();
assert!(p.starts_with(&yconn));
let p2 = resolve_path(&cfg, "srv", Some(LayerArg::System)).unwrap();
assert!(p2.starts_with(sys.path()));
}
#[test]
fn test_resolve_path_unknown_name_returns_error() {
let cwd = TempDir::new().unwrap();
let empty = TempDir::new().unwrap();
let cfg = load(cwd.path(), None, empty.path());
let err = resolve_path(&cfg, "no-such", None).unwrap_err();
assert!(err.to_string().contains("no-such"));
}
#[test]
fn test_resolve_path_layer_flag_name_missing_in_layer() {
let cwd = TempDir::new().unwrap();
let user = TempDir::new().unwrap();
write_yaml(
user.path(),
"connections.yaml",
&simple_conn("srv", "1.2.3.4"),
);
let empty = TempDir::new().unwrap();
let cfg = load(cwd.path(), Some(user.path()), empty.path());
let err = resolve_path(&cfg, "srv", Some(LayerArg::Project)).unwrap_err();
assert!(err.to_string().contains("srv"));
assert!(err.to_string().contains("project"));
}
#[test]
fn test_layer_arg_to_layer_all_variants() {
assert!(matches!(
layer_arg_to_layer(LayerArg::System),
Layer::System
));
assert!(matches!(layer_arg_to_layer(LayerArg::User), Layer::User));
assert!(matches!(
layer_arg_to_layer(LayerArg::Project),
Layer::Project
));
}
}