✨ feat: add configurable rules directory support to Rules and InitCli
- Add Rules::load_from() and Rules::save_to() methods for custom paths - Add --rules-dir option to init command - Support h:, c:, r: path prefixes for rules directory - Generate config file with correct rules path when using separate directory - Create rules directory automatically if it doesn't exist - Maintain backward compatibility with default ~/.cull-gmail/rules.toml
This commit is contained in:
committed by
Jeremiah Russell
parent
df9d2b6c8a
commit
2083c5c5fe
@@ -146,6 +146,23 @@ pub struct InitCli {
|
||||
)]
|
||||
pub credential_file: Option<PathBuf>,
|
||||
|
||||
/// Rules file directory path.
|
||||
///
|
||||
/// Optionally specify a separate directory for the rules.toml file.
|
||||
/// If not provided, rules.toml will be created in the configuration directory.
|
||||
/// Supports the same path prefixes as --config-dir (h:, c:, r:).
|
||||
///
|
||||
/// This is useful for:
|
||||
/// - Version controlling rules separately from credentials
|
||||
/// - Sharing rules across multiple configurations
|
||||
/// - Organizing files by security sensitivity
|
||||
#[arg(
|
||||
long = "rules-dir",
|
||||
value_name = "DIR",
|
||||
help = "Optional separate directory for rules.toml file"
|
||||
)]
|
||||
pub rules_dir: Option<String>,
|
||||
|
||||
/// Overwrite existing files without prompting.
|
||||
///
|
||||
/// When enabled, existing configuration files will be backed up with
|
||||
@@ -292,6 +309,31 @@ execute = false
|
||||
token_cache_env = "CULL_GMAIL_TOKEN_CACHE"
|
||||
"#;
|
||||
|
||||
/// Generate config file content with custom rules path.
|
||||
fn config_content_with_rules_path(rules_path: &str) -> String {
|
||||
format!(
|
||||
r#"# cull-gmail configuration
|
||||
# This file configures the cull-gmail application.
|
||||
|
||||
# OAuth2 credential file (relative to config_root)
|
||||
credential_file = "credential.json"
|
||||
|
||||
# Configuration root directory
|
||||
config_root = "h:.cull-gmail"
|
||||
|
||||
# Rules configuration file (supports h:, c:, r: prefixes)
|
||||
rules = "{rules_path}"
|
||||
|
||||
# Default execution mode (false = dry-run, true = execute)
|
||||
# Set to false for safety - you can override with --execute flag
|
||||
execute = false
|
||||
|
||||
# Environment variable name for token cache (for ephemeral environments)
|
||||
token_cache_env = "CULL_GMAIL_TOKEN_CACHE"
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
const RULES_FILE_CONTENT: &'static str = r#"# Example rules for cull-gmail
|
||||
# Each rule targets a Gmail label and specifies an action.
|
||||
#
|
||||
@@ -473,16 +515,19 @@ impl InitCli {
|
||||
self.plan_credential_file_operation(&mut operations, config_path, cred_file)?;
|
||||
}
|
||||
|
||||
// 3. Write config file
|
||||
self.plan_config_file_operation(&mut operations, config_path)?;
|
||||
// 3. Determine rules directory
|
||||
let rules_dir = self.get_rules_directory(config_path);
|
||||
|
||||
// 4. Write rules file
|
||||
self.plan_rules_file_operation(&mut operations, config_path)?;
|
||||
// 4. Write config file (with correct rules path)
|
||||
self.plan_config_file_operation(&mut operations, config_path, &rules_dir)?;
|
||||
|
||||
// 5. Ensure token directory exists
|
||||
// 5. Write rules file (possibly in separate directory)
|
||||
self.plan_rules_file_operation(&mut operations, &rules_dir)?;
|
||||
|
||||
// 6. Ensure token directory exists
|
||||
self.plan_token_directory(&mut operations, config_path);
|
||||
|
||||
// 6. Run OAuth2 if we have credentials
|
||||
// 7. Run OAuth2 if we have credentials
|
||||
if credential_file.is_some() {
|
||||
self.plan_oauth_operation(&mut operations);
|
||||
}
|
||||
@@ -490,6 +535,19 @@ impl InitCli {
|
||||
Ok(operations)
|
||||
}
|
||||
|
||||
/// Get the directory where rules.toml should be placed.
|
||||
///
|
||||
/// Returns the rules directory path, which is either:
|
||||
/// - The custom directory specified with --rules-dir
|
||||
/// - The config directory (default)
|
||||
fn get_rules_directory(&self, config_path: &Path) -> PathBuf {
|
||||
if let Some(ref rules_dir_str) = self.rules_dir {
|
||||
parse_config_root(rules_dir_str)
|
||||
} else {
|
||||
config_path.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
/// Plan config directory creation.
|
||||
fn plan_config_directory(&self, operations: &mut Vec<Operation>, config_path: &Path) {
|
||||
if !config_path.exists() {
|
||||
@@ -526,13 +584,26 @@ impl InitCli {
|
||||
&self,
|
||||
operations: &mut Vec<Operation>,
|
||||
config_path: &Path,
|
||||
rules_dir: &Path,
|
||||
) -> Result<()> {
|
||||
let config_file_path = config_path.join(InitDefaults::config_filename());
|
||||
self.check_file_conflicts(&config_file_path, "Configuration file")?;
|
||||
|
||||
// Generate rules path for config file
|
||||
let rules_path = rules_dir.join(InitDefaults::rules_filename());
|
||||
let rules_path_str = rules_path.to_string_lossy().to_string();
|
||||
|
||||
let config_contents = if rules_dir == config_path {
|
||||
// Rules in same directory - use relative path
|
||||
InitDefaults::CONFIG_FILE_CONTENT.to_string()
|
||||
} else {
|
||||
// Rules in different directory - use full path
|
||||
InitDefaults::config_content_with_rules_path(&rules_path_str)
|
||||
};
|
||||
|
||||
operations.push(Operation::WriteFile {
|
||||
path: config_file_path.clone(),
|
||||
contents: InitDefaults::CONFIG_FILE_CONTENT.to_string(),
|
||||
contents: config_contents,
|
||||
#[cfg(unix)]
|
||||
mode: Some(0o644),
|
||||
backup_if_exists: self.should_backup(&config_file_path),
|
||||
@@ -544,9 +615,18 @@ impl InitCli {
|
||||
fn plan_rules_file_operation(
|
||||
&self,
|
||||
operations: &mut Vec<Operation>,
|
||||
config_path: &Path,
|
||||
rules_dir: &Path,
|
||||
) -> Result<()> {
|
||||
let rules_file_path = config_path.join(InitDefaults::rules_filename());
|
||||
// Create rules directory if it doesn't exist and is different from config dir
|
||||
if !rules_dir.exists() {
|
||||
operations.push(Operation::CreateDir {
|
||||
path: rules_dir.to_path_buf(),
|
||||
#[cfg(unix)]
|
||||
mode: Some(0o755),
|
||||
});
|
||||
}
|
||||
|
||||
let rules_file_path = rules_dir.join(InitDefaults::rules_filename());
|
||||
self.check_file_conflicts(&rules_file_path, "Rules file")?;
|
||||
|
||||
operations.push(Operation::WriteFile {
|
||||
|
||||
81
src/rules.rs
81
src/rules.rs
@@ -45,7 +45,7 @@ use std::{
|
||||
collections::BTreeMap,
|
||||
env,
|
||||
fs::{self, read_to_string},
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -492,12 +492,45 @@ impl Rules {
|
||||
/// * IO errors when writing to the file system
|
||||
/// * File system permission errors
|
||||
pub fn save(&self) -> Result<()> {
|
||||
self.save_to(None)
|
||||
}
|
||||
|
||||
/// Saves the current rule configuration to a specified path.
|
||||
///
|
||||
/// If no path is provided, defaults to `~/.cull-gmail/rules.toml`.
|
||||
/// The directory is created if it doesn't exist.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Optional path where the rules should be saved
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// use cull_gmail::Rules;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let rules = Rules::new();
|
||||
/// rules.save_to(Some(Path::new("/custom/path/rules.toml")))
|
||||
/// .expect("Failed to save");
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * TOML serialization errors
|
||||
/// * IO errors when writing to the file system
|
||||
/// * File system permission errors
|
||||
pub fn save_to(&self, path: Option<&Path>) -> Result<()> {
|
||||
let save_path = if let Some(p) = path {
|
||||
p.to_path_buf()
|
||||
} else {
|
||||
let home_dir = env::home_dir()
|
||||
.ok_or_else(|| Error::HomeExpansionFailed("~/.cull-gmail/rules.toml".to_string()))?;
|
||||
let path = PathBuf::new().join(home_dir).join(".cull-gmail/rules.toml");
|
||||
home_dir.join(".cull-gmail/rules.toml")
|
||||
};
|
||||
|
||||
// Ensure directory exists
|
||||
if let Some(parent) = path.parent() {
|
||||
if let Some(parent) = save_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
@@ -505,8 +538,8 @@ impl Rules {
|
||||
log::trace!("toml conversion result: {res:#?}");
|
||||
|
||||
if let Ok(output) = res {
|
||||
fs::write(&path, output)?;
|
||||
log::trace!("Config saved to {}", path.display());
|
||||
fs::write(&save_path, output)?;
|
||||
log::trace!("Config saved to {}", save_path.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -537,12 +570,44 @@ impl Rules {
|
||||
/// * TOML parsing errors if the file is malformed
|
||||
/// * File not found errors if the configuration doesn't exist
|
||||
pub fn load() -> Result<Rules> {
|
||||
Self::load_from(None)
|
||||
}
|
||||
|
||||
/// Loads rule configuration from a specified path.
|
||||
///
|
||||
/// If no path is provided, defaults to `~/.cull-gmail/rules.toml`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Optional path to load rules from
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// use cull_gmail::Rules;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let rules = Rules::load_from(Some(Path::new("/custom/path/rules.toml")))
|
||||
/// .expect("Failed to load rules");
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * IO errors when reading from the file system
|
||||
/// * TOML parsing errors if the file is malformed
|
||||
/// * File not found errors if the configuration doesn't exist
|
||||
pub fn load_from(path: Option<&Path>) -> Result<Rules> {
|
||||
let load_path = if let Some(p) = path {
|
||||
p.to_path_buf()
|
||||
} else {
|
||||
let home_dir = env::home_dir()
|
||||
.ok_or_else(|| Error::HomeExpansionFailed("~/.cull-gmail/rules.toml".to_string()))?;
|
||||
let path = PathBuf::new().join(home_dir).join(".cull-gmail/rules.toml");
|
||||
log::trace!("Loading config from {}", path.display());
|
||||
home_dir.join(".cull-gmail/rules.toml")
|
||||
};
|
||||
|
||||
let input = read_to_string(path)?;
|
||||
log::trace!("Loading config from {}", load_path.display());
|
||||
|
||||
let input = read_to_string(load_path)?;
|
||||
let config = toml::from_str::<Rules>(&input)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user