feat: integrate configurable rules path throughout CLI

- Make parse_config_root() public for reuse in main.rs
- Add get_rules_path() helper to extract and resolve rules path from config
- Update main CLI to use configured rules path for default rule execution
- Add run_with_rules_path() method to RulesCli for custom path support
- Update all unit tests to include rules_dir field
- Rules path now supports h:, c:, r: prefixes throughout the application
This commit is contained in:
Jeremiah Russell
2025-10-21 17:52:23 +01:00
committed by Jeremiah Russell
parent 20b36a00ed
commit bcb93fd68f
4 changed files with 69 additions and 4 deletions

View File

@@ -60,7 +60,13 @@ use cull_gmail::{ClientConfig, Error, GmailClient, Result};
use lazy_regex::{Lazy, Regex, lazy_regex}; use lazy_regex::{Lazy, Regex, lazy_regex};
/// Parse configuration root path with h:, c:, r: prefixes. /// Parse configuration root path with h:, c:, r: prefixes.
fn parse_config_root(path_str: &str) -> PathBuf { ///
/// Supports:
/// - `h:path` - Relative to home directory
/// - `c:path` - Relative to current directory
/// - `r:path` - Relative to filesystem root
/// - `path` - Use path as-is
pub fn parse_config_root(path_str: &str) -> PathBuf {
static ROOT_CONFIG: Lazy<Regex> = lazy_regex!(r"^(?P<class>[hrc]):(?P<path>.+)$"); static ROOT_CONFIG: Lazy<Regex> = lazy_regex!(r"^(?P<class>[hrc]):(?P<path>.+)$");
if let Some(captures) = ROOT_CONFIG.captures(path_str) { if let Some(captures) = ROOT_CONFIG.captures(path_str) {

View File

@@ -72,6 +72,7 @@ mod unit_tests {
create_mock_credential_file(temp_dir.path()).unwrap(); create_mock_credential_file(temp_dir.path()).unwrap();
let init_cli = InitCli { let init_cli = InitCli {
rules_dir: None,
config_dir: "test".to_string(), config_dir: "test".to_string(),
credential_file: None, credential_file: None,
force: false, force: false,
@@ -88,6 +89,7 @@ mod unit_tests {
fn test_validate_credential_file_not_found() { fn test_validate_credential_file_not_found() {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let init_cli = InitCli { let init_cli = InitCli {
rules_dir: None,
config_dir: "test".to_string(), config_dir: "test".to_string(),
credential_file: None, credential_file: None,
force: false, force: false,
@@ -108,6 +110,7 @@ mod unit_tests {
fs::write(&credential_path, "invalid json content").unwrap(); fs::write(&credential_path, "invalid json content").unwrap();
let init_cli = InitCli { let init_cli = InitCli {
rules_dir: None,
config_dir: "test".to_string(), config_dir: "test".to_string(),
credential_file: None, credential_file: None,
force: false, force: false,
@@ -131,6 +134,7 @@ mod unit_tests {
let config_path = temp_dir.path().join("new-config"); let config_path = temp_dir.path().join("new-config");
let init_cli = InitCli { let init_cli = InitCli {
rules_dir: None,
config_dir: "test".to_string(), config_dir: "test".to_string(),
credential_file: None, credential_file: None,
force: false, force: false,
@@ -183,6 +187,7 @@ mod unit_tests {
fs::rename(temp_dir.path().join("credential.json"), &cred_path).unwrap(); fs::rename(temp_dir.path().join("credential.json"), &cred_path).unwrap();
let init_cli = InitCli { let init_cli = InitCli {
rules_dir: None,
config_dir: "test".to_string(), config_dir: "test".to_string(),
credential_file: None, credential_file: None,
force: false, force: false,
@@ -220,6 +225,7 @@ mod unit_tests {
fs::write(config_path.join("cull-gmail.toml"), "existing config").unwrap(); fs::write(config_path.join("cull-gmail.toml"), "existing config").unwrap();
let init_cli = InitCli { let init_cli = InitCli {
rules_dir: None,
config_dir: "test".to_string(), config_dir: "test".to_string(),
credential_file: None, credential_file: None,
force: false, force: false,
@@ -243,6 +249,7 @@ mod unit_tests {
fs::write(config_path.join("rules.toml"), "existing rules").unwrap(); fs::write(config_path.join("rules.toml"), "existing rules").unwrap();
let init_cli = InitCli { let init_cli = InitCli {
rules_dir: None,
config_dir: "test".to_string(), config_dir: "test".to_string(),
credential_file: None, credential_file: None,
force: true, force: true,
@@ -275,6 +282,7 @@ mod unit_tests {
fs::write(&test_file, "test content").unwrap(); fs::write(&test_file, "test content").unwrap();
let init_cli = InitCli { let init_cli = InitCli {
rules_dir: None,
config_dir: "test".to_string(), config_dir: "test".to_string(),
credential_file: None, credential_file: None,
force: false, force: false,
@@ -317,6 +325,7 @@ mod unit_tests {
fs::write(&test_file, "test content").unwrap(); fs::write(&test_file, "test content").unwrap();
let init_cli = InitCli { let init_cli = InitCli {
rules_dir: None,
config_dir: "test".to_string(), config_dir: "test".to_string(),
credential_file: None, credential_file: None,
force: false, force: false,

View File

@@ -127,6 +127,8 @@ use messages_cli::MessagesCli;
use rules_cli::RulesCli; use rules_cli::RulesCli;
use token_cli::{TokenCli, restore_tokens_from_string}; use token_cli::{TokenCli, restore_tokens_from_string};
use std::path::PathBuf;
/// Main CLI application structure defining global options and subcommands. /// Main CLI application structure defining global options and subcommands.
/// ///
/// This struct represents the root of the command-line interface, providing /// This struct represents the root of the command-line interface, providing
@@ -304,8 +306,11 @@ async fn run(args: Cli) -> Result<()> {
let mut client = GmailClient::new_with_config(client_config).await?; let mut client = GmailClient::new_with_config(client_config).await?;
// Get configured rules path
let rules_path = get_rules_path(&config)?;
let Some(sub_command) = args.sub_command else { let Some(sub_command) = args.sub_command else {
let rules = rules_cli::get_rules()?; let rules = rules_cli::get_rules_from(rules_path.as_deref())?;
let execute = config.get_bool("execute").unwrap_or(false); let execute = config.get_bool("execute").unwrap_or(false);
return run_rules(&mut client, rules, execute).await; return run_rules(&mut client, rules, execute).await;
}; };
@@ -317,7 +322,11 @@ async fn run(args: Cli) -> Result<()> {
} }
SubCmds::Message(messages_cli) => messages_cli.run(&mut client).await, SubCmds::Message(messages_cli) => messages_cli.run(&mut client).await,
SubCmds::Labels(labels_cli) => labels_cli.run(client).await, SubCmds::Labels(labels_cli) => labels_cli.run(client).await,
SubCmds::Rules(rules_cli) => rules_cli.run(&mut client).await, SubCmds::Rules(rules_cli) => {
rules_cli
.run_with_rules_path(&mut client, rules_path.as_deref())
.await
}
SubCmds::Token(token_cli) => { SubCmds::Token(token_cli) => {
// Token commands don't need an initialized client, just the config // Token commands don't need an initialized client, just the config
// We need to get a fresh client_config since the original was moved // We need to get a fresh client_config since the original was moved
@@ -530,6 +539,33 @@ async fn run_rules(client: &mut GmailClient, rules: Rules, execute: bool) -> Res
/// - Container deployments with injected token environment variables /// - Container deployments with injected token environment variables
/// - CI/CD pipelines with stored token secrets /// - CI/CD pipelines with stored token secrets
/// - Ephemeral compute environments requiring periodic Gmail access /// - Ephemeral compute environments requiring periodic Gmail access
/// Gets the rules file path from configuration.
///
/// Reads the `rules` configuration value and resolves it using path prefixes.
/// Supports h:, c:, r: prefixes for home, current, and root directories.
///
/// # Arguments
///
/// * `config` - Application configuration
///
/// # Returns
///
/// Returns the resolved rules file path, or None if using default location.
fn get_rules_path(config: &Config) -> Result<Option<PathBuf>> {
let rules_config = config
.get_string("rules")
.unwrap_or_else(|_| "rules.toml".to_string());
// If it's just "rules.toml" (the default), return None to use default location
if rules_config == "rules.toml" {
return Ok(None);
}
// Otherwise, parse the path with prefix support
let path = init_cli::parse_config_root(&rules_config);
Ok(Some(path))
}
fn restore_tokens_if_available(config: &Config, client_config: &ClientConfig) -> Result<()> { fn restore_tokens_if_available(config: &Config, client_config: &ClientConfig) -> Result<()> {
let token_env_var = config let token_env_var = config
.get_string("token_cache_env") .get_string("token_cache_env")

View File

@@ -263,7 +263,21 @@ impl RulesCli {
/// - **Error isolation**: Subcommand errors don't affect rule loading /// - **Error isolation**: Subcommand errors don't affect rule loading
/// - **State preservation**: Configuration errors don't corrupt existing rules /// - **State preservation**: Configuration errors don't corrupt existing rules
pub async fn run(&self, client: &mut GmailClient) -> Result<()> { pub async fn run(&self, client: &mut GmailClient) -> Result<()> {
let rules = get_rules()?; self.run_with_rules_path(client, None).await
}
/// Executes the rules command with an optional custom rules path.
///
/// # Arguments
///
/// * `client` - Mutable Gmail client for API operations
/// * `rules_path` - Optional path to rules file
pub async fn run_with_rules_path(
&self,
client: &mut GmailClient,
rules_path: Option<&Path>,
) -> Result<()> {
let rules = get_rules_from(rules_path)?;
match &self.sub_command { match &self.sub_command {
SubCmds::Config(config_cli) => config_cli.run(rules), SubCmds::Config(config_cli) => config_cli.run(rules),