//! Application configuration and run-profile validation. //! //! The [`Config`] type captures global settings such as which secret provider //! to use and Git sync preferences. The [`RunConfig`] type describes how to //! inject secrets when wrapping a command. //! //! Example: validate a minimal run profile //! ``` //! use gman::config::RunConfig; //! use validator::Validate; //! //! let rc = RunConfig{ //! name: Some("echo".into()), //! secrets: Some(vec!["api_key".into()]), //! files: None, //! flag: None, //! flag_position: None, //! arg_format: None, //! }; //! rc.validate().unwrap(); //! ``` use crate::providers::local::LocalProvider; use crate::providers::{SecretProvider, SupportedProvider}; use anyhow::Result; use log::debug; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use serde_with::{DisplayFromStr, skip_serializing_none}; use std::borrow::Cow; use std::path::PathBuf; use validator::{Validate, ValidationError}; #[skip_serializing_none] /// Describe how to inject secrets for a named command profile. /// /// A valid profile either defines no flag/file settings or provides a complete /// set of `flag`, `flag_position`, and `arg_format`. Additionally, the flag /// mode and the file‑injection mode are mutually exclusive. #[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)] #[validate(schema(function = "flags_or_none", skip_on_field_errors = false))] #[validate(schema(function = "flags_or_files"))] pub struct RunConfig { #[validate(required)] pub name: Option, #[validate(required)] pub secrets: Option>, pub files: Option>, pub flag: Option, #[validate(range(min = 1))] pub flag_position: Option, pub arg_format: Option, } fn flags_or_none(run_config: &RunConfig) -> Result<(), ValidationError> { match ( &run_config.flag, &run_config.flag_position, &run_config.arg_format, ) { (Some(_), Some(_), Some(format)) => { let has_key = format.contains("{{key}}"); let has_value = format.contains("{{value}}"); if has_key && has_value { Ok(()) } else { let mut err = ValidationError::new("missing_placeholders"); err.message = Some(Cow::Borrowed( "must contain both '{{key}}' and '{{value}}' (with the '{{' and '}}' characters) in the arg_format", )); err.add_param(Cow::Borrowed("has_key"), &has_key); err.add_param(Cow::Borrowed("has_value"), &has_value); Err(err) } } (None, None, None) => Ok(()), _ => { let mut err = ValidationError::new("both_or_none"); err.message = Some(Cow::Borrowed( "When defining a flag to pass secrets into the command with, all of 'flag', 'flag_position', and 'arg_format' must be defined in the run configuration", )); Err(err) } } } fn flags_or_files(run_config: &RunConfig) -> Result<(), ValidationError> { match (&run_config.flag, &run_config.files) { (Some(_), Some(_)) => { let mut err = ValidationError::new("flag_and_file"); err.message = Some(Cow::Borrowed( "Cannot specify both 'flag' and 'file' in the same run configuration", )); Err(err) } _ => Ok(()), } } #[serde_as] #[skip_serializing_none] /// Configuration for a secret provider. /// /// Example: create a local provider config and validate it /// ``` /// use gman::config::ProviderConfig; /// use gman::providers::SupportedProvider; /// use gman::providers::local::LocalProvider; /// use validator::Validate; /// /// let provider_type = SupportedProvider::Local(LocalProvider); /// let provider_config = ProviderConfig { provider_type, ..Default::default() }; /// provider_config.validate().unwrap(); /// ``` #[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)] pub struct ProviderConfig { #[validate(required)] pub name: Option, #[serde_as(as = "DisplayFromStr")] #[serde(rename(deserialize = "type"))] pub provider_type: SupportedProvider, pub password_file: Option, pub git_branch: Option, pub git_remote_url: Option, pub git_user_name: Option, #[validate(email)] pub git_user_email: Option, pub git_executable: Option, } impl Default for ProviderConfig { fn default() -> Self { Self { name: Some("local".into()), provider_type: SupportedProvider::Local(LocalProvider), password_file: Config::local_provider_password_file(), git_branch: Some("main".into()), git_remote_url: None, git_user_name: None, git_user_email: None, git_executable: None, } } } impl ProviderConfig { /// Instantiate the configured secret provider. /// /// ```no_run /// # use gman::config::ProviderConfig; /// let provider_config = ProviderConfig::default().extract_provider(); /// println!("using provider: {}", provider_config.name()); /// ``` pub fn extract_provider(&self) -> Box { match &self.provider_type { SupportedProvider::Local(p) => { debug!("Using local secret provider"); Box::new(*p) } } } } #[serde_as] #[skip_serializing_none] /// Global configuration for the library and CLI. /// /// Example: pick a provider and validate the configuration /// ``` /// use gman::config::Config; /// use gman::config::ProviderConfig; /// use gman::providers::SupportedProvider; /// use gman::providers::local::LocalProvider; /// use validator::Validate; /// /// let provider_type = SupportedProvider::Local(LocalProvider); /// let provider_config = ProviderConfig { provider_type, ..Default::default() }; /// let cfg = Config{ providers: vec![provider_config], ..Default::default() }; /// cfg.validate().unwrap(); /// ``` #[derive(Debug, Clone, Validate, Serialize, Deserialize, PartialEq, Eq)] #[validate(schema(function = "default_provider_exists"))] pub struct Config { pub default_provider: Option, #[validate(length(min = 1))] #[validate(nested)] pub providers: Vec, #[validate(nested)] pub run_configs: Option>, } fn default_provider_exists(config: &Config) -> Result<(), ValidationError> { if let Some(default) = &config.default_provider { if config .providers .iter() .any(|p| p.name.as_deref() == Some(default)) { Ok(()) } else { let mut err = ValidationError::new("default_provider_missing"); err.message = Some(Cow::Borrowed( "The default_provider does not match any configured provider names", )); Err(err) } } else { Ok(()) } } impl Default for Config { fn default() -> Self { Self { default_provider: Some("local".into()), providers: vec![ProviderConfig::default()], run_configs: None, } } } impl Config { /// Instantiate the configured secret provider. /// /// ```no_run /// # use gman::config::Config; /// let provider_config = Config::default().extract_provider_config(None).unwrap(); /// println!("using provider config: {:?}", provider_config.name); /// ``` pub fn extract_provider_config(&self, provider_name: Option) -> Result { let name = provider_name .or_else(|| self.default_provider.clone()) .unwrap_or_else(|| "local".into()); self.providers .iter() .find(|p| p.name.as_deref() == Some(&name)) .cloned() .ok_or_else(|| anyhow::anyhow!("No provider configuration found for '{}'", name)) } /// Discover the default password file for the local provider. /// /// On most systems this resolves to `~/.gman_password` when the file /// exists, otherwise `None`. pub fn local_provider_password_file() -> Option { let candidate = dirs::home_dir().map(|p| p.join(".gman_password")); match candidate { Some(p) if p.exists() => Some(p), _ => None, } } } pub fn load_config() -> Result { let mut config: Config = confy::load("gman", "config")?; config.validate()?; config .providers .iter_mut() .filter(|p| matches!(p.provider_type, SupportedProvider::Local(_))) .for_each(|p| { if p.password_file.is_none() && let Some(local_password_file) = Config::local_provider_password_file() { p.password_file = Some(local_password_file); } }); Ok(config) }