use ortho_config::{
MapEnv, MergeComposer, MergeLayer, OrthoResult, SharedEnvSource, load_config_file_as_chain,
};
use std::borrow::Cow;
use std::ffi::OsString;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use super::parser::Cli;
#[path = "discovery_diagnostics.rs"]
mod diagnostics;
#[path = "discovery_paths.rs"]
mod paths;
#[path = "discovery_layers.rs"]
mod layers;
#[path = "discovery_trace.rs"]
mod trace;
use diagnostics::{BoundedConfigPath, ConfigLoadFailureKind, ConfigLoadWarning};
use layers::collect_file_layers_with_trace_and_env_source;
use trace::{DiscoveryDiagnostics, DiscoveryTrace, FileLayerTrace};
const CONFIG_ENV_VAR: &str = "NETSUKE_CONFIG";
const DISCOVERY_ENV_KEYS: [&str; 7] = [
CONFIG_ENV_VAR,
"HOME",
"USERPROFILE",
"XDG_CONFIG_HOME",
"XDG_CONFIG_DIRS",
"APPDATA",
"LOCALAPPDATA",
];
pub trait EnvProvider {
fn get(&self, key: &str) -> Option<OsString>;
fn entries(&self) -> Vec<(OsString, OsString)> {
Vec::new()
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct StdEnvProvider;
#[expect(
clippy::disallowed_methods,
reason = "composition root: StdEnvProvider is the process-backed adapter behind the EnvProvider seam"
)]
impl EnvProvider for StdEnvProvider {
fn get(&self, key: &str) -> Option<OsString> {
std::env::var_os(key)
}
fn entries(&self) -> Vec<(OsString, OsString)> {
std::env::vars_os().collect()
}
}
pub struct DiscoveredLayers {
layers: Vec<MergeLayer<'static>>,
json_preference: bool,
errors: Vec<Arc<ortho_config::OrthoError>>,
diagnostics: DiscoveryDiagnostics,
}
impl DiscoveredLayers {
#[cfg(test)]
pub(crate) fn layers(&self) -> &[MergeLayer<'static>] {
&self.layers
}
pub(crate) fn first_error(&self) -> Option<&Arc<ortho_config::OrthoError>> {
self.errors.first()
}
pub(crate) const fn json_preference(&self) -> bool {
self.json_preference
}
pub(crate) fn into_parts(
self,
) -> (Vec<MergeLayer<'static>>, Vec<Arc<ortho_config::OrthoError>>) {
(self.layers, self.errors)
}
}
pub struct DiscoveryOutcome {
layers: DiscoveredLayers,
}
impl DiscoveryOutcome {
#[cfg(test)]
pub(crate) fn layers(&self) -> &[MergeLayer<'static>] {
self.layers.layers()
}
pub(crate) fn first_error(&self) -> Option<&Arc<ortho_config::OrthoError>> {
self.layers.first_error()
}
pub(crate) const fn json_preference(&self) -> bool {
self.layers.json_preference()
}
#[must_use]
pub fn into_layers(self) -> DiscoveredLayers {
self.layers
}
pub fn emit_diagnostics(&self) {
self.layers.diagnostics.emit();
}
}
pub(crate) fn discover_file_layers(cli: &Cli, env: &impl EnvProvider) -> DiscoveryOutcome {
let (trace, load_warning, outcome) = collect_file_layers_with_env(cli, env);
let diagnostics = DiscoveryDiagnostics::new(trace, load_warning);
let layers = match outcome {
Ok(discovered_layers) => {
let (layers, json_preference) =
layers::retain_layers_and_resolve_json(discovered_layers);
DiscoveredLayers {
layers,
json_preference,
errors: Vec::new(),
diagnostics,
}
}
Err(error) => DiscoveredLayers {
layers: Vec::new(),
json_preference: Cli::default().json,
errors: vec![error],
diagnostics,
},
};
DiscoveryOutcome { layers }
}
pub(crate) fn push_discovered_file_layers(
composer: &mut MergeComposer,
errors: &mut Vec<Arc<ortho_config::OrthoError>>,
discovered: DiscoveredLayers,
) {
let (layers, discovery_errors) = discovered.into_parts();
errors.extend(discovery_errors);
for layer in layers {
composer.push_layer(layer);
}
}
fn collect_file_layers_with_env(
cli: &Cli,
env: &impl EnvProvider,
) -> (
DiscoveryTrace,
Option<ConfigLoadWarning>,
OrthoResult<Vec<MergeLayer<'static>>>,
) {
let resolution = resolve_config_selector(cli.config.clone(), env);
let (file_layers, load_warning, outcome) = resolution.path.as_deref().map_or_else(
|| {
let (project_scope, outcome) = collect_file_layers_with_trace_and_env_source(
cli.directory.as_deref(),
discovery_env_source(env),
);
(FileLayerTrace::Automatic { project_scope }, None, outcome)
},
|path| {
let (load_warning, outcome) = load_layers_from_path_with_warning(path);
(
FileLayerTrace::Explicit {
path: BoundedConfigPath::from_path(Some(path)),
},
load_warning,
outcome,
)
},
);
(
DiscoveryTrace::new(&resolution, file_layers),
load_warning,
outcome,
)
}
pub(crate) fn discovery_env_source(env: &impl EnvProvider) -> SharedEnvSource {
let mut source = MapEnv::new();
for key in DISCOVERY_ENV_KEYS {
if let Some(value) = env.get(key) {
source.insert(key, value);
}
}
Arc::new(source)
}
#[cfg(test)]
pub(crate) fn explicit_config_path_with_env(cli: &Cli, env: &impl EnvProvider) -> Option<PathBuf> {
resolve_config_selector(cli.config.clone(), env).path
}
#[derive(Debug, PartialEq, Eq)]
struct ConfigPathResolution {
selector: &'static str,
path: Option<PathBuf>,
environment_lookups: Vec<(&'static str, Option<PathBuf>)>,
}
fn resolve_config_selector(
cli_config: Option<PathBuf>,
env: &impl EnvProvider,
) -> ConfigPathResolution {
if let Some(path) = cli_config {
return ConfigPathResolution {
selector: "cli_flag",
path: Some(path),
environment_lookups: Vec::new(),
};
}
let primary_path = env_config_path(env, CONFIG_ENV_VAR);
ConfigPathResolution {
selector: primary_path.as_ref().map_or("none", |_| CONFIG_ENV_VAR),
environment_lookups: vec![(CONFIG_ENV_VAR, primary_path.clone())],
path: primary_path,
}
}
fn env_config_path(env: &impl EnvProvider, var_name: &str) -> Option<PathBuf> {
env.get(var_name)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
#[cfg(test)]
pub(crate) fn load_layers_from_path(path: &Path) -> OrthoResult<Vec<MergeLayer<'static>>> {
let (load_warning, result) = load_layers_from_path_with_warning(path);
if let Some(warning) = load_warning {
warning.emit();
}
result
}
fn load_layers_from_path_with_warning(
path: &Path,
) -> (
Option<ConfigLoadWarning>,
OrthoResult<Vec<MergeLayer<'static>>>,
) {
match load_config_file_as_chain(path) {
Ok(Some(chain)) => (
None,
Ok(chain
.values
.into_iter()
.map(|(value, layer_path)| MergeLayer::file(Cow::Owned(value), Some(layer_path)))
.collect()),
),
Ok(None) => {
let error = Arc::new(ortho_config::OrthoError::File {
path: path.to_path_buf(),
source: Box::new(io::Error::new(
io::ErrorKind::NotFound,
"explicit configuration file not found",
)),
});
(
Some(ConfigLoadWarning::new(path, ConfigLoadFailureKind::Missing)),
Err(error),
)
}
Err(error) => (
Some(ConfigLoadWarning::new(
path,
ConfigLoadFailureKind::LoadError,
)),
Err(error),
),
}
}
pub(crate) fn collect_diag_file_layers_with_env(
cli: &Cli,
env: &impl EnvProvider,
) -> DiscoveryOutcome {
discover_file_layers(cli, env)
}
#[cfg(test)]
#[path = "discovery_event_assertions.rs"]
mod event_assertions;
#[cfg(test)]
pub(crate) use event_assertions::assert_bounded_path_event;
#[cfg(test)]
#[path = "discovery_tracing_tests.rs"]
mod tracing_tests;
#[cfg(test)]
#[path = "discovery_layer_tests.rs"]
mod layer_tests;
#[cfg(test)]
#[path = "discovery_helper_proptests.rs"]
mod helper_proptests;
#[cfg(test)]
#[path = "discovery_replay_proptests.rs"]
mod replay_proptests;
#[cfg(test)]
#[path = "config_path_precedence_tests.rs"]
mod config_path_precedence_tests;
#[cfg(test)]
#[path = "discovery_unit_tests.rs"]
mod unit_tests;