Skip to main content

onetaskgraph_core/config/
effective.rs

1//! The configuration as it ended up, with the layer each setting came from.
2//!
3//! This is what makes precedence something a user can see rather than something the
4//! tests alone know. It is built from the merge itself, not reconstructed from the
5//! final [`Config`], so it cannot claim a layer the merge did not actually take the
6//! value from.
7
8use schemars::JsonSchema;
9use serde::Serialize;
10use serde_json::Value;
11
12use crate::secrets::SecretsReport;
13
14use super::Config;
15use super::layer::{Merged, Origin, Setting, SettingPath};
16
17/// Every setting this build reads, with its value and where the value came from.
18#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
19pub struct EffectiveConfig {
20    /// Every setting, in key order.
21    pub settings: Vec<Setting>,
22    /// What the credentials file supplied, by name, and which layer answers for each.
23    ///
24    /// Beside the settings because it is the same question — where does this value
25    /// come from — asked of the one kind of value that may never be printed. So the
26    /// name and the layer are reported and the value never is.
27    pub secrets: SecretsReport,
28}
29
30impl EffectiveConfig {
31    /// Combine what the layers set with the built-in values for what they did not.
32    ///
33    /// A setting nothing set still appears, carrying [`Origin::Default`] and the value
34    /// the run will actually use — the point of the verb is to answer "what is this
35    /// command going to do", and a silent omission answers it wrongly.
36    #[must_use]
37    pub fn new(merged: &Merged, config: &Config, secrets: SecretsReport) -> Self {
38        let mut settings: Vec<Setting> = merged.values().cloned().collect();
39
40        for (key, value) in [
41            ("page_size", Value::from(config.page_size().get())),
42            (
43                "output",
44                serde_json::to_value(config.output()).expect("an output format renders as JSON"),
45            ),
46            ("interactive", Value::Bool(config.interactive())),
47            (
48                "default_sources",
49                serde_json::to_value(config.selected_sources())
50                    .expect("source names render as JSON"),
51            ),
52        ] {
53            let key = SettingPath::parse(key).expect("a literal path with no empty segment");
54            if !settings.iter().any(|setting| setting.key == key) {
55                settings.push(Setting {
56                    key,
57                    value,
58                    origin: Origin::Default,
59                });
60            }
61        }
62
63        settings.sort_by(|left, right| left.key.cmp(&right.key));
64        Self { settings, secrets }
65    }
66
67    /// The table a person reads, one setting per line.
68    ///
69    /// Values render as compact JSON rather than bare: this table's whole job is to
70    /// answer what a setting *is*, and a bare `50` beside a bare `"50"` would hide the
71    /// difference between a number a document set and a string an environment variable
72    /// spelled.
73    #[must_use]
74    pub fn render_text(&self) -> String {
75        let key_width = self
76            .settings
77            .iter()
78            .map(|setting| setting.key.to_string().chars().count())
79            .max()
80            .unwrap_or(0);
81        let values: Vec<String> = self
82            .settings
83            .iter()
84            .map(|setting| render_value(&setting.value))
85            .collect();
86        // Capped: one long list — a whole in-memory fixture, say — would otherwise
87        // push the layer column off the far side of a terminal for every other row.
88        // A value wider than the cap simply runs on, and its layer follows it.
89        let value_width = values
90            .iter()
91            .map(|value| value.chars().count())
92            .filter(|width| *width <= VALUE_COLUMN_CAP)
93            .max()
94            .unwrap_or(0);
95
96        let mut rendered = String::new();
97        for (setting, value) in self.settings.iter().zip(values) {
98            rendered.push_str(&format!(
99                "{:key_width$}  {:value_width$}  {}\n",
100                setting.key.to_string(),
101                value,
102                setting.origin
103            ));
104        }
105
106        rendered.push_str(&match self.secrets.path.as_ref() {
107            Some(path) => format!("\nsecrets file  {}\n", path.display()),
108            None => "\nsecrets file  none — neither XDG_CONFIG_HOME nor HOME is set\n".to_owned(),
109        });
110        if self.secrets.variables.is_empty() {
111            rendered.push_str("  (it defines no variables, or is not there)\n");
112        }
113        let name_width = self
114            .secrets
115            .variables
116            .iter()
117            .map(|credential| credential.variable.as_str().chars().count())
118            .max()
119            .unwrap_or(0);
120        for credential in &self.secrets.variables {
121            rendered.push_str(&format!(
122                "  {:name_width$}  resolved from the {}\n",
123                credential.variable, credential.resolved_from
124            ));
125        }
126        rendered
127    }
128}
129
130/// How wide the value column grows before a value is left to run on.
131const VALUE_COLUMN_CAP: usize = 44;
132
133/// One value as compact JSON.
134fn render_value(value: &Value) -> String {
135    serde_json::to_string(value).expect("a value that was deserialized from JSON re-renders")
136}