sofie_cli/
config_command.rs1use 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
12pub 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}