use std::{collections::HashMap, fs, ops::ControlFlow};
use camino::{Utf8Path, Utf8PathBuf};
use relative_path::{RelativePath, RelativePathBuf};
use serde::Deserialize;
use tracing::debug;
use crate::{filesystem::WrapToPath, next::errors::WrapConfigErr};
use super::errors::ConfigError;
#[derive(Deserialize, Clone, Debug)]
pub(crate) struct ConfigVar {
pub(crate) key: String,
pub(crate) env_name: String,
}
#[derive(Deserialize, Debug)]
pub(crate) struct ConfigScope {
pub(crate) name: Option<String>,
pub(crate) sub: Option<String>,
pub(crate) service: Option<String>,
pub(crate) require_hash: Option<bool>,
}
#[derive(Deserialize, Debug, Default)]
pub(crate) struct ArgumentConfig {
pub(crate) scope: Option<ConfigScope>,
pub(crate) executable: Option<String>,
pub(crate) execution_path: Option<String>,
pub(crate) envs: Option<Vec<ConfigVar>>,
}
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub(crate) enum ConfigAddress {
Local {
path: String,
state_path: Option<String>,
},
Git {
url: String,
local: Option<bool>,
git_ref: String,
target_path: Option<String>,
state_path: Option<String>,
},
}
#[derive(Deserialize, Debug)]
pub(crate) struct StateConfig {
pub(crate) address: Option<ConfigAddress>,
}
#[derive(Deserialize, Debug, Default)]
pub(crate) struct Config {
pub(crate) argument: Option<ArgumentConfig>,
pub(crate) state: Option<StateConfig>,
}
pub(crate) fn load_dploy_config(config_dir_path: &Utf8Path) -> Result<Config, ConfigError> {
let toml_path = config_dir_path.join("tidploy.toml");
let json_path = config_dir_path.join("tidploy.json");
let choose_json = json_path.exists();
let file_path = if choose_json { json_path } else { toml_path };
if !file_path.exists() {
debug!("No config exists at path {:?}", file_path);
return Ok(Config::default());
}
let config_str = fs::read_to_string(&file_path)
.to_config_err(format!("Failed to read config file at {:?}", &file_path))?;
let dploy_config: Config = if choose_json {
serde_json::from_str(&config_str).to_config_err(format!(
"Failed to deserialize file {:?} to JSON",
&file_path
))?
} else {
toml::from_str(&config_str).to_config_err(format!(
"Failed to deserialize file {:?} to JSON",
&file_path
))?
};
debug!("Loaded config at path {:?}: {:?}", file_path, dploy_config);
Ok(dploy_config)
}
pub(crate) fn overwrite_option<T>(original: Option<T>, replacing: Option<T>) -> Option<T> {
match replacing {
Some(replacing) => Some(replacing),
None => original,
}
}
pub(crate) fn merge_option<T>(
original: Option<T>,
replacing: Option<T>,
merge_fn: &dyn Fn(T, T) -> T,
) -> Option<T> {
match original {
Some(original) => match replacing {
Some(replacing) => Some(merge_fn(original, replacing)),
None => Some(original),
},
None => replacing,
}
}
fn overwrite_scope(original: ConfigScope, replacing: ConfigScope) -> ConfigScope {
ConfigScope {
name: overwrite_option(original.name, replacing.name),
sub: overwrite_option(original.sub, replacing.sub),
service: overwrite_option(original.service, replacing.service),
require_hash: overwrite_option(original.require_hash, replacing.require_hash),
}
}
pub(crate) fn merge_vars(
root_vars: Vec<ConfigVar>,
overwrite_vars: Vec<ConfigVar>,
) -> Vec<ConfigVar> {
let mut vars_map: HashMap<String, String> = root_vars
.iter()
.map(|v| (v.key.clone(), v.env_name.clone()))
.collect();
for cfg_var in overwrite_vars {
vars_map.insert(cfg_var.key, cfg_var.env_name);
}
vars_map
.into_iter()
.map(|(k, v)| ConfigVar {
env_name: v,
key: k,
})
.collect()
}
fn overwrite_arguments(
root_config: ArgumentConfig,
overwrite_config: ArgumentConfig,
) -> ArgumentConfig {
let scope = merge_option(root_config.scope, overwrite_config.scope, &overwrite_scope);
let execution_path =
overwrite_option(root_config.execution_path, overwrite_config.execution_path);
let executable = overwrite_option(root_config.executable, overwrite_config.executable);
let envs = merge_option(root_config.envs, overwrite_config.envs, &merge_vars);
ArgumentConfig {
scope,
executable,
execution_path,
envs,
}
}
fn overwrite_arg_config(
root_config: Option<ArgumentConfig>,
overwrite_config: Option<ArgumentConfig>,
) -> Option<ArgumentConfig> {
merge_option(root_config, overwrite_config, &overwrite_arguments)
}
pub(crate) fn get_component_paths(
start_path: &Utf8Path,
final_path: &RelativePath,
) -> Vec<Utf8PathBuf> {
let paths: Vec<Utf8PathBuf> = final_path
.normalize()
.components()
.scan(RelativePathBuf::new(), |state, component| {
state.push(component);
Some(state.to_utf8_path(start_path))
})
.collect();
paths
}
pub(crate) fn traverse_arg_configs(
start_path: &Utf8Path,
final_path: &RelativePath,
) -> Result<Option<ArgumentConfig>, ConfigError> {
debug!(
"Traversing configs from {:?} to relative {:?}",
start_path, final_path
);
let root_config = load_dploy_config(start_path)?.argument;
let paths = get_component_paths(start_path, final_path);
let combined_config = paths.iter().try_fold(root_config, |state, path| {
let inner_config = load_dploy_config(path).map(|c| c.argument);
match inner_config {
Ok(config) => ControlFlow::Continue(overwrite_arg_config(state, config)),
Err(source) => ControlFlow::Break(source),
}
});
match combined_config {
ControlFlow::Break(e) => Err(e),
ControlFlow::Continue(config) => Ok(config),
}
}
#[cfg(test)]
mod tests {
use std::env;
use camino::Utf8PathBuf;
use relative_path::RelativePathBuf;
use super::get_component_paths;
#[test]
fn paths_simple() {
let path = Utf8PathBuf::from_path_buf(env::current_dir().unwrap()).unwrap();
let relative1 = RelativePathBuf::from("./this/that");
let relative2 = RelativePathBuf::from("this/that");
let paths1 = get_component_paths(&path, &relative1);
let paths2 = get_component_paths(&path, &relative2);
let comp2 = path.join("this").join("that");
let comp1 = path.join("this");
assert_eq!(vec![comp1.clone(), comp2.clone()], paths1);
assert_eq!(vec![comp1, comp2], paths2);
}
}