Skip to main content

rover/cli/
config.rs

1//! `rover config show` (and `set` in Task 13).
2//!
3//! Renders the effective configuration as a TOML-flavoured listing where
4//! every leaf carries an inline `# <dotted-key>: from <source>` comment so
5//! `grep ssrf.level` against the output works.
6
7use anyhow::Context;
8
9use crate::config::{Config, default_config_path, provenance, resolve_existing_config_path};
10
11pub struct ShowArgs {
12    /// Optional config path. `None` resolves the active config file
13    /// (`ROVER_CONFIG`, platform config dir, then `./rover.toml`), falling back
14    /// to the canonical default path for display when none exists.
15    pub config_path: Option<std::path::PathBuf>,
16}
17
18pub fn show(args: ShowArgs) -> anyhow::Result<i32> {
19    // Read the same file the runtime would load. When none exists, fall back to
20    // the canonical default path so the header still points at where a config
21    // would live; the read below then yields an empty (defaults) view.
22    let path = args
23        .config_path
24        .or_else(resolve_existing_config_path)
25        .unwrap_or_else(default_config_path);
26    let file_text = std::fs::read_to_string(&path).unwrap_or_default();
27
28    // Validate the file parses cleanly. `show` shouldn't run against a broken
29    // file — surface the parse error to the user rather than printing garbage.
30    let cfg: Config = if file_text.is_empty() {
31        Config::default()
32    } else {
33        toml::from_str(&file_text).with_context(|| format!("parsing {}", path.display()))?
34    };
35    let rows = provenance::provenance_for(&file_text);
36    let effective = effective_values(&cfg);
37
38    println!("# rover effective configuration");
39    println!("# defaults | file ({}) | env", path.display());
40    println!();
41
42    // Group by top-level section.
43    let mut by_section: std::collections::BTreeMap<&str, Vec<&provenance::ProvenanceRow>> =
44        std::collections::BTreeMap::new();
45    for r in &rows {
46        let section = r.dotted.split_once('.').map(|(a, _)| a).unwrap_or("");
47        by_section.entry(section).or_default().push(r);
48    }
49    for (section, section_rows) in by_section {
50        if !section.is_empty() {
51            println!("[{section}]");
52        }
53        for r in section_rows {
54            let leaf = r
55                .dotted
56                .rsplit_once('.')
57                .map(|(_, b)| b)
58                .unwrap_or(&r.dotted);
59            let value = effective
60                .get(r.dotted.as_str())
61                .cloned()
62                .unwrap_or_else(|| "<unknown>".to_string());
63            let source = match r.source {
64                provenance::Source::Default => "defaults",
65                provenance::Source::File => "file",
66                provenance::Source::Env => "env",
67            };
68            // Include the full dotted key in the comment so a `grep <dotted>`
69            // against the output matches the right line.
70            println!(
71                "{leaf} = {value}  # from: {source} ({dotted})",
72                dotted = r.dotted,
73            );
74        }
75        println!();
76    }
77    Ok(0)
78}
79
80pub struct SetArgs {
81    pub config_path: Option<std::path::PathBuf>,
82    pub key: String,
83    pub value: String,
84}
85
86pub fn set(args: SetArgs) -> anyhow::Result<i32> {
87    // Modify the active config file when one already exists (so a set lands in
88    // the file the runtime reads); otherwise create the canonical default.
89    let path = args
90        .config_path
91        .or_else(resolve_existing_config_path)
92        .unwrap_or_else(default_config_path);
93    // Ensure parent dir exists so a first-time set creates the file cleanly.
94    if let Some(parent) = path.parent() {
95        std::fs::create_dir_all(parent)
96            .with_context(|| format!("creating parent dir {}", parent.display()))?;
97    }
98    // Touch the file if missing so apply_set's read succeeds.
99    if !path.exists() {
100        std::fs::write(&path, "").with_context(|| format!("creating {}", path.display()))?;
101    }
102    match crate::config::edit::apply_set(&path, &args.key, &args.value) {
103        Ok(()) => {
104            eprintln!(
105                "✓ {} = {}  (wrote {})",
106                args.key,
107                args.value,
108                path.display()
109            );
110            Ok(0)
111        }
112        Err(e) => {
113            eprintln!("error: {e}");
114            Ok(1)
115        }
116    }
117}
118
119/// Build a `dotted-key → TOML-formatted-string` map of effective values by
120/// serializing the loaded Config to a generic `toml::Value` and indexing each
121/// known leaf.
122fn effective_values(cfg: &Config) -> std::collections::HashMap<&'static str, String> {
123    let v = toml::Value::try_from(cfg).unwrap_or_else(|_| toml::Value::Table(Default::default()));
124    let mut out = std::collections::HashMap::new();
125    for dotted in provenance::known_leaves() {
126        if let Some(val) = lookup_dotted(&v, dotted) {
127            out.insert(*dotted, render_toml_value(&val));
128        }
129    }
130    out
131}
132
133fn lookup_dotted(v: &toml::Value, dotted: &str) -> Option<toml::Value> {
134    let mut cur = v.clone();
135    for part in dotted.split('.') {
136        let toml::Value::Table(t) = cur else {
137            return None;
138        };
139        cur = t.get(part)?.clone();
140    }
141    Some(cur)
142}
143
144fn render_toml_value(v: &toml::Value) -> String {
145    match v {
146        toml::Value::String(s) => format!("\"{s}\""),
147        toml::Value::Integer(n) => n.to_string(),
148        toml::Value::Float(f) => f.to_string(),
149        toml::Value::Boolean(b) => b.to_string(),
150        toml::Value::Array(_) | toml::Value::Table(_) => {
151            toml::to_string(v).unwrap_or_default().trim().to_string()
152        }
153        toml::Value::Datetime(d) => d.to_string(),
154    }
155}