use clap::Args;
use std::path::Path;
use std::path::PathBuf;
use homeboy::component::{self, Component};
use homeboy::error::ErrorCode;
#[derive(Args, Debug, Clone, Default)]
pub struct ComponentArgs {
#[arg(short, long)]
pub component: Option<String>,
#[arg(long)]
pub path: Option<String>,
}
#[allow(dead_code)]
impl ComponentArgs {
pub fn resolve(&self) -> homeboy::Result<Component> {
let mut comp = component::resolve(self.component.as_deref())?;
if let Some(ref path) = self.path {
comp.local_path = path.clone();
}
Ok(comp)
}
pub fn resolve_root(&self) -> homeboy::Result<PathBuf> {
if let Some(ref p) = self.path {
Ok(PathBuf::from(p))
} else {
let comp = component::resolve(self.component.as_deref())?;
component::validate_local_path(&comp)
}
}
pub fn load(&self) -> homeboy::Result<Component> {
let id = self.component.as_deref().ok_or_else(|| {
homeboy::Error::validation_missing_argument(vec!["component".to_string()])
})?;
let mut comp = component::load(id)?;
if let Some(ref path) = self.path {
comp.local_path = path.clone();
}
Ok(comp)
}
}
#[derive(Args, Debug, Clone)]
pub struct PositionalComponentArgs {
pub component: String,
#[arg(long)]
pub path: Option<String>,
}
impl PositionalComponentArgs {
pub fn load(&self) -> homeboy::Result<Component> {
if let Some(ref path) = self.path {
match component::load(&self.component) {
Ok(mut comp) => {
comp.local_path = path.clone();
Ok(comp)
}
Err(err) if matches!(err.code, ErrorCode::ComponentNotFound) => {
if let Some(mut discovered) = component::discover_from_portable(Path::new(path))
{
discovered.id = self.component.clone();
discovered.local_path = path.clone();
Ok(discovered)
} else {
Ok(Component::new(
self.component.clone(),
path.clone(),
String::new(),
None,
))
}
}
Err(err) => Err(err),
}
} else {
component::load(&self.component)
}
}
pub fn id(&self) -> &str {
&self.component
}
pub fn source_path(&self) -> homeboy::Result<PathBuf> {
if let Some(ref path) = self.path {
Ok(PathBuf::from(path))
} else {
let comp = component::load(&self.component)?;
let expanded = shellexpand::tilde(&comp.local_path);
Ok(PathBuf::from(expanded.as_ref()))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_uses_path_when_component_missing() {
let args = PositionalComponentArgs {
component: "missing-component".to_string(),
path: Some("/tmp/homeboy-missing-component".to_string()),
};
let loaded = args
.load()
.expect("path-based synthetic component should load");
assert_eq!(loaded.id, "missing-component");
assert_eq!(loaded.local_path, "/tmp/homeboy-missing-component");
assert_eq!(loaded.remote_path, "");
}
#[test]
fn load_prefers_portable_config_when_path_contains_homeboy_json() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("homeboy.json"),
r#"{
"extensions": {
"wordpress": {}
},
"changelog_target": "docs/CHANGELOG.md"
}"#,
)
.unwrap();
let args = PositionalComponentArgs {
component: "data-machine".to_string(),
path: Some(dir.path().to_string_lossy().to_string()),
};
let loaded = args
.load()
.expect("portable config should seed synthetic component");
assert_eq!(loaded.id, "data-machine");
assert_eq!(loaded.local_path, dir.path().to_string_lossy());
assert!(loaded.extensions.is_some());
assert!(loaded
.extensions
.as_ref()
.unwrap()
.contains_key("wordpress"));
assert_eq!(
loaded.changelog_target.as_deref(),
Some("docs/CHANGELOG.md")
);
}
}
#[derive(Args, Debug, Clone, Default)]
pub struct BaselineArgs {
#[arg(long)]
pub baseline: bool,
#[arg(long)]
pub ignore_baseline: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct WriteModeArgs {
#[arg(long)]
pub write: bool,
}
#[allow(dead_code)]
impl WriteModeArgs {
pub fn is_dry_run(&self) -> bool {
!self.write
}
}
#[derive(Args, Debug, Clone, Default)]
pub struct DryRunArgs {
#[arg(long)]
pub dry_run: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct HiddenJsonArgs {
#[arg(long, hide = true)]
pub json: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct SettingArgs {
#[arg(long, value_parser = super::parse_key_val)]
pub setting: Vec<(String, String)>,
}