use crate::bones::parse_directive_str;
use crate::default_shells::get_default_shells;
use crate::schema;
use crate::version::{get_version_parts, VersionCompatibility, VersionDifference, BONNIE_VERSION};
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
version: String, env_files: Option<Vec<String>>, default_shell: Option<DefaultShell>,
scripts: Scripts,
}
impl Config {
pub fn new(cfg_string: &str) -> Result<Self, String> {
let cfg: Result<Self, toml::de::Error> = toml::from_str(cfg_string);
let cfg = match cfg {
Ok(cfg) => cfg,
Err(err) if err.to_string().starts_with("missing field `version`") => return Err("Your Bonnie configuration file appears to be missing a 'version' key. From Bonnie 0.2.0 onwards, this key is mandatory for compatibility reasons. Please add `version = \"".to_string() + BONNIE_VERSION + "\"` to the top of your Bonnie configuration file."),
Err(err) => return Err(format!("Invalid Bonnie configuration file. Error: '{}'", err))
};
Ok(cfg)
}
pub fn to_final(
&self,
bonnie_version_str: &str,
output: &mut impl std::io::Write,
) -> Result<schema::Config, String> {
Self::parse_version_against_current(&self.version, bonnie_version_str, output)?;
Self::load_env_files(self.env_files.clone())?;
let cfg = self.parse()?;
Ok(cfg)
}
pub fn parse_version_against_current(
cfg_version_str: &str,
bonnie_version_str: &str,
output: &mut impl std::io::Write,
) -> Result<(), String> {
let bonnie_version = get_version_parts(bonnie_version_str)?;
let cfg_version = get_version_parts(cfg_version_str)?;
let compat = bonnie_version.is_compatible_with(&cfg_version);
match compat {
VersionCompatibility::DifferentBetaVersion(version_difference) => return Err("The provided configuration file is incompatible with this version of Bonnie. You are running Bonnie v".to_string() + bonnie_version_str + ", but the configuration file expects Bonnie v" + cfg_version_str + ". " + match version_difference {
VersionDifference::TooNew => "This issue can be fixed by updating Bonnie to the appropriate version, which can be done at https://github.com/arctic-hen7/bonnie/releases.",
VersionDifference::TooOld => "This issue can be fixed by updating the configuration file, which may require changing some of its syntax (see https://github.com/arctic-hen7/bonnie for how to do so). Alternatively, you can download an older version of Bonnie from https://github.com/arctic-hen7/bonnie/releases (not recommended)."
}),
VersionCompatibility::DifferentMajor(version_difference) => return Err("The provided configuration file is incompatible with this version of Bonnie. You are running Bonnie v".to_string() + bonnie_version_str + ", but the configuration file expects Bonnie v" + cfg_version_str + ". " + match version_difference {
VersionDifference::TooNew => "This issue can be fixed by updating Bonnie to the appropriate version, which can be done at https://github.com/arctic-hen7/bonnie/releases.",
VersionDifference::TooOld => "This issue can be fixed by updating the configuration file, which may require changing some of its syntax (see https://github.com/arctic-hen7/bonnie for how to do so). Alternatively, you can download an older version of Bonnie from https://github.com/arctic-hen7/bonnie/releases (not recommended)."
}),
VersionCompatibility::DifferentMinor(version_difference) => writeln!(output, "{}", "The provided configuration file is compatible with this version of Bonnie, but has a different minor version. You are running Bonnie v".to_string() + bonnie_version_str + ", but the configuration file expects Bonnie v" + cfg_version_str + ". " + match version_difference {
VersionDifference::TooNew => "This issue can be fixed by updating Bonnie to the appropriate version, which can be done at https://github.com/arctic-hen7/bonnie/releases.",
VersionDifference::TooOld => "This issue can be fixed by updating the configuration file, which may require changing some of its syntax (see https://github.com/arctic-hen7/bonnie for how to do so). Alternatively, you can download an older version of Bonnie from https://github.com/arctic-hen7/bonnie/releases (not recommended)."
}).expect("Failed to write warning."),
VersionCompatibility::DifferentPatch(version_difference) => writeln!(output, "{}", "The provided configuration file is compatible with this version of Bonnie, but has a different patch version. You are running Bonnie v".to_string() + bonnie_version_str + ", but the configuration file expects Bonnie v" + cfg_version_str + ". " + match version_difference {
VersionDifference::TooNew => "You may want to update Bonnie to the appropriate version, which can be done at https://github.com/arctic-hen7/bonnie/releases.",
VersionDifference::TooOld => "You may want to update the configuration file (which shouldn't require any syntax changes)."
}).expect("Failed to write warning."),
_ => ()
};
Ok(())
}
pub fn load_env_files(env_files: Option<Vec<String>>) -> Result<(), String> {
let env_files = match env_files {
Some(env_files) => env_files,
None => Vec::new(),
};
for env_file in env_files.iter() {
let res = dotenv::from_filename(&env_file);
if res.is_err() {
return Err(format!("Requested environment variable file '{}' could not be loaded. Either the file doesn't exist, Bonnie doesn't have the permissions necessary to access it, or something inside it can't be processed.", &env_file));
}
}
Ok(())
}
fn parse(&self) -> Result<schema::Config, String> {
let default_shell = match &self.default_shell {
Some(DefaultShell::Simple(generic)) => schema::DefaultShell {
generic: generic.parse(),
targets: HashMap::new(),
},
Some(DefaultShell::Complex { generic, targets }) => schema::DefaultShell {
generic: generic.parse(),
targets: match targets {
Some(raw_targets) => {
let mut targets = HashMap::new();
for (target_name, shell) in raw_targets.iter() {
targets.insert(target_name.to_string(), shell.parse());
}
targets
}
None => HashMap::new(), },
},
None => get_default_shells(),
};
fn parse_scripts(
raw_scripts: &Scripts,
is_order_defined: bool,
) -> Result<schema::Scripts, String> {
let mut scripts: schema::Scripts = HashMap::new();
for (script_name, raw_command) in raw_scripts.iter() {
let command = match raw_command {
Command::Simple(raw_command_wrapper) => schema::Command {
args: Vec::new(),
env_vars: Vec::new(),
subcommands: None,
order: None,
cmd: Some(raw_command_wrapper.parse()), description: None
},
Command::Complex {
args,
env_vars,
subcommands,
order,
cmd,
desc
} => schema::Command {
args: match is_order_defined {
_ if matches!(subcommands, Some(_)) && matches!(order, None) && matches!(args, Some(_)) => return Err(format!("Error in parsing Bonnie configuration file: if `subcommands` is specified without `order`, `args` cannot be specified. This error occurred in in the '{}' script/subscript.", script_name)),
true if matches!(args, Some(_)) => return Err(format!("Error in parsing Bonnie configuration file: if `order` is specified, subscripts cannot specify `args`, as no environment variables can be provided to them. Environment variables to be interpolated in ordered subcommands must be set at the top-level. This error occurred in the '{}' script/subscript.", script_name)),
true => Vec::new(),
false => args.as_ref().unwrap_or(&Vec::new()).to_vec()
},
env_vars: env_vars.as_ref().unwrap_or(&Vec::new()).to_vec(),
subcommands: match subcommands {
Some(subcommands) => Some(
parse_scripts(subcommands, matches!(order, Some(_)))?
),
None => None
},
order: match is_order_defined {
true if matches!(subcommands, Some(_)) => match order {
Some(order) => Some(parse_directive_str(order)?),
None => return Err(format!("Error in parsing Bonnie configuration file: if `order` is specified, all further nested subsubcommands must also specify `order`. This occurred in the '{}' script/subscript.", script_name))
}
true | false => match order {
Some(order) => Some(parse_directive_str(order)?),
None => None
}
},
cmd: match cmd {
Some(_) if matches!(order, Some(_)) => return Err(format!("Error in parsing Bonnie configuration file: both `cmd` and `order` were specified. This would lead to problems of ambiguous execution, so commands can have either the top-level `cmd` property or ordered subcommands, the two are mutually exclusive. This error occurred in in the '{}' script/subscript.", script_name)),
_ if matches!(subcommands, Some(_)) => cmd.as_ref().map(|cmd| cmd.parse()),
Some(cmd) => Some(cmd.parse()),
None => return Err(format!("Error in parsing Bonnie configuration file: if `subcommands` is not specified, `cmd` is mandatory. This error occurred in in the '{}' script/subscript.", script_name))
},
description: desc.clone()
},
};
scripts.insert(script_name.to_string(), command);
}
Ok(scripts)
}
let scripts = parse_scripts(&self.scripts, false)?;
Ok(schema::Config {
default_shell,
scripts,
env_files: match &self.env_files {
Some(env_files) => env_files.to_vec(),
None => Vec::new(),
},
version: self.version.clone(),
})
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum DefaultShell {
Simple(Shell), Complex {
generic: Shell, targets: Option<HashMap<String, Shell>>,
},
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum Shell {
Simple(Vec<String>),
WithDelimiter {
parts: Vec<String>,
delimiter: String,
},
}
impl Shell {
fn parse(&self) -> schema::Shell {
match self {
Shell::Simple(parts) => schema::Shell {
parts: parts.to_vec(),
delimiter: " && ".to_string(),
},
Shell::WithDelimiter { parts, delimiter } => schema::Shell {
parts: parts.to_vec(),
delimiter: delimiter.to_string(),
},
}
}
}
type TargetString = String; type Scripts = HashMap<String, Command>;
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum Command {
Simple(CommandWrapper), Complex {
args: Option<Vec<String>>,
env_vars: Option<Vec<String>>,
subcommands: Option<Scripts>, order: Option<OrderString>, cmd: Option<CommandWrapper>, desc: Option<String>, },
}
type OrderString = String; #[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum CommandWrapper {
Universal(CommandCore), Specific {
generic: CommandCore,
targets: Option<HashMap<TargetString, CommandCore>>,
},
}
impl CommandWrapper {
fn parse(&self) -> schema::CommandWrapper {
match self {
CommandWrapper::Universal(raw_command_core) => schema::CommandWrapper {
generic: raw_command_core.parse(),
targets: HashMap::new(),
},
CommandWrapper::Specific {
generic,
targets: None,
} => schema::CommandWrapper {
generic: generic.parse(),
targets: HashMap::new(),
},
CommandWrapper::Specific {
generic,
targets: Some(targets),
} => {
let parsed_generic = generic.parse();
let mut parsed_targets: HashMap<schema::TargetString, schema::CommandCore> =
HashMap::new();
for (target_name, raw_command_core) in targets.iter() {
parsed_targets.insert(target_name.to_string(), raw_command_core.parse());
}
schema::CommandWrapper {
generic: parsed_generic,
targets: parsed_targets,
}
}
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum CommandCore {
Simple(CommandBox), WithShell {
exec: CommandBox, shell: Option<Shell>,
},
}
impl CommandCore {
fn parse(&self) -> schema::CommandCore {
match self {
CommandCore::Simple(exec) => schema::CommandCore {
exec: exec.parse(),
shell: None,
},
CommandCore::WithShell {
exec,
shell: Some(shell),
} => schema::CommandCore {
exec: exec.parse(),
shell: Some(shell.parse()),
},
CommandCore::WithShell { exec, shell: None } => schema::CommandCore {
exec: exec.parse(),
shell: None,
},
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum CommandBox {
Simple(String),
MultiStage(Vec<String>),
}
impl CommandBox {
fn parse(&self) -> Vec<String> {
match self {
CommandBox::Simple(cmd_str) => vec![cmd_str.to_string()],
CommandBox::MultiStage(cmd_strs) => {
cmd_strs.iter().map(|cmd_str| cmd_str.to_string()).collect()
}
}
}
}