use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::error::PackParseError;
use super::predicate::{Combiner, ExecOnFail, OsKind, Predicate, RequireOnFail};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum SymlinkKind {
#[default]
Auto,
File,
Directory,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum EnvScope {
#[default]
User,
Machine,
Session,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SymlinkArgs {
pub src: String,
pub dst: String,
#[serde(default)]
pub backup: bool,
#[serde(default = "default_true")]
pub normalize: bool,
#[serde(default)]
pub kind: SymlinkKind,
}
fn default_true() -> bool {
true
}
impl SymlinkArgs {
#[must_use]
pub fn new(src: String, dst: String, backup: bool, normalize: bool, kind: SymlinkKind) -> Self {
Self { src, dst, backup, normalize, kind }
}
}
impl EnvArgs {
#[must_use]
pub fn new(name: String, value: String, scope: EnvScope) -> Self {
Self { name, value, scope }
}
}
impl MkdirArgs {
#[must_use]
pub fn new(path: String, mode: Option<String>) -> Self {
Self { path, mode }
}
}
impl RmdirArgs {
#[must_use]
pub fn new(path: String, backup: bool, force: bool) -> Self {
Self { path, backup, force }
}
}
impl RequireSpec {
#[must_use]
pub fn new(combiner: Combiner, on_fail: RequireOnFail) -> Self {
Self { combiner, on_fail }
}
}
impl WhenSpec {
#[must_use]
pub fn new(
os: Option<OsKind>,
all_of: Option<Vec<Predicate>>,
any_of: Option<Vec<Predicate>>,
none_of: Option<Vec<Predicate>>,
actions: Vec<Action>,
) -> Self {
Self { os, all_of, any_of, none_of, actions }
}
}
impl ExecSpec {
#[must_use]
pub fn new(
cmd: Option<Vec<String>>,
cmd_shell: Option<String>,
shell: bool,
cwd: Option<String>,
env: Option<BTreeMap<String, String>>,
on_fail: ExecOnFail,
) -> Self {
Self { cmd, cmd_shell, shell, cwd, env, on_fail }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnlinkArgs {
pub dst: String,
}
impl UnlinkArgs {
#[must_use]
pub fn new(dst: String) -> Self {
Self { dst }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvArgs {
pub name: String,
pub value: String,
#[serde(default)]
pub scope: EnvScope,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MkdirArgs {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RmdirArgs {
pub path: String,
#[serde(default)]
pub backup: bool,
#[serde(default)]
pub force: bool,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RequireSpec {
pub combiner: Combiner,
pub on_fail: RequireOnFail,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WhenSpec {
pub os: Option<OsKind>,
pub all_of: Option<Vec<Predicate>>,
pub any_of: Option<Vec<Predicate>>,
pub none_of: Option<Vec<Predicate>>,
pub actions: Vec<Action>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ExecSpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub cmd: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cmd_shell: Option<String>,
pub shell: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<BTreeMap<String, String>>,
pub on_fail: ExecOnFail,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
Symlink(SymlinkArgs),
Unlink(UnlinkArgs),
Env(EnvArgs),
Mkdir(MkdirArgs),
Rmdir(RmdirArgs),
Require(RequireSpec),
When(WhenSpec),
Exec(ExecSpec),
}
pub const VALID_ACTION_KEYS: &[&str] =
&["symlink", "env", "mkdir", "rmdir", "require", "when", "exec"];
impl Action {
pub fn from_yaml(value: &serde_yaml::Value) -> Result<Self, PackParseError> {
let (key, v) = single_key_entry(value)?;
match key.as_str() {
"symlink" => parse_symlink(v),
"env" => parse_env(v),
"mkdir" => parse_mkdir(v),
"rmdir" => parse_rmdir(v),
"require" => parse_require(v).map(Self::Require),
"when" => parse_when(v).map(Self::When),
"exec" => parse_exec(v).map(Self::Exec),
other => Err(PackParseError::UnknownActionKey { key: other.to_string() }),
}
}
pub fn parse_list(value: Option<&serde_yaml::Value>) -> Result<Vec<Self>, PackParseError> {
let Some(value) = value else {
return Ok(Vec::new());
};
if value.is_null() {
return Ok(Vec::new());
}
let seq = value.as_sequence().ok_or_else(|| PackParseError::UnknownActionKey {
key: "<actions must be a sequence>".to_string(),
})?;
seq.iter().map(Self::from_yaml).collect()
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Symlink(_) => "symlink",
Self::Unlink(_) => "unlink",
Self::Env(_) => "env",
Self::Mkdir(_) => "mkdir",
Self::Rmdir(_) => "rmdir",
Self::Require(_) => "require",
Self::When(_) => "when",
Self::Exec(_) => "exec",
}
}
#[must_use]
pub fn iter_symlinks(&self) -> Box<dyn Iterator<Item = &SymlinkArgs> + '_> {
match self {
Self::Symlink(s) => Box::new(std::iter::once(s)),
Self::When(w) => Box::new(w.actions.iter().flat_map(Self::iter_symlinks)),
_ => Box::new(std::iter::empty()),
}
}
}
fn single_key_entry(
value: &serde_yaml::Value,
) -> Result<(String, &serde_yaml::Value), PackParseError> {
let mapping = value.as_mapping().ok_or(PackParseError::EmptyActionEntry)?;
match mapping.len() {
0 => return Err(PackParseError::EmptyActionEntry),
1 => {}
_ => {
let keys = mapping.iter().filter_map(|(k, _)| k.as_str().map(str::to_owned)).collect();
return Err(PackParseError::MultipleActionKeys { keys });
}
}
let (k, v) = mapping.iter().next().expect("len==1 checked above");
let key =
k.as_str().ok_or_else(|| PackParseError::UnknownActionKey { key: format!("{k:?}") })?;
Ok((key.to_string(), v))
}
fn parse_symlink(value: &serde_yaml::Value) -> Result<Action, PackParseError> {
Ok(Action::Symlink(serde_yaml::from_value(value.clone())?))
}
fn parse_env(value: &serde_yaml::Value) -> Result<Action, PackParseError> {
Ok(Action::Env(serde_yaml::from_value(value.clone())?))
}
fn parse_mkdir(value: &serde_yaml::Value) -> Result<Action, PackParseError> {
Ok(Action::Mkdir(serde_yaml::from_value(value.clone())?))
}
fn parse_rmdir(value: &serde_yaml::Value) -> Result<Action, PackParseError> {
Ok(Action::Rmdir(serde_yaml::from_value(value.clone())?))
}
fn parse_require(value: &serde_yaml::Value) -> Result<RequireSpec, PackParseError> {
let mapping = value.as_mapping().ok_or_else(|| PackParseError::InvalidPredicate {
detail: "require: expects a mapping".to_string(),
})?;
let combiner = Combiner::from_mapping(mapping, 0)?;
let on_fail = match mapping.get(serde_yaml::Value::String("on_fail".to_string())) {
Some(v) => serde_yaml::from_value::<RequireOnFail>(v.clone())?,
None => RequireOnFail::default(),
};
Ok(RequireSpec { combiner, on_fail })
}
fn parse_when(value: &serde_yaml::Value) -> Result<WhenSpec, PackParseError> {
let mapping = value.as_mapping().ok_or_else(|| PackParseError::InvalidPredicate {
detail: "when: expects a mapping".to_string(),
})?;
let os = match mapping.get(serde_yaml::Value::String("os".to_string())) {
Some(v) => Some(serde_yaml::from_value::<OsKind>(v.clone())?),
None => None,
};
let all_of = optional_predicate_list(mapping, "all_of")?;
let any_of = optional_predicate_list(mapping, "any_of")?;
let none_of = optional_predicate_list(mapping, "none_of")?;
let actions_value = mapping.get(serde_yaml::Value::String("actions".to_string()));
let actions = Action::parse_list(actions_value)?;
Ok(WhenSpec { os, all_of, any_of, none_of, actions })
}
fn optional_predicate_list(
mapping: &serde_yaml::Mapping,
key: &str,
) -> Result<Option<Vec<Predicate>>, PackParseError> {
let Some(value) = mapping.get(serde_yaml::Value::String(key.to_string())) else {
return Ok(None);
};
let seq = value.as_sequence().ok_or_else(|| PackParseError::InvalidPredicate {
detail: format!("{key} must be a sequence of predicates"),
})?;
let preds: Vec<Predicate> =
seq.iter().map(|v| Predicate::from_yaml(v, 1)).collect::<Result<_, _>>()?;
Ok(Some(preds))
}
fn parse_exec(value: &serde_yaml::Value) -> Result<ExecSpec, PackParseError> {
#[derive(Deserialize)]
struct Raw {
#[serde(default)]
cmd: Option<Vec<String>>,
#[serde(default)]
cmd_shell: Option<String>,
#[serde(default)]
shell: bool,
#[serde(default)]
cwd: Option<String>,
#[serde(default)]
env: Option<BTreeMap<String, String>>,
#[serde(default)]
on_fail: ExecOnFail,
}
let raw: Raw = serde_yaml::from_value(value.clone())?;
let cmd_present = raw.cmd.is_some();
let cmd_shell_present = raw.cmd_shell.is_some();
let valid = match raw.shell {
false => cmd_present && !cmd_shell_present,
true => !cmd_present && cmd_shell_present,
};
if !valid {
return Err(PackParseError::ExecCmdMutex {
shell: raw.shell,
cmd_present,
cmd_shell_present,
});
}
Ok(ExecSpec {
cmd: raw.cmd,
cmd_shell: raw.cmd_shell,
shell: raw.shell,
cwd: raw.cwd,
env: raw.env,
on_fail: raw.on_fail,
})
}