Skip to main content

sofie_cli/
config_command.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use sofie_check::{Finding, Rule};
6use sofie_model::{App, ConfigKey};
7
8use crate::dev_command::env_entries;
9use crate::env_example::{framework_keys, module_keys, remote_keys};
10use crate::gen_command::{GenError, io_error};
11
12/// Values are never repeated, as they may be secrets.
13pub fn check_config(root: &Path, environment: &str) -> Result<Vec<Finding>, GenError> {
14    let app = sofie_model::load(root).map_err(GenError::Model)?;
15    let file = root.join(format!(".env.{environment}"));
16    let text = fs::read_to_string(&file).map_err(|source| io_error(&file, source))?;
17    let values = EnvironmentFile::parse(&file, &text);
18    Ok(app_keys(&app)
19        .iter()
20        .filter_map(|key| values.problem(key))
21        .collect())
22}
23
24fn app_keys(app: &App) -> Vec<ConfigKey> {
25    let modules = app.modules.iter().flat_map(module_keys);
26    let remote = app
27        .split
28        .remote
29        .iter()
30        .flat_map(|remote| remote_keys(&remote.name));
31    framework_keys(app)
32        .into_iter()
33        .chain(modules)
34        .chain(remote)
35        .collect()
36}
37
38struct EnvironmentFile {
39    file: PathBuf,
40    values: BTreeMap<String, (String, usize)>,
41}
42
43impl EnvironmentFile {
44    fn parse(file: &Path, text: &str) -> Self {
45        let values = env_entries(text)
46            .map(|(line, key, value)| (key.to_owned(), (value.to_owned(), line)))
47            .collect();
48        Self {
49            file: file.to_path_buf(),
50            values,
51        }
52    }
53
54    fn problem(&self, key: &ConfigKey) -> Option<Finding> {
55        let Some((value, line)) = self.values.get(&key.name) else {
56            return self.missing(key);
57        };
58        if key.kind.accepts(value) {
59            return None;
60        }
61        Some(self.finding(
62            *line,
63            format!("{} is not {}", key.name, key.kind.description()),
64            format!("set {} to {}", key.name, key.kind.description()),
65        ))
66    }
67
68    fn missing(&self, key: &ConfigKey) -> Option<Finding> {
69        let inherited = key
70            .fallback
71            .as_ref()
72            .is_some_and(|fallback| self.values.contains_key(fallback));
73        if !key.required || inherited {
74            return None;
75        }
76        let documented = key
77            .doc
78            .first()
79            .map_or(String::new(), |doc| format!(": {doc}"));
80        Some(self.finding(
81            1,
82            format!(
83                "{} is not set, and the app cannot start without it",
84                key.name
85            ),
86            format!("add `{}=…`{documented}", key.name),
87        ))
88    }
89
90    fn finding(&self, line: usize, message: String, fix: String) -> Finding {
91        Finding {
92            rule: Rule::Config,
93            file: self.file.clone(),
94            line,
95            message,
96            fix,
97        }
98    }
99}