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 = env_files.unwrap_or_default();
89 for env_file in env_files.iter() {
91 let res = dotenv::from_filename(env_file);
94 if res.is_err() {
95 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));
96 }
97 }
98
99 Ok(())
100 }
101 fn parse(&self) -> Result<schema::Config, String> {
104 let default_shell = match &self.default_shell {
106 Some(DefaultShell::Simple(generic)) => schema::DefaultShell {
108 generic: generic.parse(),
109 targets: HashMap::new(),
110 },
111 Some(DefaultShell::Complex { generic, targets }) => schema::DefaultShell {
113 generic: generic.parse(),
114 targets: match targets {
115 Some(raw_targets) => {
116 let mut targets = HashMap::new();
118 for (target_name, shell) in raw_targets.iter() {
119 targets.insert(target_name.to_string(), shell.parse());
120 }
121 targets
122 }
123 None => HashMap::new(), },
125 },
126 None => get_default_shells(),
128 };
129 fn parse_scripts(
134 raw_scripts: &Scripts,
135 is_order_defined: bool,
136 ) -> Result<schema::Scripts, String> {
137 let mut scripts: schema::Scripts = HashMap::new();
138 for (script_name, raw_command) in raw_scripts.iter() {
139 let command = match raw_command {
140 Command::Simple(raw_command_wrapper) => schema::Command {
141 args: Vec::new(),
142 env_vars: Vec::new(),
143 subcommands: None,
144 order: None,
145 cmd: Some(raw_command_wrapper.parse()), description: None
147 },
148 Command::Complex {
149 args,
150 env_vars,
151 subcommands,
152 order,
153 cmd,
154 desc
155 } => schema::Command {
156 args: match is_order_defined {
158 _ if subcommands.is_some() && order.is_none() && args.is_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)),
160 true if args.is_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)),
162 true => Vec::new(),
164 false => args.as_ref().unwrap_or(&Vec::new()).to_vec()
166 },
167 env_vars: env_vars.as_ref().unwrap_or(&Vec::new()).to_vec(),
169 subcommands: match subcommands {
172 Some(subcommands) => Some(
174 parse_scripts(subcommands, order.is_some())?
175 ),
176 None => None
177 },
178 order: match is_order_defined {
180 true if subcommands.is_some() => match order {
181 Some(order) => Some(parse_directive_str(order)?),
183 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))
185 }
186 true | false => match order {
188 Some(order) => Some(parse_directive_str(order)?),
189 None => None
190 }
191 },
192 cmd: match cmd {
194 Some(_) if order.is_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)),
196 _ if subcommands.is_some() => cmd.as_ref().map(|cmd| cmd.parse()),
198 Some(cmd) => Some(cmd.parse()),
200 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))
202 },
203 description: desc.clone()
204 },
205 };
206 scripts.insert(script_name.to_string(), command);
207 }
208
209 Ok(scripts)
210 }
211
212 let scripts = parse_scripts(&self.scripts, false)?;
213
214 Ok(schema::Config {
215 default_shell,
216 scripts,
217 env_files: match &self.env_files {
219 Some(env_files) => env_files.to_vec(),
220 None => Vec::new(),
221 },
222 version: self.version.clone(),
223 })
224 }
225}
226#[derive(Debug, Clone, Deserialize)]
227#[serde(untagged)]
228enum DefaultShell {
229 Simple(Shell), Complex {
231 generic: Shell, targets: Option<HashMap<String, Shell>>,
233 },
234}
235#[derive(Debug, Clone, Deserialize)]
239#[serde(untagged)]
240pub enum Shell {
241 Simple(Vec<String>),
242 WithDelimiter {
243 parts: Vec<String>,
244 delimiter: String,
245 },
246}
247impl Shell {
248 fn parse(&self) -> schema::Shell {
249 match self {
250 Shell::Simple(parts) => schema::Shell {
251 parts: parts.to_vec(),
252 delimiter: " && ".to_string(),
254 },
255 Shell::WithDelimiter { parts, delimiter } => schema::Shell {
256 parts: parts.to_vec(),
257 delimiter: delimiter.to_string(),
259 },
260 }
261 }
262}
263type TargetString = String; type Scripts = HashMap<String, Command>;
265
266#[derive(Debug, Clone, Deserialize)]
267#[serde(untagged)]
268enum Command {
269 Simple(CommandWrapper), Complex {
271 args: Option<Vec<String>>,
272 env_vars: Option<Vec<String>>,
273 subcommands: Option<Scripts>, order: Option<OrderString>, cmd: Option<CommandWrapper>, desc: Option<String>, },
278}
279type OrderString = String; #[derive(Debug, Clone, Deserialize)]
283#[serde(untagged)]
284enum CommandWrapper {
285 Universal(CommandCore), Specific {
287 generic: CommandCore,
288 targets: Option<HashMap<TargetString, CommandCore>>,
289 },
290}
291impl CommandWrapper {
292 fn parse(&self) -> schema::CommandWrapper {
294 match self {
295 CommandWrapper::Universal(raw_command_core) => schema::CommandWrapper {
297 generic: raw_command_core.parse(),
298 targets: HashMap::new(),
299 },
300 CommandWrapper::Specific {
302 generic,
303 targets: None,
304 } => schema::CommandWrapper {
305 generic: generic.parse(),
306 targets: HashMap::new(),
307 },
308 CommandWrapper::Specific {
309 generic,
310 targets: Some(targets),
311 } => {
312 let parsed_generic = generic.parse();
313 let mut parsed_targets: HashMap<schema::TargetString, schema::CommandCore> =
314 HashMap::new();
315 for (target_name, raw_command_core) in targets.iter() {
316 parsed_targets.insert(target_name.to_string(), raw_command_core.parse());
317 }
318 schema::CommandWrapper {
319 generic: parsed_generic,
320 targets: parsed_targets,
321 }
322 }
323 }
324 }
325}
326#[derive(Debug, Clone, Deserialize)]
327#[serde(untagged)]
328enum CommandCore {
329 Simple(CommandBox), WithShell {
331 exec: CommandBox, shell: Option<Shell>,
333 },
334}
335impl CommandCore {
336 fn parse(&self) -> schema::CommandCore {
338 match self {
339 CommandCore::Simple(exec) => schema::CommandCore {
340 exec: exec.parse(),
341 shell: None,
342 },
343 CommandCore::WithShell {
344 exec,
345 shell: Some(shell),
346 } => schema::CommandCore {
347 exec: exec.parse(),
348 shell: Some(shell.parse()),
349 },
350 CommandCore::WithShell { exec, shell: None } => schema::CommandCore {
352 exec: exec.parse(),
353 shell: None,
354 },
355 }
356 }
357}
358#[derive(Debug, Clone, Deserialize)]
360#[serde(untagged)]
361enum CommandBox {
362 Simple(String),
363 MultiStage(Vec<String>),
364}
365impl CommandBox {
366 fn parse(&self) -> Vec<String> {
368 match self {
369 CommandBox::Simple(cmd_str) => vec![cmd_str.to_string()],
371 CommandBox::MultiStage(cmd_strs) => {
372 cmd_strs.iter().map(|cmd_str| cmd_str.to_string()).collect()
373 }
374 }
375 }
376}