1use anyhow::Result;
2use std::borrow::Cow;
3
4use super::error::{ConfigError, ConfigResult};
5use super::types::{Config, ConfigSection, ConfigSource};
6
7struct SectionSpec {
8 title: &'static str,
9 entries: &'static [EntrySpec],
10}
11
12struct EntrySpec {
13 key: &'static str,
14 full_key: &'static str,
15 value: for<'a> fn(&'a Config) -> Cow<'a, str>,
16}
17
18const SECTION_SPECS: &[SectionSpec] = &[
19 SectionSpec {
20 title: "Core",
21 entries: &[
22 EntrySpec {
23 key: "log_level",
24 full_key: "core.log_level",
25 value: core_log_level,
26 },
27 EntrySpec {
28 key: "file_log_level",
29 full_key: "core.file_log_level",
30 value: core_file_log_level,
31 },
32 EntrySpec {
33 key: "auto_update",
34 full_key: "core.auto_update",
35 value: core_auto_update,
36 },
37 EntrySpec {
38 key: "confirm_remove",
39 full_key: "core.confirm_remove",
40 value: core_confirm_remove,
41 },
42 EntrySpec {
43 key: "default_yes",
44 full_key: "core.default_yes",
45 value: core_default_yes,
46 },
47 EntrySpec {
48 key: "color",
49 full_key: "core.color",
50 value: core_color,
51 },
52 ],
53 },
54 SectionSpec {
55 title: "Paths",
56 entries: &[
57 EntrySpec {
58 key: "root",
59 full_key: "paths.root",
60 value: paths_root,
61 },
62 EntrySpec {
63 key: "packages",
64 full_key: "paths.packages",
65 value: paths_packages,
66 },
67 EntrySpec {
68 key: "data",
69 full_key: "paths.data",
70 value: paths_data,
71 },
72 EntrySpec {
73 key: "logs",
74 full_key: "paths.logs",
75 value: paths_logs,
76 },
77 EntrySpec {
78 key: "cache",
79 full_key: "paths.cache",
80 value: paths_cache,
81 },
82 ],
83 },
84];
85
86fn core_log_level(config: &Config) -> Cow<'_, str> {
87 Cow::Borrowed(config.core.log_level.as_str())
88}
89
90fn core_file_log_level(config: &Config) -> Cow<'_, str> {
91 Cow::Borrowed(config.core.file_log_level.as_str())
92}
93
94fn core_auto_update(config: &Config) -> Cow<'_, str> {
95 Cow::Owned(config.core.auto_update.to_string())
96}
97
98fn core_confirm_remove(config: &Config) -> Cow<'_, str> {
99 Cow::Owned(config.core.confirm_remove.to_string())
100}
101
102fn core_default_yes(config: &Config) -> Cow<'_, str> {
103 Cow::Owned(config.core.default_yes.to_string())
104}
105
106fn core_color(config: &Config) -> Cow<'_, str> {
107 Cow::Owned(config.core.color.to_string())
108}
109
110fn paths_root(config: &Config) -> Cow<'_, str> {
111 Cow::Borrowed(config.paths.root.as_str())
112}
113
114fn paths_packages(config: &Config) -> Cow<'_, str> {
115 Cow::Borrowed(config.paths.packages.as_str())
116}
117
118fn paths_data(config: &Config) -> Cow<'_, str> {
119 Cow::Borrowed(config.paths.data.as_str())
120}
121
122fn paths_logs(config: &Config) -> Cow<'_, str> {
123 Cow::Borrowed(config.paths.logs.as_str())
124}
125
126fn paths_cache(config: &Config) -> Cow<'_, str> {
127 Cow::Borrowed(config.paths.cache.as_str())
128}
129
130fn find_entry(key: &str) -> Option<&'static EntrySpec> {
131 SECTION_SPECS
132 .iter()
133 .flat_map(|section| section.entries.iter())
134 .find(|entry| entry.full_key == key)
135}
136
137impl Config {
138 pub fn sections(&self) -> Vec<ConfigSection> {
139 SECTION_SPECS
140 .iter()
141 .map(|section| ConfigSection {
142 title: section.title.to_string(),
143 entries: section
144 .entries
145 .iter()
146 .map(|entry| (entry.key.to_string(), (entry.value)(self).into_owned()))
147 .collect(),
148 })
149 .collect()
150 }
151
152 pub fn effective_value(&self, key: &str) -> ConfigResult<(String, ConfigSource)> {
153 self.lookup_effective(key)?
154 .ok_or_else(|| ConfigError::UnknownKey {
155 key: key.trim().to_string(),
156 })
157 }
158
159 pub fn effective_optional_value(
160 &self,
161 key: &str,
162 ) -> ConfigResult<Option<(String, ConfigSource)>> {
163 self.lookup_effective(key)
164 }
165
166 pub fn effective_sections(&self) -> Result<Vec<ConfigSection>> {
167 let mut sections = Vec::new();
168
169 for section in SECTION_SPECS {
170 let mut entries = Vec::with_capacity(section.entries.len());
171
172 for entry in section.entries {
173 let display_value = if let Some(value) = self.env.value(entry.full_key) {
174 format!("{value} [env override]")
175 } else {
176 (entry.value)(self).into_owned()
177 };
178
179 entries.push((entry.key.to_string(), display_value));
180 }
181
182 sections.push(ConfigSection {
183 title: section.title.to_string(),
184 entries,
185 });
186 }
187
188 Ok(sections)
189 }
190
191 pub fn get_value(&self, key: &str) -> ConfigResult<Option<String>> {
192 let key = key.trim();
193
194 if key.is_empty() {
195 return Err(ConfigError::EmptyKey);
196 }
197
198 Ok(find_entry(key).map(|entry| (entry.value)(self).into_owned()))
199 }
200
201 fn lookup_effective(&self, key: &str) -> ConfigResult<Option<(String, ConfigSource)>> {
202 let key = key.trim();
203
204 if key.is_empty() {
205 return Err(ConfigError::EmptyKey);
206 }
207
208 let Some(entry) = find_entry(key) else {
209 return Ok(None);
210 };
211
212 if let Some(value) = self.env.value(key) {
213 return Ok(Some((value.to_string(), ConfigSource::Env)));
214 }
215
216 Ok(Some(((entry.value)(self).into_owned(), ConfigSource::File)))
217 }
218}