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            (
47                "default_sources",
48                serde_json::to_value(config.selected_sources())
49                    .expect("source names render as JSON"),
50            ),
51        ] {
52            let key = SettingPath::parse(key).expect("a literal path with no empty segment");
53            if !settings.iter().any(|setting| setting.key == key) {
54                settings.push(Setting {
55                    key,
56                    value,
57                    origin: Origin::Default,
58                });
59            }
60        }
61
62        settings.sort_by(|left, right| left.key.cmp(&right.key));
63        Self { settings, secrets }
64    }
65
66    /// The table a person reads, one setting per line.
67    ///
68    /// Values render as compact JSON rather than bare: this table's whole job is to
69    /// answer what a setting *is*, and a bare `50` beside a bare `"50"` would hide the
70    /// difference between a number a document set and a string an environment variable
71    /// spelled.
72    #[must_use]
73    pub fn render_text(&self) -> String {
74        let key_width = self
75            .settings
76            .iter()
77            .map(|setting| setting.key.to_string().chars().count())
78            .max()
79            .unwrap_or(0);
80        let values: Vec<String> = self
81            .settings
82            .iter()
83            .map(|setting| render_value(&setting.value))
84            .collect();
85        // Capped: one long list — a whole in-memory fixture, say — would otherwise
86        // push the layer column off the far side of a terminal for every other row.
87        // A value wider than the cap simply runs on, and its layer follows it.
88        let value_width = values
89            .iter()
90            .map(|value| value.chars().count())
91            .filter(|width| *width <= VALUE_COLUMN_CAP)
92            .max()
93            .unwrap_or(0);
94
95        let mut rendered = String::new();
96        for (setting, value) in self.settings.iter().zip(values) {
97            rendered.push_str(&format!(
98                "{:key_width$}  {:value_width$}  {}\n",
99                setting.key.to_string(),
100                value,
101                setting.origin
102            ));
103        }
104
105        rendered.push_str(&match self.secrets.path.as_ref() {
106            Some(path) => format!("\nsecrets file  {}\n", path.display()),
107            None => "\nsecrets file  none — neither XDG_CONFIG_HOME nor HOME is set\n".to_owned(),
108        });
109        if self.secrets.variables.is_empty() {
110            rendered.push_str("  (it defines no variables, or is not there)\n");
111        }
112        let name_width = self
113            .secrets
114            .variables
115            .iter()
116            .map(|credential| credential.variable.as_str().chars().count())
117            .max()
118            .unwrap_or(0);
119        for credential in &self.secrets.variables {
120            rendered.push_str(&format!(
121                "  {:name_width$}  resolved from the {}\n",
122                credential.variable, credential.resolved_from
123            ));
124        }
125        rendered
126    }
127}
128
129/// How wide the value column grows before a value is left to run on.
130const VALUE_COLUMN_CAP: usize = 44;
131
132/// One value as compact JSON.
133fn render_value(value: &Value) -> String {
134    serde_json::to_string(value).expect("a value that was deserialized from JSON re-renders")
135}