use figment::{
Error, Metadata, Profile, Provider,
value::{Dict, Map, Tag, Value},
};
use serde_json;
use std::{collections::HashMap, env};
pub struct Nested {
prefix: String,
separator: String,
env_vars: HashMap<String, String>,
metadata: Metadata,
}
impl Nested {
pub fn prefixed<S: AsRef<str>>(prefix: S) -> Self {
Self::prefixed_with_separator(prefix, "_")
}
pub fn prefixed_with_separator<S1: AsRef<str>, S2: AsRef<str>>(
prefix: S1,
separator: S2,
) -> Self {
let prefix = prefix.as_ref().to_string();
let separator = separator.as_ref().to_string();
let env_vars = Self::collect_env_vars(&prefix);
let metadata = Metadata::named("Env::Nested");
Self {
prefix,
separator,
env_vars,
metadata,
}
}
pub fn new() -> Self {
Self::prefixed("")
}
pub fn named<S: AsRef<str>>(mut self, name: S) -> Self {
self.metadata = Metadata::named(name.as_ref().to_string());
self
}
fn collect_env_vars(prefix: &str) -> HashMap<String, String> {
env::vars()
.filter(|(key, _)| key.starts_with(prefix))
.collect()
}
fn parse_env_value(value: &str) -> Result<Value, Error> {
let trimmed = value.trim();
if (trimmed.starts_with('[') && trimmed.ends_with(']'))
|| (trimmed.starts_with('{') && trimmed.ends_with('}'))
{
if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(trimmed) {
return Self::json_value_to_figment_value(json_val);
}
}
match trimmed.to_lowercase().as_str() {
"true" | "yes" | "1" | "on" => return Ok(Value::Bool(Tag::default(), true)),
"false" | "no" | "0" | "off" => return Ok(Value::Bool(Tag::default(), false)),
_ => {}
}
if let Ok(int_val) = trimmed.parse::<i64>() {
return Ok(Value::from(int_val));
}
if let Ok(float_val) = trimmed.parse::<f64>() {
return Ok(Value::from(float_val));
}
Ok(Value::String(Tag::default(), trimmed.to_string()))
}
fn json_value_to_figment_value(json_val: serde_json::Value) -> Result<Value, Error> {
match json_val {
serde_json::Value::Null => Ok(Value::String(Tag::default(), "null".to_string())),
serde_json::Value::Bool(b) => Ok(Value::Bool(Tag::default(), b)),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(Value::from(i))
} else if let Some(f) = n.as_f64() {
Ok(Value::from(f))
} else {
Ok(Value::String(Tag::default(), n.to_string()))
}
}
serde_json::Value::String(s) => Ok(Value::String(Tag::default(), s)),
serde_json::Value::Array(arr) => {
let figment_array: Result<Vec<Value>, Error> = arr
.into_iter()
.map(Self::json_value_to_figment_value)
.collect();
Ok(Value::Array(Tag::default(), figment_array?))
}
serde_json::Value::Object(obj) => {
let mut figment_dict = Dict::new();
for (k, v) in obj {
figment_dict.insert(k, Self::json_value_to_figment_value(v)?);
}
Ok(Value::Dict(Tag::default(), figment_dict))
}
}
}
fn insert_nested_value(
dict: &mut Dict,
path_parts: &[&str],
value: Value,
) -> Result<(), Error> {
if path_parts.is_empty() {
return Ok(());
}
if path_parts.len() == 1 {
dict.insert(path_parts[0].to_string(), value);
return Ok(());
}
let key = path_parts[0];
let remaining_path = &path_parts[1..];
let nested_dict = dict
.entry(key.to_string())
.or_insert_with(|| Value::Dict(Tag::default(), Dict::new()));
match nested_dict {
Value::Dict(_, nested) => {
Self::insert_nested_value(nested, remaining_path, value)?;
}
_ => {
let mut new_dict = Dict::new();
Self::insert_nested_value(&mut new_dict, remaining_path, value)?;
*nested_dict = Value::Dict(Tag::default(), new_dict);
}
}
Ok(())
}
}
impl Default for Nested {
fn default() -> Self {
Self::new()
}
}
impl Provider for Nested {
fn metadata(&self) -> Metadata {
self.metadata.clone()
}
fn data(&self) -> Result<Map<Profile, Map<String, Value>>, Error> {
let mut dict = Dict::new();
for (key, value) in &self.env_vars {
let key_without_prefix = if !self.prefix.is_empty() && key.starts_with(&self.prefix) {
&key[self.prefix.len()..]
} else {
key
};
if key_without_prefix.is_empty() {
continue;
}
let path_parts: Vec<&str> = key_without_prefix.splitn(2, &self.separator).collect();
let path_parts: Vec<String> =
path_parts.iter().map(|part| part.to_lowercase()).collect();
let path_refs: Vec<&str> = path_parts.iter().map(|s| s.as_str()).collect();
let parsed_value = Self::parse_env_value(value)?;
Self::insert_nested_value(&mut dict, &path_refs, parsed_value)?;
}
Ok(Map::from([(Profile::Default, dict)]))
}
fn profile(&self) -> Option<Profile> {
None
}
}