Skip to main content

stmo_cli/commands/
discover.rs

1#![allow(clippy::missing_errors_doc)]
2
3use crate::api::RedashClient;
4use anyhow::Result;
5use std::path::Path;
6
7fn format_resource_line(id: u64, name: &str, url: &str, is_draft: bool) -> String {
8    let draft = if is_draft { " [DRAFT]" } else { "" };
9    format!("  {id} - {name}{draft}\n  {url}")
10}
11
12// discover never touches the filesystem to decide what to print — the caller
13// passes in what it already knows, so this stays trivially unit-testable.
14fn next_steps(queries_dir_exists: bool, first_query_id: Option<u64>) -> String {
15    if let Some(id) = first_query_id {
16        if queries_dir_exists {
17            format!("\nNext: stmo-cli fetch {id}")
18        } else {
19            format!(
20                "\nNo queries/ directory here.\nRun 'stmo-cli init <dir>' to set one up, \
21                 then 'stmo-cli fetch {id}'."
22            )
23        }
24    } else {
25        "\nNo queries of your own on Redash yet.\nTry 'stmo-cli discover --search <term>' \
26         to find other people's."
27            .to_string()
28    }
29}
30
31pub async fn discover(client: &RedashClient, search: Option<&str>, limit: usize) -> Result<()> {
32    if let Some(term) = search {
33        discover_by_search(client, term, limit).await
34    } else {
35        discover_own_queries(client).await
36    }
37}
38
39async fn discover_own_queries(client: &RedashClient) -> Result<()> {
40    println!("Fetching your queries from Redash...\n");
41
42    let queries = client.fetch_all_queries().await?;
43
44    println!("=== QUERIES ({}) ===\n", queries.len());
45    for query in &queries {
46        let archived = if query.is_archived { " [ARCHIVED]" } else { "" };
47        let draft = if query.is_draft { " [DRAFT]" } else { "" };
48        println!("  {} - {}{}{}", query.id, query.name, archived, draft);
49    }
50
51    println!(
52        "{}",
53        next_steps(Path::new("queries").exists(), queries.first().map(|q| q.id))
54    );
55
56    Ok(())
57}
58
59async fn discover_by_search(client: &RedashClient, term: &str, limit: usize) -> Result<()> {
60    println!("Searching for '{term}'...\n");
61
62    let (queries, dashboards) = tokio::join!(
63        client.search_queries(term, limit),
64        client.search_dashboards(term, limit),
65    );
66    let queries = queries?;
67    let dashboards = dashboards?;
68
69    let base = client.base_url();
70
71    println!("=== QUERIES ({}) ===\n", queries.len());
72    if queries.is_empty() {
73        println!("  No queries found for '{term}'.");
74    } else {
75        for query in &queries {
76            let url = format!("{base}/queries/{}", query.id);
77            println!(
78                "{}",
79                format_resource_line(query.id, &query.name, &url, query.is_draft)
80            );
81        }
82        if queries.len() == limit {
83            println!("\n  (showing first {limit}; raise --limit for more)");
84        }
85    }
86
87    println!("\n=== DASHBOARDS ({}) ===\n", dashboards.len());
88    if dashboards.is_empty() {
89        println!("  No dashboards found for '{term}'.");
90    } else {
91        for dashboard in &dashboards {
92            let url = format!("{base}/dashboard/{}", dashboard.slug);
93            println!(
94                "{}",
95                format_resource_line(dashboard.id, &dashboard.name, &url, dashboard.is_draft)
96            );
97        }
98        if dashboards.len() == limit {
99            println!("\n  (showing first {limit}; raise --limit for more)");
100        }
101    }
102
103    Ok(())
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn format_resource_line_plain() {
112        let line = format_resource_line(
113            42,
114            "My Dashboard",
115            "https://example.com/dashboard/my",
116            false,
117        );
118        assert!(line.contains("42"));
119        assert!(line.contains("My Dashboard"));
120        assert!(line.contains("https://example.com/dashboard/my"));
121        assert!(!line.contains("[DRAFT]"));
122    }
123
124    #[test]
125    fn format_resource_line_draft() {
126        let line = format_resource_line(7, "WIP", "https://example.com/dashboard/wip", true);
127        assert!(line.contains("[DRAFT]"));
128        assert!(line.contains("WIP"));
129    }
130
131    #[test]
132    fn next_steps_dir_exists_suggests_fetch() {
133        let message = next_steps(true, Some(123));
134        assert!(message.contains("fetch 123"));
135        assert!(!message.contains("init"));
136    }
137
138    #[test]
139    fn next_steps_dir_missing_suggests_init() {
140        let message = next_steps(false, Some(123));
141        assert!(message.contains("queries/"));
142        assert!(message.contains("stmo-cli init"));
143    }
144
145    #[test]
146    fn next_steps_no_queries_suggests_search() {
147        let message = next_steps(false, None);
148        assert!(message.contains("--search"));
149        assert!(!message.contains("stmo-cli init"));
150    }
151}