use camino::Utf8PathBuf;
use serde_json::Value as JsonValue;
use crate::{OrthoError, OrthoMergeExt, OrthoResult};
use figment::Figment;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use super::error::{file_error, invalid_input};
use super::extends::{get_extends, process_extends};
use super::helpers::open_parent_dir_and_name;
use super::parser::parse_config_by_format;
use super::path::{canonicalise, normalize_cycle_key, resolve_base_path};
#[derive(Debug, Default)]
pub struct FileLayerChain {
pub values: Vec<(JsonValue, Utf8PathBuf)>,
}
fn strip_extends_key(value: &mut JsonValue) {
if let JsonValue::Object(map) = value {
map.remove("extends");
}
}
fn to_utf8_path(canonical: &Path) -> Utf8PathBuf {
Utf8PathBuf::from_path_buf(canonical.to_path_buf())
.unwrap_or_else(|p| Utf8PathBuf::from(p.to_string_lossy().into_owned()))
}
fn file_exists_and_is_regular(path: &Path) -> OrthoResult<Option<()>> {
let utf8_path = to_utf8_path(path);
let (dir, file_name) = match open_parent_dir_and_name(&utf8_path) {
Ok(pair) => pair,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(file_error(path, error)),
};
match dir.metadata(file_name) {
Ok(metadata) if metadata.is_file() => Ok(Some(())),
Ok(_) => Err(invalid_input(
path,
"configuration path is not a regular file",
)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(file_error(path, error)),
}
}
fn read_file_to_string(path: &Path) -> OrthoResult<String> {
let utf8_path = to_utf8_path(path);
let (dir, file_name) = open_parent_dir_and_name(&utf8_path).map_err(|e| file_error(path, e))?;
dir.read_to_string(file_name)
.map_err(|e| file_error(path, e))
}
pub fn load_config_file(path: &Path) -> OrthoResult<Option<Figment>> {
let mut visited = HashSet::new();
let mut stack = Vec::new();
load_config_file_inner(path, &mut visited, &mut stack)
}
fn with_cycle_detection<T, F>(
path: &Path,
visited: &mut HashSet<PathBuf>,
stack: &mut Vec<PathBuf>,
operation: F,
) -> OrthoResult<Option<T>>
where
F: FnOnce(&Path, &mut HashSet<PathBuf>, &mut Vec<PathBuf>) -> OrthoResult<T>,
{
if file_exists_and_is_regular(path)?.is_none() {
return Ok(None);
}
let canonical = canonicalise(path)?;
let normalized = normalize_cycle_key(&canonical);
if !visited.insert(normalized.clone()) {
let mut cycle: Vec<String> = stack.iter().map(|p| p.display().to_string()).collect();
cycle.push(canonical.display().to_string());
return Err(std::sync::Arc::new(OrthoError::CyclicExtends {
cycle: cycle.join(" -> "),
}));
}
stack.push(canonical.clone());
let result = operation(&canonical, visited, stack);
visited.remove(&normalized);
stack.pop();
result.map(Some)
}
pub(super) fn load_config_file_inner(
path: &Path,
visited: &mut HashSet<PathBuf>,
stack: &mut Vec<PathBuf>,
) -> OrthoResult<Option<Figment>> {
with_cycle_detection(
path,
visited,
stack,
|canonical, visited_paths, stack_paths| {
let data = read_file_to_string(canonical)?;
let figment = parse_config_by_format(canonical, &data)?;
process_extends(figment, canonical, visited_paths, stack_paths)
},
)
}
pub fn load_config_file_as_chain(path: &Path) -> OrthoResult<Option<FileLayerChain>> {
let mut visited = HashSet::new();
let mut stack = Vec::new();
with_cycle_detection(path, &mut visited, &mut stack, load_chain_for_file)
}
fn load_chain_for_file(
canonical: &Path,
visited: &mut HashSet<PathBuf>,
stack: &mut Vec<PathBuf>,
) -> OrthoResult<FileLayerChain> {
let data = read_file_to_string(canonical)?;
let figment = parse_config_by_format(canonical, &data)?;
let mut value: JsonValue = figment.extract().into_ortho_merge()?;
strip_extends_key(&mut value);
let utf8_path = to_utf8_path(canonical);
let parent_chain = if let Some(base) = get_extends(&figment, canonical)? {
let base_canonical = resolve_base_path(canonical, base)?;
let chain = with_cycle_detection(&base_canonical, visited, stack, load_chain_for_file)?;
if chain.is_none() {
return Err(file_error(
&base_canonical,
std::io::Error::new(std::io::ErrorKind::NotFound, "extended path does not exist"),
));
}
chain
} else {
None
};
let chain = match parent_chain {
Some(mut parent) => {
parent.values.push((value, utf8_path));
parent
}
None => FileLayerChain {
values: vec![(value, utf8_path)],
},
};
Ok(chain)
}