use regex::Regex;
use serde_json::map::Entry;
use serde_json::{Map, Number, Value};
use std::sync::OnceLock;
use qubit_common::serde::duration_with_unit;
use qubit_value::{MultiValues, ValueError};
use super::{ConfigError, ConfigReader, ConfigResult, Property};
static VARIABLE_PATTERN: OnceLock<Regex> = OnceLock::new();
#[inline]
fn get_variable_pattern() -> &'static Regex {
VARIABLE_PATTERN.get_or_init(|| {
Regex::new(r"\$\{([^}]+)\}").expect("Failed to compile variable pattern regex")
})
}
pub(crate) fn map_value_error(key: &str, err: ValueError) -> ConfigError {
ConfigError::from((key, err))
}
pub(crate) fn substitute_variables<R: ConfigReader + ?Sized>(
value: &str,
config: &R,
max_depth: usize,
) -> ConfigResult<String> {
substitute_variables_by(value, max_depth, |var_name| {
find_variable_value(var_name, config)
})
}
pub(crate) fn substitute_variables_with_fallback<
P: ConfigReader + ?Sized,
F: ConfigReader + ?Sized,
>(
value: &str,
primary: &P,
fallback: &F,
max_depth: usize,
) -> ConfigResult<String> {
substitute_variables_by(value, max_depth, |var_name| {
find_variable_value_with_fallback(var_name, primary, fallback)
})
}
fn substitute_variables_by(
value: &str,
max_depth: usize,
mut resolve: impl FnMut(&str) -> ConfigResult<String>,
) -> ConfigResult<String> {
if value.is_empty() {
return Ok(value.to_string());
}
let pattern = get_variable_pattern();
let mut result = value.to_string();
let mut depth = 0;
loop {
if !pattern.is_match(&result) {
break;
}
if depth >= max_depth {
return Err(ConfigError::SubstitutionDepthExceeded(max_depth));
}
let mut first_error: Option<ConfigError> = None;
let replaced = pattern.replace_all(&result, |caps: ®ex::Captures| {
let var_name = caps.get(1).map(|m| m.as_str()).unwrap_or_default();
match resolve(var_name) {
Ok(v) => v,
Err(err) => {
if first_error.is_none() {
first_error = Some(err);
}
caps.get(0)
.map(|m| m.as_str().to_string())
.unwrap_or_default()
}
}
});
if let Some(err) = first_error {
return Err(err);
}
result = replaced.into_owned();
depth += 1;
}
Ok(result)
}
fn find_variable_value<R: ConfigReader + ?Sized>(
var_name: &str,
config: &R,
) -> ConfigResult<String> {
match config.get::<String>(var_name) {
Ok(value) => Ok(value),
Err(ConfigError::PropertyNotFound(_)) | Err(ConfigError::PropertyHasNoValue(_)) => {
std::env::var(var_name).map_err(|_| {
ConfigError::SubstitutionError(format!("Cannot resolve variable: {}", var_name))
})
}
Err(err) => Err(err),
}
}
fn find_variable_value_with_fallback<P: ConfigReader + ?Sized, F: ConfigReader + ?Sized>(
var_name: &str,
primary: &P,
fallback: &F,
) -> ConfigResult<String> {
match primary.get::<String>(var_name) {
Ok(value) => Ok(value),
Err(ConfigError::PropertyNotFound(_)) | Err(ConfigError::PropertyHasNoValue(_)) => {
find_variable_value(var_name, fallback)
}
Err(err) => Err(err),
}
}
pub(crate) fn insert_deserialize_value(root: &mut Map<String, Value>, key: &str, value: Value) {
if !key.contains('.') || key.is_empty() {
root.insert(key.to_string(), value);
return;
}
let fallback_value = value.clone();
if try_insert_nested_json_value(root, key, value).is_err() {
root.insert(key.to_string(), fallback_value);
}
}
fn try_insert_nested_json_value(
root: &mut Map<String, Value>,
key: &str,
value: Value,
) -> Result<(), ()> {
let parts: Vec<&str> = key.split('.').collect();
if parts.iter().any(|part| part.is_empty()) {
return Err(());
}
let (leaf, parents) = parts
.split_last()
.expect("split on a string always returns at least one segment");
let mut current = root;
for part in parents {
let next = match current.entry(part.to_string()) {
Entry::Vacant(entry) => entry.insert(Value::Object(Map::new())),
Entry::Occupied(entry) => entry.into_mut(),
};
match next {
Value::Object(obj) => {
current = obj;
}
_ => return Err(()),
}
}
current.insert((*leaf).to_string(), value);
Ok(())
}
pub(crate) fn property_to_json_value(prop: &Property) -> Value {
let mv = prop.value();
match mv {
MultiValues::Empty(_) => Value::Null,
MultiValues::Bool(v) => {
if v.len() == 1 {
Value::Bool(v[0])
} else {
Value::Array(v.iter().map(|b| Value::Bool(*b)).collect())
}
}
MultiValues::Int8(v) => scalar_or_array(v, |x| Value::Number((*x).into())),
MultiValues::Int16(v) => scalar_or_array(v, |x| Value::Number((*x).into())),
MultiValues::Int32(v) => scalar_or_array(v, |x| Value::Number((*x).into())),
MultiValues::Int64(v) => scalar_or_array(v, |x| Value::Number((*x).into())),
MultiValues::IntSize(v) => scalar_or_array(v, |x| Value::Number(Number::from(*x as i64))),
MultiValues::UInt8(v) => scalar_or_array(v, |x| Value::Number((*x).into())),
MultiValues::UInt16(v) => scalar_or_array(v, |x| Value::Number((*x).into())),
MultiValues::UInt32(v) => scalar_or_array(v, |x| Value::Number((*x).into())),
MultiValues::UInt64(v) => scalar_or_array(v, |x| Value::Number((*x).into())),
MultiValues::UIntSize(v) => scalar_or_array(v, |x| Value::Number(Number::from(*x as u64))),
MultiValues::Float32(v) => scalar_or_array(v, |x| {
Number::from_f64(*x as f64)
.map(Value::Number)
.unwrap_or(Value::Null)
}),
MultiValues::Float64(v) => scalar_or_array(v, |x| {
Number::from_f64(*x)
.map(Value::Number)
.unwrap_or(Value::Null)
}),
MultiValues::String(v) => scalar_or_array(v, |x| Value::String(x.clone())),
MultiValues::Duration(v) => {
scalar_or_array(v, |x| Value::String(duration_with_unit::format(x)))
}
MultiValues::Url(v) => scalar_or_array(v, |x| Value::String(x.to_string())),
MultiValues::StringMap(v) => {
if v.len() == 1 {
let obj: Map<String, Value> = v[0]
.iter()
.map(|(k, val)| (k.clone(), Value::String(val.clone())))
.collect();
Value::Object(obj)
} else {
Value::Array(
v.iter()
.map(|m| {
let obj: Map<String, Value> = m
.iter()
.map(|(k, val)| (k.clone(), Value::String(val.clone())))
.collect();
Value::Object(obj)
})
.collect(),
)
}
}
MultiValues::Json(v) => {
if v.len() == 1 {
v[0].clone()
} else {
Value::Array(v.clone())
}
}
MultiValues::Char(v) => scalar_or_array(v, |x| Value::String(x.to_string())),
MultiValues::BigInteger(v) => scalar_or_array(v, |x| Value::String(x.to_string())),
MultiValues::BigDecimal(v) => scalar_or_array(v, |x| Value::String(x.to_string())),
MultiValues::DateTime(v) => scalar_or_array(v, |x| Value::String(x.to_string())),
MultiValues::Date(v) => scalar_or_array(v, |x| Value::String(x.to_string())),
MultiValues::Time(v) => scalar_or_array(v, |x| Value::String(x.to_string())),
MultiValues::Instant(v) => scalar_or_array(v, |x| Value::String(x.to_string())),
MultiValues::Int128(v) => scalar_or_array(v, |x| Value::String(x.to_string())),
MultiValues::UInt128(v) => scalar_or_array(v, |x| Value::String(x.to_string())),
}
}
pub(crate) fn substitute_json_strings_with_fallback<
P: ConfigReader + ?Sized,
F: ConfigReader + ?Sized,
>(
value: &mut Value,
primary: &P,
fallback: &F,
) -> ConfigResult<()> {
if !primary.is_enable_variable_substitution() {
return Ok(());
}
match value {
Value::String(s) => {
*s = substitute_variables_with_fallback(
s,
primary,
fallback,
primary.max_substitution_depth(),
)?;
}
Value::Array(values) => {
for value in values {
substitute_json_strings_with_fallback(value, primary, fallback)?;
}
}
Value::Object(map) => {
for value in map.values_mut() {
substitute_json_strings_with_fallback(value, primary, fallback)?;
}
}
_ => {}
}
Ok(())
}
fn scalar_or_array<T, F>(v: &[T], f: F) -> Value
where
F: Fn(&T) -> Value,
{
if v.len() == 1 {
f(&v[0])
} else {
Value::Array(v.iter().map(f).collect())
}
}