use std::collections::BTreeMap;
use std::sync::OnceLock;
use crate::util::config_reader::ConfigReader;
use crate::util::multi_level_map::{ConfigValue, MultiLevelMap};
use crate::util::overrides;
const APP_CONFIG_READER_YML: &str = "classpath:/app-config-reader.yml";
const DEFAULT_PROFILE_PREFIX: &str = "classpath:/application-";
const ACTIVE_PROFILES: &str = "app.profiles.active";
const ENV_ACTIVE_PROFILES: &str = "APP_PROFILES_ACTIVE";
const EMBEDDED_MANIFEST: &str = include_str!("../../resources/app-config-reader.yml");
static INSTANCE: OnceLock<AppConfigReader> = OnceLock::new();
pub(crate) fn try_base_reader() -> Option<&'static ConfigReader> {
INSTANCE.get().map(|app| &app.reader)
}
pub struct AppConfigReader {
reader: ConfigReader,
}
impl AppConfigReader {
pub fn get_instance() -> &'static AppConfigReader {
INSTANCE.get_or_init(Self::build)
}
fn build() -> Self {
let (files, profile_prefix) = load_manifest();
let mut consolidated: BTreeMap<String, ConfigValue> = BTreeMap::new();
for filename in &files {
merge_config(&mut consolidated, filename);
}
let profiles = active_profiles(&consolidated);
let mut updated = 0usize;
for profile in &profiles {
updated += merge_config(
&mut consolidated,
&format!("{profile_prefix}{profile}.properties"),
);
updated += merge_config(&mut consolidated, &format!("{profile_prefix}{profile}.yml"));
}
if updated > 0 {
log::info!(
"Updated {updated} parameter{} from active profiles {profiles:?}",
if updated == 1 { "" } else { "s" }
);
}
let map = MultiLevelMap::from_flat_map(&consolidated);
let reader = ConfigReader::new_base(map);
if reader.is_empty() {
log::error!("Configuration is empty - please check");
}
AppConfigReader { reader }
}
pub fn get(&self, key: &str) -> Option<ConfigValue> {
self.reader.get(key)
}
pub fn get_or(&self, key: &str, default: ConfigValue) -> ConfigValue {
self.reader.get_or(key, default)
}
pub fn get_property(&self, key: &str) -> Option<String> {
self.reader.get_property(key)
}
pub fn get_property_or(&self, key: &str, default: &str) -> String {
self.reader.get_property_or(key, default)
}
pub fn exists(&self, key: &str) -> bool {
self.reader.exists(key)
}
pub fn is_empty(&self) -> bool {
self.reader.is_empty()
}
pub fn get_map(&self) -> &MultiLevelMap {
self.reader.get_map()
}
pub fn get_composite_key_values(&self) -> &BTreeMap<String, ConfigValue> {
self.reader.get_composite_key_values()
}
pub fn is_base_config(&self) -> bool {
self.reader.is_base_config()
}
}
fn load_manifest() -> (Vec<String>, String) {
let manifest = match ConfigReader::load_raw(APP_CONFIG_READER_YML) {
Ok(reader) => reader.get_map().clone(),
Err(_) => {
let value: serde_yaml::Value = serde_yaml::from_str(EMBEDDED_MANIFEST)
.expect("embedded app-config-reader.yml is valid YAML");
match ConfigValue::from_yaml(&value) {
ConfigValue::Map(m) => MultiLevelMap::from_map(m),
_ => MultiLevelMap::new(),
}
}
};
let files = match manifest.get_element("resources") {
Some(ConfigValue::List(list)) => list
.iter()
.filter_map(|v| v.as_text().map(str::to_string))
.collect(),
_ => {
log::error!("Unable to parse configuration - missing 'resources' section in app-config-reader.yml");
Vec::new()
}
};
let prefix = match manifest.get_element("profiles") {
Some(ConfigValue::Text(p)) => p.clone(),
_ => DEFAULT_PROFILE_PREFIX.to_string(),
};
(files, prefix)
}
fn merge_config(consolidated: &mut BTreeMap<String, ConfigValue>, filename: &str) -> usize {
match ConfigReader::load_raw(filename) {
Ok(reader) => {
let flat = reader.get_map().flat_map();
if !flat.is_empty() {
log::info!("Loaded {filename}");
let n = flat.len();
consolidated.extend(flat);
n
} else {
0
}
}
Err(_) => 0, }
}
fn active_profiles(consolidated: &BTreeMap<String, ConfigValue>) -> Vec<String> {
let raw = std::env::var(ENV_ACTIVE_PROFILES)
.ok()
.or_else(|| overrides::get(ACTIVE_PROFILES))
.or_else(|| {
consolidated
.get(ACTIVE_PROFILES)
.and_then(|v| v.as_text().map(str::to_string))
});
match raw {
Some(text) => text
.split([',', ' '])
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect(),
None => Vec::new(),
}
}