// // 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::HashMap; 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)] pub struct Condition { #[serde(rename = "has_env", skip_serializing_if = "Option::is_none")] #[getset(get = "pub")] pub(super) has_env: Option>, #[serde(rename = "env_eq", skip_serializing_if = "Option::is_none")] #[getset(get = "pub")] pub(super) env_eq: Option>, #[serde(rename = "in_image", skip_serializing_if = "Option::is_none")] #[getset(get = "pub")] pub(super) in_image: Option>, } impl Condition { #[cfg(test)] pub fn new(has_env: Option>, env_eq: Option>, in_image: Option>) -> 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 { 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 { 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 { 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 { 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) } } } /// Manual implementation of PartialOrd for Condition /// /// Because HashMap does not implement PartialOrd impl PartialOrd for Condition { fn partial_cmp(&self, other: &Self) -> Option { use std::cmp::Ordering as O; let cmp_has_env = match (self.has_env.as_ref(), other.has_env.as_ref()) { (Some(a), Some(b)) => a.partial_cmp(b), (Some(_), None) => Some(O::Greater), (None, Some(_)) => Some(O::Less), (None, None) => Some(O::Equal), }; if cmp_has_env.as_ref().map(|o| *o != O::Equal).unwrap_or(false) { return cmp_has_env } let cmp_env_eq = match (self.env_eq.as_ref(), other.env_eq.as_ref()) { // TODO: Is this safe? We ignore the HashMaps here and just say they are equal. They are most certainly not. (Some(_), Some(_)) => Some(O::Equal), (Some(_), None) => Some(O::Greater), (None, Some(_)) => Some(O::Less), (None, None) => Some(O::Equal), }; if cmp_env_eq.as_ref().map(|o| *o != O::Equal).unwrap_or(false) { return cmp_env_eq } match (self.in_image.as_ref(), other.in_image.as_ref()) { (Some(a), Some(b)) => a.partial_cmp(b), (Some(_), None) => Some(O::Greater), (None, Some(_)) => Some(O::Less), (None, None) => Some(O::Equal), } } } /// Manual implementation of Ord for Condition /// /// Because HashMap does not implement Ord impl Ord for Condition { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal) } } /// Manual implementation of Hash for Condition /// /// Because HashMap does not implement Hash impl std::hash::Hash for Condition { fn hash(&self, state: &mut H) { self.has_env.hash(state); if let Some(hm) = self.env_eq.as_ref() { hm.iter().for_each(|(k, v)| (k, v).hash(state)); }; self.in_image.hash(state); } } /// Helper type for supporting Vec and T in value /// position of Condition #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] #[serde(untagged)] pub enum OneOrMore { One(T), More(Vec), } impl Into> for OneOrMore { fn into(self) -> Vec { match self { OneOrMore::One(o) => vec![o], OneOrMore::More(m) => m, } } } #[cfg(test)] impl From> for OneOrMore { fn from(v: Vec) -> Self { OneOrMore::More(v) } } #[cfg(test)] impl From for OneOrMore { 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)], } #[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::::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::::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 = HashMap::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::::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::::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::::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::::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::::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::::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::::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 = HashMap::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 = HashMap::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 = HashMap::new(); hm.insert(EnvironmentVariableName::from("A"), String::from("1")); Some(hm) }, None); assert!(condition.matches(&data).unwrap()); } }