1use crate::bones::parse_directive_str;
7use crate::default_shells::get_default_shells;
8use crate::schema;
9use crate::version::{get_version_parts, VersionCompatibility, VersionDifference, BONNIE_VERSION};
10use serde::Deserialize;
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Deserialize)]
14pub struct Config {
15 version: String, env_files: Option<Vec<String>>, default_shell: Option<DefaultShell>,
18 scripts: Scripts,
19}
20impl Config {
21 pub fn new(cfg_string: &str) -> Result<Self, String> {
22 let cfg: Result<Self, toml::de::Error> = toml::from_str(cfg_string);
23 let cfg = match cfg {
24 Ok(cfg) => cfg,
25 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."),
27 Err(err) => return Err(format!("Invalid Bonnie configuration file. Error: '{}'", err))
28 };
29
30 Ok(cfg)
31 }
32 pub fn to_final(
36 &self,
37 bonnie_version_str: &str,
38 output: &mut impl std::io::Write,
39 ) -> Result<schema::Config, String> {
40 Self::parse_version_against_current(&self.version, bonnie_version_str, output)?;
42 Self::load_env_files(self.env_files.clone())?;
43 let cfg = self.parse()?;
45
46 Ok(cfg)
47 }
48 pub fn parse_version_against_current(
52 cfg_version_str: &str,
53 bonnie_version_str: &str,
54 output: &mut impl std::io::Write,
55 ) -> Result<(), String> {
56 let bonnie_version = get_version_parts(bonnie_version_str)?;
58 let cfg_version = get_version_parts(cfg_version_str)?;
59 let compat = bonnie_version.is_compatible_with(&cfg_version);
61 match compat {
62 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 {
63 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.",
64 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)."
65 }),
66 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 {
67 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.",
68 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)."
69 }),
70 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 {
72 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.",
73 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)."
74 }).expect("Failed to write warning."),
75 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 {
76 VersionDifference::TooNew => "You may want to update Bonnie to the appropriate version, which can be done at https://github.com/arctic-hen7/bonnie/releases.",
77 VersionDifference::TooOld => "You may want to update the configuration file (which shouldn't require any syntax changes)."
78 }).expect("Failed to write warning."),
79 _ => ()
80 };
81
82 Ok(())
84 }
85 pub fn load_env_files(env_files: Option<Vec<String>>) -> Result<(), String> {
88 let env_files = match env_files {
89 Some(env_files) => env_files,
90 None => Vec::new(),
91 };
92 for env_file in env_files.iter() {
94 let res = dotenv::from_filename(&env_file);
97 if res.is_err() {
98 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));
99 }
100 }
101
102 Ok(())
103 }
104 fn parse(&self) -> Result<schema::Config, String> {
107 let default_shell = match &self.default_shell {
109 Some(DefaultShell::Simple(generic)) => schema::DefaultShell {
111 generic: generic.parse(),
112 targets: HashMap::new(),
113 },
114 Some(DefaultShell::Complex { generic, targets }) => schema::DefaultShell {
116 generic: generic.parse(),
117 targets: match targets {
118 Some(raw_targets) => {
119 let mut targets = HashMap::new();
121 for (target_name, shell) in raw_targets.iter() {
122 targets.insert(target_name.to_string(), shell.parse());
123 }
124 targets
125 }
126 None => HashMap::new(), },
128 },
129 None => get_default_shells(),
131 };
132 fn parse_scripts(
137 raw_scripts: &Scripts,
138 is_order_defined: bool,
139 ) -> Result<schema::Scripts, String> {
140 let mut scripts: schema::Scripts = HashMap::new();
141 for (script_name, raw_command) in raw_scripts.iter() {
142 let command = match raw_command {
143 Command::Simple(raw_command_wrapper) => schema::Command {
144 args: Vec::new(),
145 env_vars: Vec::new(),
146 subcommands: None,
147 order: None,
148 cmd: Some(raw_command_wrapper.parse()), description: None
150 },
151 Command::Complex {
152 args,
153 env_vars,
154 subcommands,
155 order,
156 cmd,
157 desc
158 } => schema::Command {
159 args: match is_order_defined {
161 _ 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)),
163 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)),
165 true => Vec::new(),
167 false => args.as_ref().unwrap_or(&Vec::new()).to_vec()
169 },
170 env_vars: env_vars.as_ref().unwrap_or(&Vec::new()).to_vec(),
172 subcommands: match subcommands {
175 Some(subcommands) => Some(
177 parse_scripts(subcommands, matches!(order, Some(_)))?
178 ),
179 None => None
180 },
181 order: match is_order_defined {
183 true if matches!(subcommands, Some(_)) => match order {
184 Some(order) => Some(parse_directive_str(order)?),
186 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))
188 }
189 true | false => match order {
191 Some(order) => Some(parse_directive_str(order)?),
192 None => None
193 }
194 },
195 cmd: match cmd {
197 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)),
199 _ if matches!(subcommands, Some(_)) => cmd.as_ref().map(|cmd| cmd.parse()),
201 Some(cmd) => Some(cmd.parse()),
203 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))
205 },
206 description: desc.clone()
207 },
208 };
209 scripts.insert(script_name.to_string(), command);
210 }
211
212 Ok(scripts)
213 }
214
215 let scripts = parse_scripts(&self.scripts, false)?;
216
217 Ok(schema::Config {
218 default_shell,
219 scripts,
220 env_files: match &self.env_files {
222 Some(env_files) => env_files.to_vec(),
223 None => Vec::new(),
224 },
225 version: self.version.clone(),
226 })
227 }
228}
229#[derive(Debug, Clone, Deserialize)]
230#[serde(untagged)]
231enum DefaultShell {
232 Simple(Shell), Complex {
234 generic: Shell, targets: Option<HashMap<String, Shell>>,
236 },
237}
238#[derive(Debug, Clone, Deserialize)]
242#[serde(untagged)]
243pub enum Shell {
244 Simple(Vec<String>),
245 WithDelimiter {
246 parts: Vec<String>,
247 delimiter: String,
248 },
249}
250impl Shell {
251 fn parse(&self) -> schema::Shell {
252 match self {
253 Shell::Simple(parts) => schema::Shell {
254 parts: parts.to_vec(),
255 delimiter: " && ".to_string(),
257 },
258 Shell::WithDelimiter { parts, delimiter } => schema::Shell {
259 parts: parts.to_vec(),
260 delimiter: delimiter.to_string(),
262 },
263 }
264 }
265}
266type TargetString = String; type Scripts = HashMap<String, Command>;
268
269#[derive(Debug, Clone, Deserialize)]
270#[serde(untagged)]
271enum Command {
272 Simple(CommandWrapper), Complex {
274 args: Option<Vec<String>>,
275 env_vars: Option<Vec<String>>,
276 subcommands: Option<Scripts>, order: Option<OrderString>, cmd: Option<CommandWrapper>, desc: Option<String>, },
281}
282type OrderString = String; #[derive(Debug, Clone, Deserialize)]
286#[serde(untagged)]
287enum CommandWrapper {
288 Universal(CommandCore), Specific {
290 generic: CommandCore,
291 targets: Option<HashMap<TargetString, CommandCore>>,
292 },
293}
294impl CommandWrapper {
295 fn parse(&self) -> schema::CommandWrapper {
297 match self {
298 CommandWrapper::Universal(raw_command_core) => schema::CommandWrapper {
300 generic: raw_command_core.parse(),
301 targets: HashMap::new(),
302 },
303 CommandWrapper::Specific {
305 generic,
306 targets: None,
307 } => schema::CommandWrapper {
308 generic: generic.parse(),
309 targets: HashMap::new(),
310 },
311 CommandWrapper::Specific {
312 generic,
313 targets: Some(targets),
314 } => {
315 let parsed_generic = generic.parse();
316 let mut parsed_targets: HashMap<schema::TargetString, schema::CommandCore> =
317 HashMap::new();
318 for (target_name, raw_command_core) in targets.iter() {
319 parsed_targets.insert(target_name.to_string(), raw_command_core.parse());
320 }
321 schema::CommandWrapper {
322 generic: parsed_generic,
323 targets: parsed_targets,
324 }
325 }
326 }
327 }
328}
329#[derive(Debug, Clone, Deserialize)]
330#[serde(untagged)]
331enum CommandCore {
332 Simple(CommandBox), WithShell {
334 exec: CommandBox, shell: Option<Shell>,
336 },
337}
338impl CommandCore {
339 fn parse(&self) -> schema::CommandCore {
341 match self {
342 CommandCore::Simple(exec) => schema::CommandCore {
343 exec: exec.parse(),
344 shell: None,
345 },
346 CommandCore::WithShell {
347 exec,
348 shell: Some(shell),
349 } => schema::CommandCore {
350 exec: exec.parse(),
351 shell: Some(shell.parse()),
352 },
353 CommandCore::WithShell { exec, shell: None } => schema::CommandCore {
355 exec: exec.parse(),
356 shell: None,
357 },
358 }
359 }
360}
361#[derive(Debug, Clone, Deserialize)]
363#[serde(untagged)]
364enum CommandBox {
365 Simple(String),
366 MultiStage(Vec<String>),
367}
368impl CommandBox {
369 fn parse(&self) -> Vec<String> {
371 match self {
372 CommandBox::Simple(cmd_str) => vec![cmd_str.to_string()],
374 CommandBox::MultiStage(cmd_strs) => {
375 cmd_strs.iter().map(|cmd_str| cmd_str.to_string()).collect()
376 }
377 }
378 }
379}