use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::env_source::{SharedEnvSource, process_env_source};
use super::ConfigDiscovery;
use super::telemetry;
type ProjectRootResolver = Arc<dyn Fn() -> std::io::Result<PathBuf> + Send + Sync>;
#[derive(Clone)]
pub struct ConfigDiscoveryBuilder {
env_var: Option<String>,
app_name: String,
config_file_name: String,
custom_dotfile_name: Option<String>,
custom_project_file_name: Option<String>,
project_roots: Vec<PathBuf>,
explicit_paths: Vec<PathBuf>,
required_explicit_paths: Vec<PathBuf>,
env_source: Option<SharedEnvSource>,
project_root_resolver: ProjectRootResolver,
}
impl std::fmt::Debug for ConfigDiscoveryBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConfigDiscoveryBuilder")
.field("env_var", &self.env_var)
.field("app_name", &self.app_name)
.field("config_file_name", &self.config_file_name)
.field("custom_dotfile_name", &self.custom_dotfile_name)
.field("custom_project_file_name", &self.custom_project_file_name)
.field("project_roots", &self.project_roots.len())
.field("explicit_paths", &self.explicit_paths.len())
.field(
"required_explicit_paths",
&self.required_explicit_paths.len(),
)
.field("env_source_injected", &self.env_source.is_some())
.finish_non_exhaustive()
}
}
impl ConfigDiscoveryBuilder {
#[must_use]
pub fn new(app_name: impl Into<String>) -> Self {
Self {
env_var: None,
app_name: app_name.into(),
config_file_name: String::from("config.toml"),
custom_dotfile_name: None,
custom_project_file_name: None,
project_roots: Vec::new(),
explicit_paths: Vec::new(),
required_explicit_paths: Vec::new(),
env_source: None,
project_root_resolver: Arc::new(std::env::current_dir),
}
}
#[cfg(test)]
pub(crate) fn with_project_root_resolver(mut self, resolver: ProjectRootResolver) -> Self {
self.project_root_resolver = resolver;
self
}
#[must_use]
pub fn env_source(mut self, env_source: SharedEnvSource) -> Self {
self.env_source = Some(env_source);
self
}
#[must_use]
pub fn env_var(mut self, env_var: impl Into<String>) -> Self {
self.env_var = Some(env_var.into());
self
}
#[must_use]
pub fn config_file_name(mut self, name: impl Into<String>) -> Self {
self.config_file_name = name.into();
self
}
#[must_use]
pub fn dotfile_name(mut self, name: impl Into<String>) -> Self {
self.custom_dotfile_name = Some(name.into());
self
}
#[must_use]
pub fn project_file_name(mut self, name: impl Into<String>) -> Self {
self.custom_project_file_name = Some(name.into());
self
}
#[must_use]
pub fn clear_project_roots(mut self) -> Self {
self.project_roots.clear();
self
}
#[must_use]
pub fn project_roots<I, P>(mut self, roots: I) -> Self
where
I: IntoIterator<Item = P>,
P: Into<PathBuf>,
{
self.project_roots = roots.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn add_project_root(mut self, root: impl Into<PathBuf>) -> Self {
self.project_roots.push(root.into());
self
}
#[must_use]
pub fn add_explicit_path(mut self, path: impl Into<PathBuf>) -> Self {
self.explicit_paths.push(path.into());
self
}
#[must_use]
pub fn add_required_path(mut self, path: impl Into<PathBuf>) -> Self {
self.required_explicit_paths.push(path.into());
self
}
fn default_dotfile(&self) -> String {
let stem = self.app_name.trim();
let extension = Path::new(&self.config_file_name)
.extension()
.and_then(|ext| ext.to_str())
.filter(|ext| !ext.is_empty());
if stem.is_empty() {
let mut name = String::from('.');
name.push_str(extension.unwrap_or("config"));
return name;
}
let mut name = String::from('.');
name.push_str(stem);
if let Some(ext) = extension {
name.push('.');
name.push_str(ext);
}
name
}
#[must_use]
pub fn build(self) -> ConfigDiscovery {
let default_dotfile = self.default_dotfile();
let dotfile_name = self.custom_dotfile_name.unwrap_or(default_dotfile);
let project_file_name = self
.custom_project_file_name
.unwrap_or_else(|| dotfile_name.clone());
telemetry::source_selected(if self.env_source.is_some() {
telemetry::SOURCE_INJECTED
} else {
telemetry::SOURCE_PROCESS
});
let mut project_roots = self.project_roots;
if project_roots.is_empty() {
match (self.project_root_resolver)() {
Ok(dir) => project_roots.push(dir),
Err(_) => telemetry::project_root_cwd_unavailable(),
}
}
ConfigDiscovery {
env_var: self.env_var,
explicit_paths: self.explicit_paths,
required_explicit_paths: self.required_explicit_paths,
app_name: self.app_name,
config_file_name: self.config_file_name,
dotfile_name,
project_file_name,
project_roots,
env_source: self.env_source.unwrap_or_else(process_env_source),
}
}
}