use figment::{
Error, Metadata, Profile, Provider,
providers::Format,
value::{Map, Value},
};
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::UNIX_EPOCH,
};
#[derive(Debug, Clone)]
struct FormatCacheEntry {
format: ConfigFormat,
modified_time: u64,
}
type FormatCache = Arc<Mutex<HashMap<PathBuf, FormatCacheEntry>>>;
lazy_static::lazy_static! {
static ref FORMAT_CACHE: FormatCache = Arc::new(Mutex::new(HashMap::new()));
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ConfigFormat {
Json,
Toml,
Yaml,
}
pub struct Universal {
provider: Box<dyn Provider>,
}
impl Universal {
pub fn file<P: AsRef<Path>>(path: P) -> Self {
let path = path.as_ref();
if path.exists() {
if let Some(provider) = Self::try_extension_detection(path) {
return Self { provider };
}
if let Some(provider) = Self::try_cached_content_detection(path) {
return Self { provider };
}
if let Some(provider) = Self::try_all_formats(path) {
return Self { provider };
}
}
if let Some(provider) = Self::try_multiple_extensions(path) {
return Self { provider };
}
Self::empty_provider()
}
pub fn string<S: AsRef<str>>(content: S) -> Self {
let content = content.as_ref();
let format = Self::detect_format_from_content(content);
let provider: Box<dyn Provider> = match format {
ConfigFormat::Json => Box::new(figment::providers::Json::string(content)),
ConfigFormat::Toml => Box::new(figment::providers::Toml::string(content)),
ConfigFormat::Yaml => Box::new(figment::providers::Yaml::string(content)),
};
Self { provider }
}
pub fn file_with_extensions<P: AsRef<Path>>(base_path: P) -> Self {
Self::try_multiple_extensions(base_path.as_ref())
.map(|provider| Self { provider })
.unwrap_or_else(Self::empty_provider)
}
fn try_extension_detection(path: &Path) -> Option<Box<dyn Provider>> {
let extension = path.extension()?.to_str()?.to_lowercase();
match extension.as_str() {
"json" => Some(Box::new(figment::providers::Json::file(path))),
"toml" => Some(Box::new(figment::providers::Toml::file(path))),
"yaml" | "yml" => Some(Box::new(figment::providers::Yaml::file(path))),
_ => None,
}
}
fn try_cached_content_detection(path: &Path) -> Option<Box<dyn Provider>> {
let modified_time = fs::metadata(path)
.and_then(|meta| meta.modified())
.map(|time| {
time.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
})
.unwrap_or(0);
if let Ok(cache) = FORMAT_CACHE.lock() {
if let Some(entry) = cache.get(path) {
if entry.modified_time == modified_time {
return Self::create_provider_for_format(path, entry.format);
}
}
}
let content = fs::read_to_string(path).ok()?;
let format = Self::detect_format_from_content(&content);
if let Ok(mut cache) = FORMAT_CACHE.lock() {
cache.insert(
path.to_path_buf(),
FormatCacheEntry {
format,
modified_time,
},
);
}
Self::create_provider_for_format(path, format)
}
fn try_all_formats(path: &Path) -> Option<Box<dyn Provider>> {
let formats: [Box<dyn Fn() -> Box<dyn Provider>>; 3] = [
Box::new(|| Box::new(figment::providers::Toml::file(path)) as Box<dyn Provider>),
Box::new(|| Box::new(figment::providers::Yaml::file(path)) as Box<dyn Provider>),
Box::new(|| Box::new(figment::providers::Json::file(path)) as Box<dyn Provider>),
];
for create_provider in &formats {
let provider = create_provider();
if provider.data().is_ok() {
return Some(provider);
}
}
None
}
fn create_provider_for_format(path: &Path, format: ConfigFormat) -> Option<Box<dyn Provider>> {
match format {
ConfigFormat::Json => Some(Box::new(figment::providers::Json::file(path))),
ConfigFormat::Toml => Some(Box::new(figment::providers::Toml::file(path))),
ConfigFormat::Yaml => Some(Box::new(figment::providers::Yaml::file(path))),
}
}
fn try_multiple_extensions(base_path: &Path) -> Option<Box<dyn Provider>> {
let extensions = ["toml", "yaml", "yml", "json"];
for ext in &extensions {
let path_with_ext = base_path.with_extension(ext);
if path_with_ext.exists() {
if let Some(provider) = Self::try_extension_detection(&path_with_ext) {
return Some(provider);
}
if let Some(provider) = Self::try_cached_content_detection(&path_with_ext) {
return Some(provider);
}
if let Some(provider) = Self::try_all_formats(&path_with_ext) {
return Some(provider);
}
}
}
None
}
fn detect_format_from_content(content: &str) -> ConfigFormat {
let trimmed = content.trim();
if Self::is_toml_format(trimmed) {
ConfigFormat::Toml
} else if Self::is_yaml_format(trimmed) {
ConfigFormat::Yaml
} else if Self::is_json_format(trimmed) {
ConfigFormat::Json
} else {
ConfigFormat::Toml
}
}
fn is_toml_format(content: &str) -> bool {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') && line.ends_with(']') {
return true;
}
if line.contains('=') && !line.contains(':') {
return true;
}
}
false
}
fn is_yaml_format(content: &str) -> bool {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line == "---" {
return true;
}
if let Some(colon_pos) = line.find(':') {
let before_colon = &line[..colon_pos];
let after_colon = &line[colon_pos + 1..];
if before_colon.ends_with("http") || before_colon.ends_with("https") {
continue;
}
if !before_colon.contains('=')
&& (after_colon.starts_with(' ') || after_colon.is_empty())
{
return true;
}
}
}
false
}
fn is_json_format(content: &str) -> bool {
if content.starts_with('{') && content.ends_with('}') {
return true;
}
if content.starts_with('[') && content.ends_with(']') {
let lines: Vec<&str> = content.lines().collect();
if lines.len() == 1 || content.contains(',') || content.contains('"') {
return true;
}
}
false
}
fn empty_provider() -> Self {
Self {
provider: Box::new(figment::providers::Serialized::defaults(())),
}
}
pub fn clear_cache() {
if let Ok(mut cache) = FORMAT_CACHE.lock() {
cache.clear();
}
}
}
impl Provider for Universal {
fn metadata(&self) -> Metadata {
let inner_metadata = self.provider.metadata();
let format = inner_metadata.name;
let metadata_name = format!("Format::Universal::{format}");
let mut metadata = Metadata::named(metadata_name);
if let Some(source) = inner_metadata.source {
metadata = metadata.source(source);
}
metadata
}
fn data(&self) -> Result<Map<Profile, Map<String, Value>>, Error> {
self.provider.data()
}
fn profile(&self) -> Option<Profile> {
self.provider.profile()
}
}