1use std::collections::BTreeMap;
14
15use serde::{Deserialize, Serialize};
16
17use super::error::PackParseError;
18use super::predicate::{Combiner, ExecOnFail, OsKind, Predicate, RequireOnFail};
19
20#[non_exhaustive]
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
26#[serde(rename_all = "lowercase")]
27pub enum SymlinkKind {
28 #[default]
30 Auto,
31 File,
33 Directory,
35}
36
37#[non_exhaustive]
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
43#[serde(rename_all = "lowercase")]
44pub enum EnvScope {
45 #[default]
47 User,
48 Machine,
50 Session,
52}
53
54#[non_exhaustive]
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct SymlinkArgs {
62 pub src: String,
64 pub dst: String,
66 #[serde(default)]
68 pub backup: bool,
69 #[serde(default = "default_true")]
71 pub normalize: bool,
72 #[serde(default)]
74 pub kind: SymlinkKind,
75}
76
77fn default_true() -> bool {
78 true
79}
80
81impl SymlinkArgs {
82 #[must_use]
87 pub fn new(src: String, dst: String, backup: bool, normalize: bool, kind: SymlinkKind) -> Self {
88 Self { src, dst, backup, normalize, kind }
89 }
90}
91
92impl EnvArgs {
93 #[must_use]
95 pub fn new(name: String, value: String, scope: EnvScope) -> Self {
96 Self { name, value, scope }
97 }
98}
99
100impl MkdirArgs {
101 #[must_use]
103 pub fn new(path: String, mode: Option<String>) -> Self {
104 Self { path, mode }
105 }
106}
107
108impl RmdirArgs {
109 #[must_use]
111 pub fn new(path: String, backup: bool, force: bool) -> Self {
112 Self { path, backup, force }
113 }
114}
115
116impl RequireSpec {
117 #[must_use]
119 pub fn new(combiner: Combiner, on_fail: RequireOnFail) -> Self {
120 Self { combiner, on_fail }
121 }
122}
123
124impl WhenSpec {
125 #[must_use]
127 pub fn new(
128 os: Option<OsKind>,
129 all_of: Option<Vec<Predicate>>,
130 any_of: Option<Vec<Predicate>>,
131 none_of: Option<Vec<Predicate>>,
132 actions: Vec<Action>,
133 ) -> Self {
134 Self { os, all_of, any_of, none_of, actions }
135 }
136}
137
138impl ExecSpec {
139 #[must_use]
141 pub fn new(
142 cmd: Option<Vec<String>>,
143 cmd_shell: Option<String>,
144 shell: bool,
145 cwd: Option<String>,
146 env: Option<BTreeMap<String, String>>,
147 on_fail: ExecOnFail,
148 ) -> Self {
149 Self { cmd, cmd_shell, shell, cwd, env, on_fail }
150 }
151}
152
153#[non_exhaustive]
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct UnlinkArgs {
167 pub dst: String,
169}
170
171impl UnlinkArgs {
172 #[must_use]
176 pub fn new(dst: String) -> Self {
177 Self { dst }
178 }
179}
180
181#[non_exhaustive]
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct EnvArgs {
188 pub name: String,
190 pub value: String,
192 #[serde(default)]
194 pub scope: EnvScope,
195}
196
197#[non_exhaustive]
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct MkdirArgs {
204 pub path: String,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub mode: Option<String>,
209}
210
211#[non_exhaustive]
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217pub struct RmdirArgs {
218 pub path: String,
220 #[serde(default)]
222 pub backup: bool,
223 #[serde(default)]
225 pub force: bool,
226}
227
228#[non_exhaustive]
234#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
235pub struct RequireSpec {
236 pub combiner: Combiner,
238 pub on_fail: RequireOnFail,
240}
241
242#[non_exhaustive]
251#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct WhenSpec {
253 pub os: Option<OsKind>,
255 pub all_of: Option<Vec<Predicate>>,
257 pub any_of: Option<Vec<Predicate>>,
259 pub none_of: Option<Vec<Predicate>>,
261 pub actions: Vec<Action>,
263}
264
265#[non_exhaustive]
274#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
275pub struct ExecSpec {
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub cmd: Option<Vec<String>>,
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub cmd_shell: Option<String>,
282 pub shell: bool,
284 #[serde(skip_serializing_if = "Option::is_none")]
286 pub cwd: Option<String>,
287 #[serde(skip_serializing_if = "Option::is_none")]
289 pub env: Option<BTreeMap<String, String>>,
290 pub on_fail: ExecOnFail,
292}
293
294#[non_exhaustive]
300#[derive(Debug, Clone, PartialEq, Eq)]
301pub enum Action {
302 Symlink(SymlinkArgs),
304 Unlink(UnlinkArgs),
307 Env(EnvArgs),
309 Mkdir(MkdirArgs),
311 Rmdir(RmdirArgs),
313 Require(RequireSpec),
315 When(WhenSpec),
317 Exec(ExecSpec),
319}
320
321pub const VALID_ACTION_KEYS: &[&str] =
324 &["symlink", "env", "mkdir", "rmdir", "require", "when", "exec"];
325
326impl Action {
327 pub fn from_yaml(value: &serde_yaml::Value) -> Result<Self, PackParseError> {
333 let (key, v) = single_key_entry(value)?;
334 match key.as_str() {
335 "symlink" => parse_symlink(v),
336 "env" => parse_env(v),
337 "mkdir" => parse_mkdir(v),
338 "rmdir" => parse_rmdir(v),
339 "require" => parse_require(v).map(Self::Require),
340 "when" => parse_when(v).map(Self::When),
341 "exec" => parse_exec(v).map(Self::Exec),
342 other => Err(PackParseError::UnknownActionKey { key: other.to_string() }),
343 }
344 }
345
346 pub fn parse_list(value: Option<&serde_yaml::Value>) -> Result<Vec<Self>, PackParseError> {
348 let Some(value) = value else {
349 return Ok(Vec::new());
350 };
351 if value.is_null() {
352 return Ok(Vec::new());
353 }
354 let seq = value.as_sequence().ok_or_else(|| PackParseError::UnknownActionKey {
355 key: "<actions must be a sequence>".to_string(),
356 })?;
357 seq.iter().map(Self::from_yaml).collect()
358 }
359
360 #[must_use]
365 pub fn name(&self) -> &'static str {
366 match self {
367 Self::Symlink(_) => "symlink",
368 Self::Unlink(_) => "unlink",
369 Self::Env(_) => "env",
370 Self::Mkdir(_) => "mkdir",
371 Self::Rmdir(_) => "rmdir",
372 Self::Require(_) => "require",
373 Self::When(_) => "when",
374 Self::Exec(_) => "exec",
375 }
376 }
377
378 #[must_use]
394 pub fn iter_symlinks(&self) -> Box<dyn Iterator<Item = &SymlinkArgs> + '_> {
395 match self {
396 Self::Symlink(s) => Box::new(std::iter::once(s)),
397 Self::When(w) => Box::new(w.actions.iter().flat_map(Self::iter_symlinks)),
398 _ => Box::new(std::iter::empty()),
399 }
400 }
401}
402
403fn single_key_entry(
406 value: &serde_yaml::Value,
407) -> Result<(String, &serde_yaml::Value), PackParseError> {
408 let mapping = value.as_mapping().ok_or(PackParseError::EmptyActionEntry)?;
409 match mapping.len() {
410 0 => return Err(PackParseError::EmptyActionEntry),
411 1 => {}
412 _ => {
413 let keys = mapping.iter().filter_map(|(k, _)| k.as_str().map(str::to_owned)).collect();
414 return Err(PackParseError::MultipleActionKeys { keys });
415 }
416 }
417 let (k, v) = mapping.iter().next().expect("len==1 checked above");
418 let key =
419 k.as_str().ok_or_else(|| PackParseError::UnknownActionKey { key: format!("{k:?}") })?;
420 Ok((key.to_string(), v))
421}
422
423fn parse_symlink(value: &serde_yaml::Value) -> Result<Action, PackParseError> {
424 Ok(Action::Symlink(serde_yaml::from_value(value.clone())?))
425}
426
427fn parse_env(value: &serde_yaml::Value) -> Result<Action, PackParseError> {
428 Ok(Action::Env(serde_yaml::from_value(value.clone())?))
429}
430
431fn parse_mkdir(value: &serde_yaml::Value) -> Result<Action, PackParseError> {
432 Ok(Action::Mkdir(serde_yaml::from_value(value.clone())?))
433}
434
435fn parse_rmdir(value: &serde_yaml::Value) -> Result<Action, PackParseError> {
436 Ok(Action::Rmdir(serde_yaml::from_value(value.clone())?))
437}
438
439fn parse_require(value: &serde_yaml::Value) -> Result<RequireSpec, PackParseError> {
440 let mapping = value.as_mapping().ok_or_else(|| PackParseError::InvalidPredicate {
441 detail: "require: expects a mapping".to_string(),
442 })?;
443 let combiner = Combiner::from_mapping(mapping, 0)?;
444 let on_fail = match mapping.get(serde_yaml::Value::String("on_fail".to_string())) {
445 Some(v) => serde_yaml::from_value::<RequireOnFail>(v.clone())?,
446 None => RequireOnFail::default(),
447 };
448 Ok(RequireSpec { combiner, on_fail })
449}
450
451fn parse_when(value: &serde_yaml::Value) -> Result<WhenSpec, PackParseError> {
452 let mapping = value.as_mapping().ok_or_else(|| PackParseError::InvalidPredicate {
453 detail: "when: expects a mapping".to_string(),
454 })?;
455
456 let os = match mapping.get(serde_yaml::Value::String("os".to_string())) {
457 Some(v) => Some(serde_yaml::from_value::<OsKind>(v.clone())?),
458 None => None,
459 };
460
461 let all_of = optional_predicate_list(mapping, "all_of")?;
462 let any_of = optional_predicate_list(mapping, "any_of")?;
463 let none_of = optional_predicate_list(mapping, "none_of")?;
464
465 let actions_value = mapping.get(serde_yaml::Value::String("actions".to_string()));
466 let actions = Action::parse_list(actions_value)?;
467
468 Ok(WhenSpec { os, all_of, any_of, none_of, actions })
469}
470
471fn optional_predicate_list(
472 mapping: &serde_yaml::Mapping,
473 key: &str,
474) -> Result<Option<Vec<Predicate>>, PackParseError> {
475 let Some(value) = mapping.get(serde_yaml::Value::String(key.to_string())) else {
476 return Ok(None);
477 };
478 let seq = value.as_sequence().ok_or_else(|| PackParseError::InvalidPredicate {
479 detail: format!("{key} must be a sequence of predicates"),
480 })?;
481 let preds: Vec<Predicate> =
482 seq.iter().map(|v| Predicate::from_yaml(v, 1)).collect::<Result<_, _>>()?;
483 Ok(Some(preds))
484}
485
486fn parse_exec(value: &serde_yaml::Value) -> Result<ExecSpec, PackParseError> {
487 #[derive(Deserialize)]
490 struct Raw {
491 #[serde(default)]
492 cmd: Option<Vec<String>>,
493 #[serde(default)]
494 cmd_shell: Option<String>,
495 #[serde(default)]
496 shell: bool,
497 #[serde(default)]
498 cwd: Option<String>,
499 #[serde(default)]
500 env: Option<BTreeMap<String, String>>,
501 #[serde(default)]
502 on_fail: ExecOnFail,
503 }
504
505 let raw: Raw = serde_yaml::from_value(value.clone())?;
506
507 let cmd_present = raw.cmd.is_some();
508 let cmd_shell_present = raw.cmd_shell.is_some();
509
510 let valid = match raw.shell {
511 false => cmd_present && !cmd_shell_present,
512 true => !cmd_present && cmd_shell_present,
513 };
514 if !valid {
515 return Err(PackParseError::ExecCmdMutex {
516 shell: raw.shell,
517 cmd_present,
518 cmd_shell_present,
519 });
520 }
521
522 Ok(ExecSpec {
523 cmd: raw.cmd,
524 cmd_shell: raw.cmd_shell,
525 shell: raw.shell,
526 cwd: raw.cwd,
527 env: raw.env,
528 on_fail: raw.on_fail,
529 })
530}