#[cfg(test)]
use ortho_config::MapEnv;
use ortho_config::{
ConfigDiscovery, MergeLayer, MergeProvenance, OrthoResult, SharedEnvSource,
load_config_file_as_chain,
};
use serde_json::Value;
use std::borrow::Cow;
use std::path::{Path, PathBuf};
#[cfg(test)]
use std::sync::Arc;
use super::super::parser::Cli;
use super::CONFIG_ENV_VAR;
use super::diagnostics::{BoundedConfigPath, debug_optional_config_path_from_fields};
use super::paths::{FsPathNormalizer, PathNormalizer, normalized_path_key};
pub(super) fn retain_layers_and_resolve_json(
layers: Vec<MergeLayer<'static>>,
) -> (Vec<MergeLayer<'static>>, bool) {
let mut json = Cli::default().json;
let mut retained = Vec::with_capacity(layers.len());
for layer in layers {
debug_assert_eq!(
layer.provenance(),
MergeProvenance::File,
"discovery must retain only file layers"
);
let path = layer.path().map(ToOwned::to_owned);
let value = layer.into_value();
if let Some(layer_json) = json_from_value(&value) {
json = layer_json;
}
retained.push(MergeLayer::file(Cow::Owned(value), path));
}
(retained, json)
}
fn json_from_value(value: &Value) -> Option<bool> {
value
.as_object()
.and_then(|map| map.get("json"))
.and_then(Value::as_bool)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum ProjectScopeTrace {
Included(BoundedConfigPath),
Appended(BoundedConfigPath),
}
impl ProjectScopeTrace {
pub(super) fn emit(&self) {
match self {
Self::Included(path) => {
debug_optional_config_path_from_fields(
"discovery included project-scope layers",
path,
);
}
Self::Appended(path) => {
debug_optional_config_path_from_fields("appending project-scope layers", path);
}
}
}
}
fn config_discovery(directory: Option<&PathBuf>, env_source: SharedEnvSource) -> ConfigDiscovery {
let mut builder = ConfigDiscovery::builder("netsuke")
.env_var(CONFIG_ENV_VAR)
.env_source(env_source);
if let Some(dir) = directory {
builder = builder.clear_project_roots().add_project_root(dir);
}
builder.build()
}
pub(super) fn collect_file_layers_with_trace_and_env_source(
directory: Option<&Path>,
env_source: SharedEnvSource,
) -> (
Option<ProjectScopeTrace>,
OrthoResult<Vec<MergeLayer<'static>>>,
) {
collect_file_layers_with_normalizer_and_trace(directory, &FsPathNormalizer, env_source)
}
fn comparison_key(normalizer: &impl PathNormalizer, path: &str) -> PathBuf {
normalized_path_key(normalizer, path).unwrap_or_else(|_| PathBuf::from(path))
}
#[cfg(test)]
pub(super) fn collect_file_layers_with_normalizer(
directory: Option<&Path>,
normalizer: &impl PathNormalizer,
) -> OrthoResult<Vec<MergeLayer<'static>>> {
let isolated_config_dirs = directory.map_or_else(
|| PathBuf::from(".netsuke-test-absent-xdg-config-dirs"),
|path| path.join(".netsuke-test-absent-xdg-config-dirs"),
);
let mut test_env = MapEnv::new();
test_env.insert("XDG_CONFIG_DIRS", isolated_config_dirs.into_os_string());
collect_file_layers_with_normalizer_and_trace(directory, normalizer, Arc::new(test_env)).1
}
fn collect_file_layers_with_normalizer_and_trace(
directory: Option<&Path>,
normalizer: &impl PathNormalizer,
env_source: SharedEnvSource,
) -> (
Option<ProjectScopeTrace>,
OrthoResult<Vec<MergeLayer<'static>>>,
) {
let discovery = config_discovery(directory.map(PathBuf::from).as_ref(), env_source);
let mut file_layers = discovery.compose_layers();
let mut errors = file_layers.required_errors;
if file_layers.value.is_empty() {
errors.append(&mut file_layers.optional_errors);
}
if let Some(err) = errors.into_iter().next() {
return (None, Err(err));
}
let project_file = project_scope_file(directory);
let project_key = project_file
.as_deref()
.map(|path| comparison_key(normalizer, &path.to_string_lossy()));
let has_project_layer = file_layers.value.iter().any(|layer| {
layer.path().is_some_and(|path| {
project_key
.as_deref()
.is_some_and(|key| key.to_string_lossy() == path.as_str())
})
});
let project_trace_path = BoundedConfigPath::from_path(project_file.as_deref());
if has_project_layer {
return (
Some(ProjectScopeTrace::Included(project_trace_path)),
Ok(file_layers.value),
);
}
let trace = ProjectScopeTrace::Appended(project_trace_path);
let result = project_scope_layers(project_file.as_deref()).map(|project_layers| {
file_layers
.value
.into_iter()
.chain(project_layers)
.collect()
});
(Some(trace), result)
}
fn project_scope_file(directory: Option<&Path>) -> Option<PathBuf> {
let root = directory
.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok())?;
Some(root.join(".netsuke.toml"))
}
fn project_scope_layers(project_file: Option<&Path>) -> OrthoResult<Vec<MergeLayer<'static>>> {
let Some(path) = project_file else {
return Ok(Vec::new());
};
match load_config_file_as_chain(path) {
Ok(Some(chain)) => Ok(chain
.values
.into_iter()
.map(|(value, layer_path)| MergeLayer::file(Cow::Owned(value), Some(layer_path)))
.collect()),
Ok(None) => Ok(Vec::new()),
Err(err) => Err(err),
}
}