diff --git a/src/cli.rs b/src/cli.rs index 58ff52d..1e5f4de 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -23,7 +23,7 @@ enum Commands { query: String, #[arg(long, help = "Filter by source id or label")] source: Option, - #[arg(long, help = "Filter by time range")] + #[arg(long, value_parser = crate::query::parse_time_range_arg, help = "Filter by time range")] time: Option, #[arg(long, help = "Filter by project directory, including child paths")] project: Option, @@ -62,14 +62,14 @@ enum Commands { json: bool, #[arg(long, help = "Filter by source id or label")] source: Option, - #[arg(long, help = "Filter by time range")] + #[arg(long, value_parser = crate::query::parse_time_range_arg, help = "Filter by time range")] time: Option, }, #[command(about = "Export session records as JSON Lines")] Export { #[arg(long, help = "Filter by source id or label")] source: Option, - #[arg(long, help = "Filter by time range")] + #[arg(long, value_parser = crate::query::parse_time_range_arg, help = "Filter by time range")] time: Option, #[arg(long, help = "Filter by project directory, including child paths")] project: Option, @@ -387,6 +387,27 @@ mod tests { assert!(Cli::try_parse_from(["recall", "export", "--jsonl"]).is_err()); } + #[test] + fn time_filter_validates_values_for_every_command() { + let commands: &[&[&str]] = &[ + &["recall", "search", "query", "--time", "definitely-invalid"], + &["recall", "session", "list", "--time", "definitely-invalid"], + &["recall", "usage", "--time", "definitely-invalid"], + &["recall", "export", "--time", "definitely-invalid"], + ]; + + for args in commands { + let Err(error) = Cli::try_parse_from(*args) else { + panic!("accepted invalid --time for {}", args[1]); + }; + assert_eq!(error.exit_code(), 2); + } + + for value in ["today", "7d", "week", "30d", "month", "all", "WEEK"] { + assert!(Cli::try_parse_from(["recall", "usage", "--time", value]).is_ok()); + } + } + #[test] fn info_accepts_json_format() { let cli = Cli::try_parse_from(["recall", "info", "--format", "json"]).unwrap(); diff --git a/src/export.rs b/src/export.rs index 00be013..6bb2982 100644 --- a/src/export.rs +++ b/src/export.rs @@ -48,13 +48,14 @@ pub(crate) fn run_cli( limit: usize, include_filter: Option<&str>, ) -> Result<()> { + let time_range = parse_time_range(time_filter)?; let store = Store::open()?; let sources = adapters::source_labels(); let scope = store.resolve_scope(project_filter, repo_filter)?.announce(); let options = ExportOptions { session_ids: Vec::new(), sources: resolve_source_filter(source_filter, &sources)?, - time_range: parse_time_range(time_filter), + time_range, scope, thread_role, limit: if limit == 0 { None } else { Some(limit) }, diff --git a/src/query.rs b/src/query.rs index d81802a..7ab38ce 100644 --- a/src/query.rs +++ b/src/query.rs @@ -39,11 +39,11 @@ pub(crate) fn run_search( ); } + let time_range = parse_time_range(time_filter)?; let store = Store::open()?; let engine = SearchEngine::new(&store.conn); let sources = adapters::source_labels(); let resolved_source = resolve_source_filter(source_filter, &sources)?; - let time_range = parse_time_range(time_filter); let scope = store.resolve_scope(project_filter, repo_filter)?.announce(); let embedding = query_embedding(&store, query, |message| println!("{message}"))?; @@ -98,12 +98,22 @@ pub(crate) fn resolve_source_filter( Ok(Some(vec![resolved])) } -pub(crate) fn parse_time_range(time_filter: Option<&str>) -> TimeRange { - match time_filter.map(|t| t.to_lowercase()) { - Some(ref t) if t == "today" => TimeRange::Today, - Some(ref t) if t == "7d" || t == "week" => TimeRange::Week, - Some(ref t) if t == "30d" || t == "month" => TimeRange::Month, - _ => TimeRange::All, +pub(crate) fn parse_time_range_arg(value: &str) -> std::result::Result { + parse_time_range(Some(value)).map(|_| value.to_owned()).map_err(|error| error.to_string()) +} + +pub(crate) fn parse_time_range(time_filter: Option<&str>) -> Result { + let Some(value) = time_filter else { + return Ok(TimeRange::All); + }; + match value.to_lowercase().as_str() { + "today" => Ok(TimeRange::Today), + "7d" | "week" => Ok(TimeRange::Week), + "30d" | "month" => Ok(TimeRange::Month), + "all" => Ok(TimeRange::All), + _ => anyhow::bail!( + "unknown time range: {value}; expected today, 7d, week, 30d, month, or all" + ), } } diff --git a/src/session.rs b/src/session.rs index 1f7e2eb..1940ac2 100644 --- a/src/session.rs +++ b/src/session.rs @@ -24,7 +24,7 @@ pub(crate) enum SessionCommands { query: Option, #[arg(long, help = "Filter by source id or label")] source: Option, - #[arg(long, help = "Filter by time range")] + #[arg(long, value_parser = crate::query::parse_time_range_arg, help = "Filter by time range")] time: Option, #[arg(long, help = "Filter by project directory, including child paths")] project: Option, @@ -326,7 +326,7 @@ pub(crate) fn run_session_list( let sources = adapters::source_labels(); let resolved_source = resolve_source_filter(source_filter, &sources)?; - let time_range = parse_time_range(time_filter); + let time_range = parse_time_range(time_filter)?; let store = Store::open()?; let scope = store.resolve_scope(project_filter, repo_filter)?.announce(); diff --git a/src/usage.rs b/src/usage.rs index cb9f086..c56d97c 100644 --- a/src/usage.rs +++ b/src/usage.rs @@ -23,20 +23,19 @@ pub(crate) fn run_cli( source_filter: Option<&str>, time_filter: Option<&str>, ) -> Result<()> { + let time_range = parse_time_range(time_filter)?; let sources = usage_source_labels(); if !json { let usage_source_filter = resolve_source_filter(source_filter, &sources)?; - let usage_time_filter = time_filter.map(|_| parse_time_range(time_filter)); + let usage_time_filter = time_filter.map(|_| time_range); return crate::tui::runner::run(Some((usage_source_filter, usage_time_filter))); } run_usage_sync_job()?; let store = Store::open()?; - let filters = UsageFilters { - sources: resolve_source_filter(source_filter, &sources)?, - time_range: parse_time_range(time_filter), - }; + let filters = + UsageFilters { sources: resolve_source_filter(source_filter, &sources)?, time_range }; let report = build_usage_report(&store, &filters)?; println!("{}", serde_json::to_string_pretty(&report)?);