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>(context: &TransparentSectionContext, path: &Path) -> bool
100where
101 S: ConfigSchema,
102{
103 context.section_path_for_file::<S>(path).is_some()
104}
105
106pub fn merge_adapted_file<S>(
108 figment: Figment,
109 path: &Path,
110 context: &TransparentSectionContext,
111 tracker: &mut TransparentSectionTracker,
112) -> ConfigResult<Figment>
113where
114 S: ConfigSchema + JsonSchema,
115{
116 if let Some(section_path) = context.section_path_for_file::<S>(path) {
117 let section_key = section_path
118 .last()
119 .copied()
120 .expect("split section path must not be empty");
121
122 if context.is_transparent_section(§ion_path) && !yaml_has_root_key(path, section_key) {
123 tracker.record_section(section_key);
124 let body = read_yaml_value(path)?;
125 let inner_field = context.inner_field_for_section(§ion_path);
126 let section_body = wrap_inner_field(body, inner_field.as_str());
127 let merged = nest_section_mapping(§ion_path, section_body);
128 return Ok(figment.merge(Serialized::defaults(YamlValue::Mapping(merged))));
129 }
130 }
131
132 merge_mapping_file::<S>(figment, path, context, tracker)
133}
134
135pub fn merge_missing_transparent_sections(
140 figment: Figment,
141 context: &TransparentSectionContext,
142 tracker: &TransparentSectionTracker,
143) -> Figment {
144 let mut figment = figment;
145
146 for section_path in &context.transparent_paths {
147 let Some(section_key) = section_path.last().copied() else {
148 continue;
149 };
150
151 if tracker.seen_sections.contains(section_key) {
152 continue;
153 }
154
155 let inner_field = context.inner_field_for_section(section_path);
156 let empty_items = wrap_inner_field(YamlValue::Sequence(Vec::new()), inner_field.as_str());
157 let merged = nest_section_mapping(section_path, empty_items);
158 figment = figment.merge(Serialized::defaults(YamlValue::Mapping(merged)));
159 }
160
161 figment
162}
163
164fn nest_section_mapping(section_path: &[&str], body: YamlValue) -> Mapping {
165 let mut current = body;
166 for section in section_path.iter().rev() {
167 let mut map = Mapping::new();
168 map.insert(YamlValue::String(section.to_string()), current);
169 current = YamlValue::Mapping(map);
170 }
171
172 match current {
173 YamlValue::Mapping(map) => map,
174 other => {
175 let mut map = Mapping::new();
176 if let Some(section) = section_path.last() {
177 map.insert(YamlValue::String(section.to_string()), other);
178 }
179 map
180 }
181 }
182}
183
184fn merge_mapping_file<S>(
185 figment: Figment,
186 path: &Path,
187 context: &TransparentSectionContext,
188 tracker: &mut TransparentSectionTracker,
189) -> ConfigResult<Figment>
190where
191 S: ConfigSchema,
192{
193 match ConfigFormat::from_path(path) {
194 ConfigFormat::Yaml => {
195 let value = read_yaml_value(path)?;
196 if matches!(value, YamlValue::Null) {
197 return Ok(figment);
198 }
199 let split_file = context.section_path_for_file::<S>(path);
200 record_transparent_sections_in_value(&value, context, tracker);
201 let adapted = adapt_config_yaml(value, context, split_file.as_deref());
202 Ok(figment.merge(Serialized::defaults(adapted)))
203 }
204 ConfigFormat::Toml => Ok(figment.merge(figment::providers::Toml::file(path))),
205 ConfigFormat::Json => Ok(figment.merge(figment::providers::Json::file(path))),
206 }
207}
208
209fn record_transparent_sections_in_value(
210 value: &YamlValue,
211 context: &TransparentSectionContext,
212 tracker: &mut TransparentSectionTracker,
213) {
214 let YamlValue::Mapping(map) = value else {
215 return;
216 };
217
218 for key in map.keys() {
219 if is_transparent_section_key(key, context) {
220 if let Some(section) = key.as_str() {
221 tracker.record_section(section);
222 }
223 }
224 }
225}
226
227pub fn adapt_config_yaml(
229 value: YamlValue,
230 context: &TransparentSectionContext,
231 split_file: Option<&[&str]>,
232) -> YamlValue {
233 match value {
234 YamlValue::Sequence(_) if split_file.is_some() => {
235 adapt_split_section_body(value, context, split_file.expect("split section path"))
236 }
237 YamlValue::Mapping(map) => {
238 let mut adapted = Mapping::new();
239 for (key, child) in map {
240 let next = if is_transparent_section_key(&key, context) {
241 let section = key.as_str().unwrap_or("");
242 adapt_section_value(child, context, section)
243 } else {
244 adapt_config_yaml(child, context, None)
245 };
246 adapted.insert(key, next);
247 }
248 YamlValue::Mapping(adapted)
249 }
250 other => other,
251 }
252}
253
254fn adapt_split_section_body(
255 value: YamlValue,
256 context: &TransparentSectionContext,
257 section_path: &[&str],
258) -> YamlValue {
259 let inner_field_name = context.inner_field_for_section(section_path);
260 let inner_field = inner_field_name.as_str();
261 match value {
262 YamlValue::Sequence(sequence) => {
263 wrap_inner_field(YamlValue::Sequence(sequence), inner_field)
264 }
265 YamlValue::Mapping(map)
266 if map.contains_key(YamlValue::String(inner_field_name.clone())) =>
267 {
268 YamlValue::Mapping(map)
269 }
270 other => other,
271 }
272}
273
274fn adapt_section_value(
275 value: YamlValue,
276 context: &TransparentSectionContext,
277 section: &str,
278) -> YamlValue {
279 let fallback = [section];
280 let section_path = context
281 .transparent_paths
282 .iter()
283 .find(|path| path.last() == Some(§ion))
284 .map(Vec::as_slice)
285 .unwrap_or(&fallback);
286
287 let inner_field_name = context.inner_field_for_section(section_path);
288 let inner_field = inner_field_name.as_str();
289 match value {
290 YamlValue::Sequence(sequence) => {
291 wrap_inner_field(YamlValue::Sequence(sequence), inner_field)
292 }
293 YamlValue::Mapping(map)
294 if map.contains_key(YamlValue::String(inner_field_name.clone())) =>
295 {
296 YamlValue::Mapping(map)
297 }
298 other => other,
299 }
300}
301
302fn wrap_inner_field(value: YamlValue, inner_field: &str) -> YamlValue {
303 let mut map = Mapping::new();
304 map.insert(YamlValue::String(inner_field.to_string()), value);
305 YamlValue::Mapping(map)
306}
307
308fn is_transparent_section_key(key: &YamlValue, context: &TransparentSectionContext) -> bool {
309 key.as_str().is_some_and(|name| {
310 context
311 .transparent_paths
312 .iter()
313 .any(|path| path.last() == Some(&name))
314 })
315}
316
317fn yaml_has_root_key(path: &Path, key: &str) -> bool {
318 if ConfigFormat::from_path(path) != ConfigFormat::Yaml || key.is_empty() {
319 return false;
320 }
321
322 Figment::from(Yaml::file(path)).find_value(key).is_ok()
323}
324
325fn read_yaml_value(path: &Path) -> ConfigResult<YamlValue> {
326 let content = std::fs::read_to_string(path)?;
327 serde_yaml::from_str(&content).map_err(|error| {
328 figment::Error::from(figment::error::Kind::Message(error.to_string())).into()
329 })
330}