1use std::collections::HashSet;
4use std::path::{Path, PathBuf};
5
6use figment::{
7 Figment,
8 providers::{Format, Serialized, Yaml},
9};
10use schemars::JsonSchema;
11use serde_json::Value;
12use serde_yaml::{Mapping, Value as YamlValue};
13
14use crate::{
15 config::{ConfigResult, ConfigSchema},
16 config_format::ConfigFormat,
17 config_schema::{
18 generate::root_config_schema,
19 paths::{inner_field_for_section, split_section_paths, transparent_array_section_paths},
20 },
21 config_templates::section::section_path_for_target,
22 path::absolutize_lexical,
23};
24
25#[derive(Debug, Default, Clone)]
27pub struct TransparentSectionTracker {
28 pub seen_sections: HashSet<String>,
30}
31
32impl TransparentSectionTracker {
33 pub fn record_section(&mut self, section: &str) {
35 if !section.is_empty() {
36 self.seen_sections.insert(section.to_string());
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct TransparentSectionContext {
44 pub root_base_dir: PathBuf,
46 pub split_paths: Vec<Vec<&'static str>>,
48 pub transparent_paths: Vec<Vec<&'static str>>,
50 pub full_schema: Value,
52}
53
54impl TransparentSectionContext {
55 pub fn for_schema<S>(root_config_path: &Path) -> ConfigResult<Self>
57 where
58 S: ConfigSchema + JsonSchema,
59 {
60 let root_config_path = absolutize_lexical(root_config_path)?;
61 let root_base_dir = root_config_path
62 .parent()
63 .unwrap_or_else(|| Path::new("."))
64 .to_path_buf();
65 let full_schema = root_config_schema::<S>()?;
66 let split_paths = split_section_paths::<S>(&full_schema);
67 let transparent_paths = transparent_array_section_paths::<S>(&full_schema);
68
69 Ok(Self {
70 root_base_dir,
71 split_paths,
72 transparent_paths,
73 full_schema,
74 })
75 }
76
77 pub fn section_path_for_file<S>(&self, path: &Path) -> Option<Vec<&'static str>>
79 where
80 S: ConfigSchema,
81 {
82 section_path_for_target::<S>(&self.root_base_dir, path, &self.split_paths)
83 }
84
85 pub fn is_transparent_section(&self, section_path: &[&str]) -> bool {
87 self.transparent_paths
88 .iter()
89 .any(|path| path.as_slice() == section_path)
90 }
91
92 pub fn inner_field_for_section(&self, section_path: &[&str]) -> String {
94 inner_field_for_section(&self.full_schema, section_path)
95 }
96}
97
98pub fn is_split_section_file<S>(
100 context: &TransparentSectionContext,
101 path: &Path,
102) -> bool
103where
104 S: ConfigSchema,
105{
106 context.section_path_for_file::<S>(path).is_some()
107}
108
109pub fn merge_adapted_file<S>(
111 figment: Figment,
112 path: &Path,
113 context: &TransparentSectionContext,
114 tracker: &mut TransparentSectionTracker,
115) -> ConfigResult<Figment>
116where
117 S: ConfigSchema + JsonSchema,
118{
119 if let Some(section_path) = context.section_path_for_file::<S>(path) {
120 let section_key = section_path
121 .last()
122 .copied()
123 .expect("split section path must not be empty");
124
125 if context.is_transparent_section(§ion_path) && !yaml_has_root_key(path, section_key) {
126 tracker.record_section(section_key);
127 let body = read_yaml_value(path)?;
128 let inner_field = context.inner_field_for_section(§ion_path);
129 let section_body = wrap_inner_field(body, inner_field.as_str());
130 let merged = nest_section_mapping(§ion_path, section_body);
131 return Ok(figment.merge(Serialized::defaults(YamlValue::Mapping(merged))));
132 }
133 }
134
135 merge_mapping_file::<S>(figment, path, context, tracker)
136}
137
138pub fn merge_missing_transparent_sections(
143 figment: Figment,
144 context: &TransparentSectionContext,
145 tracker: &TransparentSectionTracker,
146) -> Figment {
147 let mut figment = figment;
148
149 for section_path in &context.transparent_paths {
150 let Some(section_key) = section_path.last().copied() else {
151 continue;
152 };
153
154 if tracker.seen_sections.contains(section_key) {
155 continue;
156 }
157
158 let inner_field = context.inner_field_for_section(section_path);
159 let empty_items = wrap_inner_field(YamlValue::Sequence(Vec::new()), inner_field.as_str());
160 let merged = nest_section_mapping(section_path, empty_items);
161 figment = figment.merge(Serialized::defaults(YamlValue::Mapping(merged)));
162 }
163
164 figment
165}
166
167fn nest_section_mapping(section_path: &[&str], body: YamlValue) -> Mapping {
168 let mut current = body;
169 for section in section_path.iter().rev() {
170 let mut map = Mapping::new();
171 map.insert(YamlValue::String(section.to_string()), current);
172 current = YamlValue::Mapping(map);
173 }
174
175 match current {
176 YamlValue::Mapping(map) => map,
177 other => {
178 let mut map = Mapping::new();
179 if let Some(section) = section_path.last() {
180 map.insert(YamlValue::String(section.to_string()), other);
181 }
182 map
183 }
184 }
185}
186
187fn merge_mapping_file<S>(
188 figment: Figment,
189 path: &Path,
190 context: &TransparentSectionContext,
191 tracker: &mut TransparentSectionTracker,
192) -> ConfigResult<Figment>
193where
194 S: ConfigSchema,
195{
196 match ConfigFormat::from_path(path) {
197 ConfigFormat::Yaml => {
198 let value = read_yaml_value(path)?;
199 if matches!(value, YamlValue::Null) {
200 return Ok(figment);
201 }
202 let split_file = context.section_path_for_file::<S>(path);
203 record_transparent_sections_in_value(&value, context, tracker);
204 let adapted = adapt_config_yaml(value, context, split_file.as_deref());
205 Ok(figment.merge(Serialized::defaults(adapted)))
206 }
207 ConfigFormat::Toml => Ok(figment.merge(figment::providers::Toml::file(path))),
208 ConfigFormat::Json => Ok(figment.merge(figment::providers::Json::file(path))),
209 }
210}
211
212fn record_transparent_sections_in_value(
213 value: &YamlValue,
214 context: &TransparentSectionContext,
215 tracker: &mut TransparentSectionTracker,
216) {
217 let YamlValue::Mapping(map) = value else {
218 return;
219 };
220
221 for key in map.keys() {
222 if is_transparent_section_key(key, context) {
223 if let Some(section) = key.as_str() {
224 tracker.record_section(section);
225 }
226 }
227 }
228}
229
230pub fn adapt_config_yaml(
232 value: YamlValue,
233 context: &TransparentSectionContext,
234 split_file: Option<&[&str]>,
235) -> YamlValue {
236 match value {
237 YamlValue::Sequence(_) if split_file.is_some() => {
238 adapt_split_section_body(value, context, split_file.expect("split section path"))
239 }
240 YamlValue::Mapping(map) => {
241 let mut adapted = Mapping::new();
242 for (key, child) in map {
243 let next = if is_transparent_section_key(&key, context) {
244 let section = key.as_str().unwrap_or("");
245 adapt_section_value(child, context, section)
246 } else {
247 adapt_config_yaml(child, context, None)
248 };
249 adapted.insert(key, next);
250 }
251 YamlValue::Mapping(adapted)
252 }
253 other => other,
254 }
255}
256
257fn adapt_split_section_body(
258 value: YamlValue,
259 context: &TransparentSectionContext,
260 section_path: &[&str],
261) -> YamlValue {
262 let inner_field_name = context.inner_field_for_section(section_path);
263 let inner_field = inner_field_name.as_str();
264 match value {
265 YamlValue::Sequence(sequence) => wrap_inner_field(YamlValue::Sequence(sequence), inner_field),
266 YamlValue::Mapping(map) if map.contains_key(YamlValue::String(inner_field_name.clone())) => {
267 YamlValue::Mapping(map)
268 }
269 other => other,
270 }
271}
272
273fn adapt_section_value(value: YamlValue, context: &TransparentSectionContext, section: &str) -> YamlValue {
274 let fallback = [section];
275 let section_path = context
276 .transparent_paths
277 .iter()
278 .find(|path| path.last() == Some(§ion))
279 .map(Vec::as_slice)
280 .unwrap_or(&fallback);
281
282 let inner_field_name = context.inner_field_for_section(section_path);
283 let inner_field = inner_field_name.as_str();
284 match value {
285 YamlValue::Sequence(sequence) => wrap_inner_field(YamlValue::Sequence(sequence), inner_field),
286 YamlValue::Mapping(map) if map.contains_key(YamlValue::String(inner_field_name.clone())) => {
287 YamlValue::Mapping(map)
288 }
289 other => other,
290 }
291}
292
293fn wrap_inner_field(value: YamlValue, inner_field: &str) -> YamlValue {
294 let mut map = Mapping::new();
295 map.insert(YamlValue::String(inner_field.to_string()), value);
296 YamlValue::Mapping(map)
297}
298
299fn is_transparent_section_key(key: &YamlValue, context: &TransparentSectionContext) -> bool {
300 key.as_str().is_some_and(|name| {
301 context
302 .transparent_paths
303 .iter()
304 .any(|path| path.last() == Some(&name))
305 })
306}
307
308fn yaml_has_root_key(path: &Path, key: &str) -> bool {
309 if ConfigFormat::from_path(path) != ConfigFormat::Yaml || key.is_empty() {
310 return false;
311 }
312
313 Figment::from(Yaml::file(path)).find_value(key).is_ok()
314}
315
316fn read_yaml_value(path: &Path) -> ConfigResult<YamlValue> {
317 let content = std::fs::read_to_string(path)?;
318 serde_yaml::from_str(&content).map_err(|error| {
319 figment::Error::from(figment::error::Kind::Message(error.to_string())).into()
320 })
321}