diff options
Diffstat (limited to 'src/package/dependency/condition.rs')
-rw-r--r-- | src/package/dependency/condition.rs | 420 |
1 files changed, 420 insertions, 0 deletions
diff --git a/src/package/dependency/condition.rs b/src/package/dependency/condition.rs new file mode 100644 index 0000000..c39ece6 --- /dev/null +++ b/src/package/dependency/condition.rs @@ -0,0 +1,420 @@ +// +// Copyright (c) 2020-2021 science+computing ag and other contributors +// +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// + +use std::collections::BTreeMap; + +use serde::Deserialize; +use serde::Serialize; +use getset::Getters; +use anyhow::Result; + +use crate::util::EnvironmentVariableName; +use crate::util::docker::ImageName; + +/// The Condition type +/// +/// This type represents a condition whether a dependency should be included in the package tree or +/// not. +/// +/// Right now, we are supporting condition by environment (set or equal) or whether a specific +/// build image is used. +/// All these settings are optional, of course. +/// +#[derive(Serialize, Deserialize, Getters, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Condition { + #[serde(rename = "has_env", skip_serializing_if = "Option::is_none")] + #[getset(get = "pub")] + pub(super) has_env: Option<OneOrMore<EnvironmentVariableName>>, + + #[serde(rename = "env_eq", skip_serializing_if = "Option::is_none")] + #[getset(get = "pub")] + pub(super) env_eq: Option<BTreeMap<EnvironmentVariableName, String>>, + + #[serde(rename = "in_image", skip_serializing_if = "Option::is_none")] + #[getset(get = "pub")] + pub(super) in_image: Option<OneOrMore<String>>, +} + +impl Condition { + #[cfg(test)] + pub fn new(has_env: Option<OneOrMore<EnvironmentVariableName>>, + env_eq: Option<BTreeMap<EnvironmentVariableName, String>>, + in_image: Option<OneOrMore<String>>) + -> Self + { + Condition { has_env, env_eq, in_image } + } + + /// Check whether the condition matches a certain set of data + /// + /// # Return value + /// + /// Always returns Ok(_) in the current implementation + pub fn matches(&self, data: &ConditionData<'_>) -> Result<bool> { + if !self.matches_env_cond(data)? { + return Ok(false) + } + + if !self.matches_env_eq_cond(data)? { + return Ok(false) + } + + if !self.matches_in_image_cond(data)? { + return Ok(false) + } + + Ok(true) + } + + fn matches_env_cond(&self, data: &ConditionData<'_>) -> Result<bool> { + if let Some(has_env_cond) = self.has_env.as_ref() { + let b = match has_env_cond { + OneOrMore::One(env) => data.env.iter().any(|(name, _)| env == name), + OneOrMore::More(envs) => envs.iter().all(|required_env| { + data.env + .iter() + .any(|(name, _)| name == required_env) + }) + }; + + if !b { + return Ok(false) + } + } + + Ok(true) + } + + fn matches_env_eq_cond(&self, data: &ConditionData<'_>) -> Result<bool> { + if let Some(env_eq_cond) = self.env_eq.as_ref() { + let b = env_eq_cond.iter() + .all(|(req_env_name, req_env_val)| { + data.env + .iter() + .find(|(env_name, _)| env_name == req_env_name) + .map(|(_, env_val)| env_val == req_env_val) + .unwrap_or(false) + }); + + if !b { + return Ok(false) + } + } + + Ok(true) + } + + fn matches_in_image_cond(&self, data: &ConditionData<'_>) -> Result<bool> { + if let Some(in_image_cond) = self.in_image.as_ref() { + let b = match in_image_cond { + OneOrMore::One(req_image) => { + // because the image_name in the ConditionData is Option, + // which is a design-decision because the image can be not-specified (in the + // "tree-of" subcommand), + // we automatically use `false` as value here. + // + // That is because if we need to have a certain image (which is what this + // condition expresses), and there is no image specified in the ConditionData, + // we are by definition are NOT in this image. + data.image_name + .as_ref() + .map(|i| i.as_ref() == req_image) + .unwrap_or(false) + }, + OneOrMore::More(req_images) => { + req_images.iter() + .any(|ri| { + data.image_name + .as_ref() + .map(|inam| inam.as_ref() == ri) + .unwrap_or(false) + }) + }, + }; + + Ok(b) + } else { + Ok(true) + } + } +} + + +/// Helper type for supporting Vec<T> and T in value +/// position of Condition +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[serde(untagged)] +pub enum OneOrMore<T: Sized> { + One(T), + More(Vec<T>), +} + +#[allow(clippy::from_over_into)] +impl<T: Sized> Into<Vec<T>> for OneOrMore<T> { + fn into(self) -> Vec<T> { + match self { + OneOrMore::One(o) => vec![o], + OneOrMore::More(m) => m, + } + } +} + +#[cfg(test)] +impl From<Vec<String>> for OneOrMore<String> { + fn from(v: Vec<String>) -> Self { + OneOrMore::More(v) + } +} + +#[cfg(test)] +impl From<String> for OneOrMore<String> { + fn from(s: String) -> Self { + OneOrMore::One(s) + } +} + + +#[derive(Debug)] +pub struct ConditionData<'a> { + pub(crate) image_name: Option<&'a ImageName>, + pub(crate) env: &'a [(EnvironmentVariableName, String)], +} + +/// Trait for all things that have a condition that can be checked against ConditionData. +/// +/// To be implemented by dependency types. +/// +/// # Return value +/// +/// Ok(true) if the dependency is relevant, considering the ConditionData +/// Ok(false) if the dependency should be ignored, considering the ConditionData +/// Err(_) if the condition checking failed (see `Condition::matches`) +/// +pub trait ConditionCheckable { + fn check_condition(&self, data: &ConditionData<'_>) -> Result<bool>; +} + +impl ConditionCheckable for crate::package::BuildDependency { + fn check_condition(&self, data: &ConditionData<'_>) -> Result<bool> { + match self { + // If the dependency is a simple one, e.g. "foo =1.2.3", there is no condition, so the + // dependency has always to be used + crate::package::BuildDependency::Simple(_) => Ok(true), + crate::package::BuildDependency::Conditional { condition, .. } => condition.matches(data), + } + } +} + +impl ConditionCheckable for crate::package::Dependency { + fn check_condition(&self, data: &ConditionData<'_>) -> Result<bool> { + match self { + // If the dependency is a simple one, e.g. "foo =1.2.3", there is no condition, so the + // dependency has always to be used + crate::package::Dependency::Simple(_) => Ok(true), + crate::package::Dependency::Conditional { condition, .. } => condition.matches(data), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_has_env_deserialization() { + let s = r#"has_env = "foo""#; + let c: Condition = toml::from_str(s).expect("Deserializing has_env"); + + assert_eq!(c.has_env.unwrap(), OneOrMore::<EnvironmentVariableName>::One(EnvironmentVariableName::from("foo"))); + assert!(c.env_eq.is_none()); + assert!(c.in_image.is_none()); + } + + #[test] + fn test_has_env_list_deserialization() { + let s = r#"has_env = ["foo", "bar"]"#; + let c: Condition = toml::from_str(s).expect("Deserializing has_env"); + + assert_eq!(c.has_env.unwrap(), { + OneOrMore::<EnvironmentVariableName>::More({ + vec![EnvironmentVariableName::from("foo"), EnvironmentVariableName::from("bar")] + }) + }); + assert!(c.env_eq.is_none()); + assert!(c.in_image.is_none()); + } + + #[test] + fn test_env_eq_deserialization() { + let s = r#"env_eq = { "foo" = "bar" }"#; + let c: Condition = toml::from_str(s).expect("Deserializing has_env"); + + assert!(c.has_env.is_none()); + assert_eq!(c.env_eq.unwrap(), { + let mut hm = BTreeMap::new(); + hm.insert(EnvironmentVariableName::from("foo"), String::from("bar")); + hm + }); + assert!(c.in_image.is_none()); + } + + #[test] + fn test_in_image_deserialization() { + let s = r#"in_image = "foo""#; + let c: Condition = toml::from_str(s).expect("Deserializing has_env"); + + assert!(c.has_env.is_none()); + assert!(c.env_eq.is_none()); + assert_eq!(c.in_image.unwrap(), OneOrMore::<String>::One(String::from("foo"))); + } + + #[test] + fn test_in_image_list_deserialization() { + let s = r#"in_image = ["foo"]"#; + let c: Condition = toml::from_str(s).expect("Deserializing has_env"); + + assert!(c.has_env.is_none()); + assert!(c.env_eq.is_none()); + assert_eq!(c.in_image.unwrap(), OneOrMore::<String>::More(vec![String::from("foo")])); + } + + #[test] + fn test_condition_empty() { + let data = ConditionData { + image_name: None, + env: &[], + }; + + let condition = Condition::new(None, None, None); + + assert!(condition.matches(&data).unwrap()); + } + + #[test] + fn test_condition_no_image() { + let data = ConditionData { + image_name: None, + env: &[], + }; + + let condition = Condition::new(None, None, { + Some(OneOrMore::<String>::One(String::from("req_image"))) + }); + + assert!(!condition.matches(&data).unwrap()); + } + + #[test] + fn test_condition_matching_image() { + let img = ImageName::from("required_image"); + let data = ConditionData { + image_name: Some(&img), + env: &[], + }; + + let condition = Condition::new(None, None, { + Some(OneOrMore::<String>::One(String::from("required_image"))) + }); + + assert!(condition.matches(&data).unwrap()); + } + + #[test] + fn test_condition_nonmatching_image() { + let img = ImageName::from("required_image"); + let data = ConditionData { + image_name: Some(&img), + env: &[], + }; + + let condition = Condition::new(None, None, { + Some(OneOrMore::<String>::One(String::from("other_image"))) + }); + + assert!(!condition.matches(&data).unwrap()); + } + + #[test] + fn test_condition_required_env_missing() { + let data = ConditionData { + image_name: None, + env: &[], + }; + + let condition = Condition::new({ + Some(OneOrMore::<EnvironmentVariableName>::One(EnvironmentVariableName::from("A"))) + }, None, None); + + assert!(!condition.matches(&data).unwrap()); + } + + #[test] + fn test_condition_required_env_present() { + let data = ConditionData { + image_name: None, + env: &[(EnvironmentVariableName::from("A"), String::from("1"))], + }; + + let condition = Condition::new({ + Some(OneOrMore::<EnvironmentVariableName>::One(EnvironmentVariableName::from("A"))) + }, None, None); + + assert!(condition.matches(&data).unwrap()); + } + + #[test] + fn test_condition_required_env_values_missing() { + let data = ConditionData { + image_name: None, + env: &[], + }; + + let condition = Condition::new(None, { + let mut hm = BTreeMap::new(); + hm.insert(EnvironmentVariableName::from("A"), String::from("1")); + Some(hm) + }, None); + + assert!(!condition.matches(&data).unwrap()); + } + + #[test] + fn test_condition_required_env_values_present_but_different() { + let data = ConditionData { + image_name: None, + env: &[(EnvironmentVariableName::from("A"), String::from("1"))], + }; + + let condition = Condition::new(None, { + let mut hm = BTreeMap::new(); + hm.insert(EnvironmentVariableName::from("A"), String::from("2")); + Some(hm) + }, None); + + assert!(!condition.matches(&data).unwrap()); + } + + #[test] + fn test_condition_required_env_values_present_and_equal() { + let data = ConditionData { + image_name: None, + env: &[(EnvironmentVariableName::from("A"), String::from("1"))], + }; + + let condition = Condition::new(None, { + let mut hm = BTreeMap::new(); + hm.insert(EnvironmentVariableName::from("A"), String::from("1")); + Some(hm) + }, None); + + assert!(condition.matches(&data).unwrap()); + } + +} |