use std::collections::HashSet;
use std::path::{Path, PathBuf};
use figment::{
Figment,
providers::{Format, Serialized, Yaml},
};
use schemars::JsonSchema;
use serde_json::Value;
use serde_yaml::{Mapping, Value as YamlValue};
use crate::{
config::{ConfigResult, ConfigSchema},
config_format::ConfigFormat,
config_schema::{
generate::root_config_schema,
paths::{inner_field_for_section, split_section_paths, transparent_array_section_paths},
},
config_templates::section::section_path_for_target,
path::absolutize_lexical,
};
#[derive(Debug, Default, Clone)]
pub struct TransparentSectionTracker {
pub seen_sections: HashSet<String>,
}
impl TransparentSectionTracker {
pub fn record_section(&mut self, section: &str) {
if !section.is_empty() {
self.seen_sections.insert(section.to_string());
}
}
}
#[derive(Debug, Clone)]
pub struct TransparentSectionContext {
pub root_base_dir: PathBuf,
pub split_paths: Vec<Vec<&'static str>>,
pub transparent_paths: Vec<Vec<&'static str>>,
pub full_schema: Value,
}
impl TransparentSectionContext {
pub fn for_schema<S>(root_config_path: &Path) -> ConfigResult<Self>
where
S: ConfigSchema + JsonSchema,
{
let root_config_path = absolutize_lexical(root_config_path)?;
let root_base_dir = root_config_path
.parent()
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
let full_schema = root_config_schema::<S>()?;
let split_paths = split_section_paths::<S>(&full_schema);
let transparent_paths = transparent_array_section_paths::<S>(&full_schema);
Ok(Self {
root_base_dir,
split_paths,
transparent_paths,
full_schema,
})
}
pub fn section_path_for_file<S>(&self, path: &Path) -> Option<Vec<&'static str>>
where
S: ConfigSchema,
{
section_path_for_target::<S>(&self.root_base_dir, path, &self.split_paths)
}
pub fn is_transparent_section(&self, section_path: &[&str]) -> bool {
self.transparent_paths
.iter()
.any(|path| path.as_slice() == section_path)
}
pub fn inner_field_for_section(&self, section_path: &[&str]) -> String {
inner_field_for_section(&self.full_schema, section_path)
}
}
pub fn is_split_section_file<S>(context: &TransparentSectionContext, path: &Path) -> bool
where
S: ConfigSchema,
{
context.section_path_for_file::<S>(path).is_some()
}
pub fn merge_adapted_file<S>(
figment: Figment,
path: &Path,
context: &TransparentSectionContext,
tracker: &mut TransparentSectionTracker,
) -> ConfigResult<Figment>
where
S: ConfigSchema + JsonSchema,
{
if let Some(section_path) = context.section_path_for_file::<S>(path) {
let section_key = section_path
.last()
.copied()
.expect("split section path must not be empty");
if context.is_transparent_section(§ion_path) && !yaml_has_root_key(path, section_key) {
tracker.record_section(section_key);
let body = read_yaml_value(path)?;
let inner_field = context.inner_field_for_section(§ion_path);
let section_body = wrap_inner_field(body, inner_field.as_str());
let merged = nest_section_mapping(§ion_path, section_body);
return Ok(figment.merge(Serialized::defaults(YamlValue::Mapping(merged))));
}
}
merge_mapping_file::<S>(figment, path, context, tracker)
}
pub fn merge_missing_transparent_sections(
figment: Figment,
context: &TransparentSectionContext,
tracker: &TransparentSectionTracker,
) -> Figment {
let mut figment = figment;
for section_path in &context.transparent_paths {
let Some(section_key) = section_path.last().copied() else {
continue;
};
if tracker.seen_sections.contains(section_key) {
continue;
}
let inner_field = context.inner_field_for_section(section_path);
let empty_items = wrap_inner_field(YamlValue::Sequence(Vec::new()), inner_field.as_str());
let merged = nest_section_mapping(section_path, empty_items);
figment = figment.merge(Serialized::defaults(YamlValue::Mapping(merged)));
}
figment
}
fn nest_section_mapping(section_path: &[&str], body: YamlValue) -> Mapping {
let mut current = body;
for section in section_path.iter().rev() {
let mut map = Mapping::new();
map.insert(YamlValue::String(section.to_string()), current);
current = YamlValue::Mapping(map);
}
match current {
YamlValue::Mapping(map) => map,
other => {
let mut map = Mapping::new();
if let Some(section) = section_path.last() {
map.insert(YamlValue::String(section.to_string()), other);
}
map
}
}
}
fn merge_mapping_file<S>(
figment: Figment,
path: &Path,
context: &TransparentSectionContext,
tracker: &mut TransparentSectionTracker,
) -> ConfigResult<Figment>
where
S: ConfigSchema,
{
match ConfigFormat::from_path(path) {
ConfigFormat::Yaml => {
let value = read_yaml_value(path)?;
if matches!(value, YamlValue::Null) {
return Ok(figment);
}
let split_file = context.section_path_for_file::<S>(path);
record_transparent_sections_in_value(&value, context, tracker);
let adapted = adapt_config_yaml(value, context, split_file.as_deref());
Ok(figment.merge(Serialized::defaults(adapted)))
}
ConfigFormat::Toml => Ok(figment.merge(figment::providers::Toml::file(path))),
ConfigFormat::Json => Ok(figment.merge(figment::providers::Json::file(path))),
}
}
fn record_transparent_sections_in_value(
value: &YamlValue,
context: &TransparentSectionContext,
tracker: &mut TransparentSectionTracker,
) {
let YamlValue::Mapping(map) = value else {
return;
};
for key in map.keys() {
if is_transparent_section_key(key, context) {
if let Some(section) = key.as_str() {
tracker.record_section(section);
}
}
}
}
pub fn adapt_config_yaml(
value: YamlValue,
context: &TransparentSectionContext,
split_file: Option<&[&str]>,
) -> YamlValue {
match value {
YamlValue::Sequence(_) if split_file.is_some() => {
adapt_split_section_body(value, context, split_file.expect("split section path"))
}
YamlValue::Mapping(map) => {
let mut adapted = Mapping::new();
for (key, child) in map {
let next = if is_transparent_section_key(&key, context) {
let section = key.as_str().unwrap_or("");
adapt_section_value(child, context, section)
} else {
adapt_config_yaml(child, context, None)
};
adapted.insert(key, next);
}
YamlValue::Mapping(adapted)
}
other => other,
}
}
fn adapt_split_section_body(
value: YamlValue,
context: &TransparentSectionContext,
section_path: &[&str],
) -> YamlValue {
let inner_field_name = context.inner_field_for_section(section_path);
let inner_field = inner_field_name.as_str();
match value {
YamlValue::Sequence(sequence) => {
wrap_inner_field(YamlValue::Sequence(sequence), inner_field)
}
YamlValue::Mapping(map)
if map.contains_key(YamlValue::String(inner_field_name.clone())) =>
{
YamlValue::Mapping(map)
}
other => other,
}
}
fn adapt_section_value(
value: YamlValue,
context: &TransparentSectionContext,
section: &str,
) -> YamlValue {
let fallback = [section];
let section_path = context
.transparent_paths
.iter()
.find(|path| path.last() == Some(§ion))
.map(Vec::as_slice)
.unwrap_or(&fallback);
let inner_field_name = context.inner_field_for_section(section_path);
let inner_field = inner_field_name.as_str();
match value {
YamlValue::Sequence(sequence) => {
wrap_inner_field(YamlValue::Sequence(sequence), inner_field)
}
YamlValue::Mapping(map)
if map.contains_key(YamlValue::String(inner_field_name.clone())) =>
{
YamlValue::Mapping(map)
}
other => other,
}
}
fn wrap_inner_field(value: YamlValue, inner_field: &str) -> YamlValue {
let mut map = Mapping::new();
map.insert(YamlValue::String(inner_field.to_string()), value);
YamlValue::Mapping(map)
}
fn is_transparent_section_key(key: &YamlValue, context: &TransparentSectionContext) -> bool {
key.as_str().is_some_and(|name| {
context
.transparent_paths
.iter()
.any(|path| path.last() == Some(&name))
})
}
fn yaml_has_root_key(path: &Path, key: &str) -> bool {
if ConfigFormat::from_path(path) != ConfigFormat::Yaml || key.is_empty() {
return false;
}
Figment::from(Yaml::file(path)).find_value(key).is_ok()
}
fn read_yaml_value(path: &Path) -> ConfigResult<YamlValue> {
let content = std::fs::read_to_string(path)?;
serde_yaml::from_str(&content).map_err(|error| {
figment::Error::from(figment::error::Kind::Message(error.to_string())).into()
})
}