use std::borrow::Cow;
use std::io;
use std::path::Path;
use std::sync::Arc;
use camino::Utf8PathBuf;
use crate::{
MergeLayer, OrthoError, OrthoMergeExt, OrthoResult, load_config_file, load_config_file_as_chain,
};
use super::outcome::DiscoveryOutcome;
use super::telemetry;
use super::{ConfigDiscovery, DiscoveryLayerOutcome, DiscoveryLayersOutcome, DiscoveryLoadOutcome};
#[derive(Debug, Default)]
struct PartitionedErrors {
required: Vec<Arc<OrthoError>>,
optional: Vec<Arc<OrthoError>>,
}
struct CandidateFailure {
operation: &'static str,
required: bool,
source: &'static str,
}
impl PartitionedErrors {
fn record(&mut self, failure: &CandidateFailure, err: Arc<OrthoError>) {
telemetry::candidate_failure(
failure.operation,
failure.required,
failure.source,
telemetry::error_category(&err),
);
if failure.required {
self.required.push(err);
} else {
self.optional.push(err);
}
}
fn into_outcome<T>(self, value: Option<T>) -> DiscoveryOutcome<T> {
DiscoveryOutcome {
value,
required_errors: self.required,
optional_errors: self.optional,
}
}
fn into_layers_outcome(self, value: Vec<MergeLayer<'static>>) -> DiscoveryLayersOutcome {
DiscoveryLayersOutcome {
value,
required_errors: self.required,
optional_errors: self.optional,
}
}
}
impl ConfigDiscovery {
fn try_candidate<T, F>(
path: &Path,
required: bool,
build: &mut F,
) -> Result<Option<T>, Arc<OrthoError>>
where
F: FnMut(figment::Figment, &Path) -> Result<T, Arc<OrthoError>>,
{
match load_config_file(path)? {
Some(figment) => build(figment, path).map(Some),
None if required => Err(Self::missing_required_error(path)),
None => Ok(None),
}
}
fn walk_candidates<T>(
&self,
operation: &'static str,
mut try_one: impl FnMut(&Path, bool) -> Result<Option<T>, Arc<OrthoError>>,
) -> (Option<T>, PartitionedErrors) {
telemetry::attempt(operation);
let mut errors = PartitionedErrors::default();
let set = self.candidate_set();
set.decisions.emit();
for (idx, candidate) in set.candidates.into_iter().enumerate() {
let required = Self::is_required_candidate(idx, set.required_bound);
match try_one(&candidate.path, required) {
Ok(Some(value)) => {
telemetry::load_outcome(
operation,
telemetry::OUTCOME_SUCCESS,
Some(candidate.source),
);
return (Some(value), errors);
}
Ok(None) => {}
Err(err) => errors.record(
&CandidateFailure {
operation,
required,
source: candidate.source,
},
err,
),
}
}
telemetry::load_outcome(operation, telemetry::OUTCOME_NOT_FOUND, None);
(None, errors)
}
fn discover_first<T, F>(&self, mut build: F) -> DiscoveryOutcome<T>
where
F: FnMut(figment::Figment, &Path) -> Result<T, Arc<OrthoError>>,
{
let (value, errors) = self
.walk_candidates(telemetry::OPERATION_DISCOVER_FIRST, |path, required| {
Self::try_candidate(path, required, &mut build)
});
errors.into_outcome(value)
}
const fn is_required_candidate(idx: usize, required_bound: usize) -> bool {
idx < required_bound
}
pub fn load_first(&self) -> OrthoResult<Option<figment::Figment>> {
let (figment, errors) = self.load_first_with_errors();
if let Some(found_figment) = figment {
return Ok(Some(found_figment));
}
if let Some(err) = OrthoError::try_aggregate(errors) {
return Err(Arc::new(err));
}
Ok(None)
}
pub fn load_first_partitioned(&self) -> DiscoveryLoadOutcome {
let outcome = self.discover_first(|figment, _| Ok(figment));
DiscoveryLoadOutcome {
figment: outcome.value,
required_errors: outcome.required_errors,
optional_errors: outcome.optional_errors,
}
}
pub fn compose_layer(&self) -> DiscoveryLayerOutcome {
let outcome = self.discover_first(|figment, path| {
figment
.extract::<crate::serde_json::Value>()
.into_ortho_merge()
.map(|value| {
let utf8_path = Utf8PathBuf::from_path_buf(path.to_path_buf())
.ok()
.unwrap_or_else(|| Utf8PathBuf::from(path.to_string_lossy().into_owned()));
MergeLayer::file(Cow::Owned(value), Some(utf8_path))
})
});
DiscoveryLayerOutcome {
value: outcome.value,
required_errors: outcome.required_errors,
optional_errors: outcome.optional_errors,
}
}
pub fn compose_layers(&self) -> DiscoveryLayersOutcome {
let (value, errors) =
self.walk_candidates(telemetry::OPERATION_COMPOSE_LAYERS, Self::chain_layers);
errors.into_layers_outcome(value.unwrap_or_default())
}
fn chain_layers(
path: &Path,
required: bool,
) -> Result<Option<Vec<MergeLayer<'static>>>, Arc<OrthoError>> {
match load_config_file_as_chain(path)? {
Some(chain) => Ok(Some(
chain
.values
.into_iter()
.map(|(value, layer_path)| {
MergeLayer::file(Cow::Owned(value), Some(layer_path))
})
.collect(),
)),
None if required => Err(Self::missing_required_error(path)),
None => Ok(None),
}
}
#[must_use]
pub fn load_first_with_errors(&self) -> (Option<figment::Figment>, Vec<Arc<OrthoError>>) {
let DiscoveryLoadOutcome {
figment,
mut required_errors,
mut optional_errors,
} = self.load_first_partitioned();
required_errors.append(&mut optional_errors);
(figment, required_errors)
}
fn missing_required_error(path: &Path) -> Arc<OrthoError> {
Arc::new(OrthoError::File {
path: path.to_path_buf(),
source: Box::new(io::Error::new(
io::ErrorKind::NotFound,
"required configuration file not found",
)),
})
}
}