use crate::{deep_merge, format::ConfigFormat, resolve_with_providers, SecretProvider};
use cirious_codex_result::{codex_ok, CodexError, Result};
use serde::de::DeserializeOwned;
use serde_json::{Map, Value};
use std::{collections::HashMap, env};
#[derive(Debug, Default)]
pub struct ConfigBuilder {
internal_map: Value,
providers: HashMap<String, Box<dyn SecretProvider>>,
}
impl ConfigBuilder {
#[must_use]
pub fn new() -> Self {
Self {
internal_map: Value::Object(serde_json::Map::new()),
providers: HashMap::new(),
}
}
pub fn add_source(mut self, content: &str, format: ConfigFormat) -> Result<Self> {
let parsed = format.parse::<Value>(content).map_err(|e| {
e.with_suggestion(crate::utils::format_suggestion(
"Check the configuration file syntax for invalid formatting.",
))
.with_meta("format", format!("{format:?}"))
})?;
self.merge_value(parsed.value);
codex_ok!(self)
}
#[must_use]
pub fn add_env_prefix(mut self, prefix: &str) -> Self {
for (key, val) in env::vars() {
if let Some(stripped) = key.strip_prefix(prefix) {
let clean_key = stripped.to_lowercase();
let parsed_val = val
.parse::<i64>()
.map(|num| Value::Number(num.into()))
.or_else(|_| val.parse::<bool>().map(Value::Bool))
.unwrap_or(Value::String(val));
if let Value::Object(ref mut map) = self.internal_map {
map.insert(clean_key, parsed_val);
}
}
}
self
}
#[must_use]
pub fn add_env_nested(mut self, prefix: &str, separator: &str) -> Self {
for (key, val) in env::vars() {
if let Some(stripped) = key.strip_prefix(prefix) {
let keys: Vec<String> = stripped.split(separator).map(str::to_lowercase).collect();
if keys.is_empty() {
continue;
}
let parsed_val = val
.parse::<i64>()
.map(|n| Value::Number(n.into()))
.or_else(|_| val.parse::<bool>().map(Value::Bool))
.unwrap_or(Value::String(val));
let mut node = &mut self.internal_map;
for k in keys.iter().take(keys.len() - 1) {
if !node.is_object() {
*node = Value::Object(Map::new());
}
node = {
if let Some(map) = node.as_object_mut() {
map.entry(k).or_insert_with(|| Value::Object(Map::new()))
} else {
return self;
}
};
}
if !node.is_object() {
*node = Value::Object(Map::new());
}
if let Some(map) = node.as_object_mut() {
if let Some(last_key) = keys.last() {
let target = map.entry(last_key).or_insert(Value::Null);
deep_merge(target, parsed_val);
}
}
}
}
self
}
#[must_use]
pub fn add_cli_overrides(mut self, overrides: HashMap<String, String>) -> Self {
for (key, val) in overrides {
let keys: Vec<String> = key.split('.').map(str::to_lowercase).collect();
let parsed_val = val
.parse::<i64>()
.map(|num| Value::Number(num.into()))
.or_else(|_| val.parse::<bool>().map(Value::Bool))
.unwrap_or(Value::String(val));
let mut node = &mut self.internal_map;
for k in keys.iter().take(keys.len() - 1) {
if !node.is_object() {
*node = Value::Object(Map::new());
}
node = {
if let Some(map) = node.as_object_mut() {
map.entry(k).or_insert_with(|| Value::Object(Map::new()))
} else {
return self;
}
};
}
if !node.is_object() {
*node = Value::Object(Map::new());
}
if let Some(map) = node.as_object_mut() {
if let Some(last_key) = keys.last() {
let target = map.entry(last_key).or_insert(Value::Null);
deep_merge(target, parsed_val);
}
}
}
self
}
#[must_use]
pub fn register_provider(mut self, scheme: &str, provider: Box<dyn SecretProvider>) -> Self {
self.providers.insert(scheme.to_string(), provider);
self
}
fn resolve_all_secrets(&mut self) {
fn walk(value: &mut Value, providers: &HashMap<String, Box<dyn SecretProvider>>) {
match value {
Value::String(s) if s.contains("://") => {
if let Some(resolved) = resolve_with_providers(s, providers) {
*value = Value::String(resolved.value);
}
}
Value::Object(map) => {
for v in map.values_mut() {
walk(v, providers);
}
}
Value::Array(arr) => {
for v in arr {
walk(v, providers);
}
}
_ => {}
}
}
walk(&mut self.internal_map, &self.providers);
}
pub fn build<T: DeserializeOwned>(mut self) -> Result<T> {
self.resolve_all_secrets();
let result = serde_json::from_value(self.internal_map).map_err(|e| {
CodexError::builder(
"CONFIG_BUILD_ERROR",
format!("Failed to map merged configurations to target struct: {e}"),
)
.with_suggestion("Ensure your struct properties match the loaded data and no required fields are missing.")
})?;
codex_ok!(result)
}
fn merge_value(&mut self, other: Value) {
deep_merge(&mut self.internal_map, other);
}
}