use std::collections::HashSet;
use crate::{Invoice, Parcel};
#[derive(Clone)]
struct FeatureReference {
group: String,
name: String,
value: String,
}
pub struct BindleFilter<'a> {
invoice: &'a Invoice,
groups: HashSet<String>,
exclude_groups: HashSet<String>,
features: Vec<FeatureReference>,
exclude_features: Vec<FeatureReference>,
}
impl<'a> BindleFilter<'a> {
pub fn new(invoice: &'a Invoice) -> Self {
Self {
invoice,
groups: HashSet::new(),
exclude_groups: HashSet::new(),
features: vec![],
exclude_features: vec![],
}
}
pub fn with_group(&mut self, group_name: &str) -> &mut Self {
self.groups.insert(group_name.to_owned());
self
}
pub fn without_group(&mut self, group_name: &str) -> &mut Self {
self.exclude_groups.insert(group_name.to_owned());
self
}
pub fn activate_feature(&mut self, group: &str, name: &str, value: &str) -> &mut Self {
self.features
.retain(|i| !(i.name == name && i.group == group));
self.features.push(FeatureReference {
group: group.to_owned(),
name: name.to_owned(),
value: value.to_owned(),
});
self
}
pub fn deactivate_feature(&mut self, group: &str, name: &str, value: &str) -> &mut Self {
self.exclude_features.push(FeatureReference {
group: group.to_owned(),
name: name.to_owned(),
value: value.to_owned(),
});
self
}
fn is_disabled(&self, parcel: &Parcel) -> bool {
match &parcel.label.feature {
None => false,
Some(feat) => {
self.exclude_features
.iter()
.any(|key| match feat.get(&key.group) {
None => false,
Some(features) => {
features.get(&key.name).unwrap_or(&"".to_owned()) == &key.value
}
})
||
self.features.iter().any(|key| match feat.get(&key.group) {
None => false, Some(features) => {
!features.get(&key.name).map(|val| val == &key.value).unwrap_or(false)
}
})
}
}
}
pub fn filter(&self) -> Vec<Parcel> {
let mut groups: HashSet<String> = match &self.invoice.group {
Some(group) => {
group
.iter()
.filter(|&i| {
if self.exclude_groups.contains(&i.name) {
return false;
}
i.required.unwrap_or(false) || self.groups.contains(&i.name)
})
.map(|g| g.name.clone())
.collect()
}
None => HashSet::new(),
};
let zero_vec = Vec::with_capacity(0);
let mut parcels: HashSet<Parcel> = self
.invoice
.parcel
.as_ref()
.unwrap_or(&zero_vec)
.iter()
.filter(|p| {
p.conditions
.as_ref()
.map(|c| {
match &c.member_of {
None => true,
Some(gnames) => gnames.iter().any(|n| groups.contains(n)),
}
})
.unwrap_or(true) })
.filter(|p| !self.is_disabled(p))
.cloned()
.collect();
let dependencies: HashSet<Parcel> = parcels.iter().fold(HashSet::new(), |mut deps, p| {
if let Some(extras) = self.walk_parcel_requires(p, &mut groups) {
deps.extend(extras)
}
deps
});
parcels.extend(dependencies);
parcels.into_iter().collect()
}
fn walk_parcel_requires(
&self,
p: &Parcel,
groupset: &mut HashSet<String>,
) -> Option<HashSet<Parcel>> {
let mut ret: HashSet<Parcel> = HashSet::new();
if let Some(c) = p.conditions.as_ref() {
if let Some(req) = &c.requires {
req.iter().for_each(|r| {
if groupset.contains(r) {
return;
}
groupset.insert(r.to_owned());
if let Some(pvec) = self.invoice.parcel.as_ref() {
pvec.iter().for_each(|p| {
if let Some(c) = p.conditions.as_ref() {
if let Some(groups) = &c.member_of {
if groups.iter().any(|g| g == r) {
if self.is_disabled(p) {
return;
}
ret.insert(p.clone());
let sub = self.walk_parcel_requires(p, groupset);
if let Some(extras) = sub {
ret.extend(extras)
}
}
}
}
});
}
})
}
}
if ret.is_empty() {
return None;
}
Some(ret)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_global_group() {
let toml = r#"
bindleVersion = "1.0.0"
[bindle]
name = "test/is-disabled"
version = "0.1.0"
[[group]]
name = "clownfish"
[[group]]
name = "unused"
# Not in global
[[parcel]]
[parcel.label]
name = "not_global"
sha256 = "12345"
mediaType = "application/octet-stream"
size = 123
[parcel.conditions]
memberOf = ["clownfish"]
# Not in global because an empty membership is non-global.
[[parcel]]
[parcel.label]
name = "also_not_global"
sha256 = "12345"
mediaType = "application/octet-stream"
size = 123
[parcel.conditions]
memberOf = [""]
# This is in the global group
[[parcel]]
[parcel.label]
name = "is_global"
sha256 = "4321"
mediaType = "application/octet-stream"
size = 321
[parcel.conditions]
requires = ["unused"]
# This is in the global group
[[parcel]]
[parcel.label]
name = "also_is_global"
sha256 = "4321"
mediaType = "application/octet-stream"
size = 321
"#;
let inv: crate::Invoice = toml::from_str(toml).expect("test invoice parsed");
let filter = BindleFilter::new(&inv).filter();
assert_eq!(2, filter.len());
}
#[test]
fn test_deactivate_feature() {
let toml = r#"
bindleVersion = "1.0.0"
[bindle]
name = "test/is-disabled"
version = "0.1.0"
[[parcel]]
[parcel.label]
name = "is_disabled"
sha256 = "12345"
mediaType = "application/octet-stream"
size = 123
[parcel.label.feature.testing]
disabled = "true"
[[parcel]]
[parcel.label]
name = "not_disabled"
sha256 = "4321"
mediaType = "application/octet-stream"
size = 321
"#;
let inv: crate::Invoice = toml::from_str(toml).expect("test invoice parsed");
{
let filter = BindleFilter::new(&inv).filter();
assert_eq!(2, filter.len());
}
{
let filter = BindleFilter::new(&inv)
.deactivate_feature("testing", "disabled", "true")
.filter();
assert_eq!(1, filter.len());
}
{
let filter = BindleFilter::new(&inv)
.deactivate_feature("testing", "disabled", "true")
.activate_feature("testing", "disabled", "true")
.filter();
assert_eq!(1, filter.len());
}
{
let filter = BindleFilter::new(&inv)
.deactivate_feature("testing", "disabled", "false")
.filter();
assert_eq!(2, filter.len());
}
}
#[test]
fn test_activate_feature() {
let toml = r#"
bindleVersion = "1.0.0"
[bindle]
name = "test/activate"
version = "0.1.0"
[[parcel]]
[parcel.label]
name = "narwhal_handler"
sha256 = "12345"
mediaType = "application/octet-stream"
size = 123
[parcel.label.feature.testing]
animal = "narwhal"
color = "blue"
[[parcel]]
[parcel.label]
name = "unicorn_handler"
sha256 = "4321"
mediaType = "application/octet-stream"
size = 321
[parcel.label.feature.testing]
animal = "unicorn"
color = "blue"
[[parcel]]
[parcel.label]
name = "default_thinger"
sha256 = "5432"
mediaType = "application/octet-stream"
size = 321
"#;
let inv: crate::Invoice = toml::from_str(toml).expect("test invoice parsed");
{
let filter = BindleFilter::new(&inv).filter();
assert_eq!(3, filter.len());
}
{
let filter = BindleFilter::new(&inv)
.activate_feature("testing", "animal", "narwhal")
.filter();
assert_eq!(2, filter.len());
assert!(filter.iter().any(|p| p.label.name == "narwhal_handler"));
assert!(!filter.iter().any(|p| p.label.name == "unicorn_handler"));
}
{
let filter = BindleFilter::new(&inv)
.activate_feature("testing", "animal", "narwhal")
.deactivate_feature("testing", "animal", "narwhal")
.filter();
assert_eq!(1, filter.len());
}
{
let filter = BindleFilter::new(&inv)
.activate_feature("testing", "animal", "narwhal")
.activate_feature("testing", "animal", "unicorn")
.filter();
assert_eq!(2, filter.len());
assert!(filter.iter().any(|p| p.label.name == "unicorn_handler"));
}
}
#[test]
fn test_required_groups() {
let toml = r#"
bindleVersion = "1.0.0"
[bindle]
name = "test/is-disabled"
version = "0.1.0"
[[group]]
name = "is_required"
required = true
[[group]]
name = "is_optional"
[[group]]
name = "also_optional"
[[parcel]]
[parcel.label]
name = "first"
sha256 = "12345"
mediaType = "application/octet-stream"
size = 123
[parcel.conditions]
memberOf = ["is_required"]
[[parcel]]
[parcel.label]
name = "second"
sha256 = "4321"
mediaType = "application/octet-stream"
size = 321
[parcel.conditions]
memberOf = ["is_optional", "also_optional"]
[[parcel]]
[parcel.label]
name = "third"
sha256 = "4321"
mediaType = "application/octet-stream"
size = 321
[parcel.conditions]
memberOf = ["also_optional"]
"#;
let inv: crate::Invoice = toml::from_str(toml).expect("test invoice parsed");
{
let filter = BindleFilter::new(&inv).filter();
assert_eq!(1, filter.len());
}
{
let filter = BindleFilter::new(&inv).with_group("is_optional").filter();
assert_eq!(2, filter.len());
}
{
let filter = BindleFilter::new(&inv)
.with_group("is_optional")
.with_group("also_optional")
.filter();
assert_eq!(3, filter.len());
}
}
#[test]
fn test_circular_dependency() {
let toml = r#"
bindleVersion = "1.0.0"
[bindle]
name = "test/is-disabled"
version = "0.1.0"
[[group]]
name = "first"
required = true
[[group]]
name = "second"
[[group]]
name = "third"
[[parcel]]
[parcel.label]
name = "p1"
sha256 = "12345"
mediaType = "application/octet-stream"
size = 123
[parcel.conditions]
memberOf = ["first"]
requires = ["second"]
[[parcel]]
[parcel.label]
name = "p2"
sha256 = "4321"
mediaType = "application/octet-stream"
size = 321
[parcel.conditions]
memberOf = ["second"]
requires =[ "third"]
[[parcel]]
[parcel.label]
name = "p3"
sha256 = "4321"
mediaType = "application/octet-stream"
size = 321
[parcel.conditions]
memberOf = ["third"]
requires = ["first", "second"] # should not cause an infinite loop
"#;
let inv: crate::Invoice = toml::from_str(toml).expect("test invoice parsed");
let filter = BindleFilter::new(&inv).filter();
assert_eq!(3, filter.len());
}
#[test]
fn test_dependency_resolution() {
let toml = r#"
bindleVersion = "1.0.0"
[bindle]
name = "test/is-disabled"
version = "0.1.0"
[[group]]
name = "first"
required = true
[[group]]
name = "second"
[[group]]
name = "third"
[[parcel]]
[parcel.label]
name = "p1"
sha256 = "12345"
mediaType = "application/octet-stream"
size = 123
[parcel.conditions]
memberOf = ["first"]
requires = ["second"]
[[parcel]]
[parcel.label]
name = "p2"
sha256 = "4321"
mediaType = "application/octet-stream"
size = 321
[parcel.conditions]
memberOf = ["second"]
requires =[ "third"]
[[parcel]]
[parcel.label]
name = "p3"
sha256 = "4321"
mediaType = "application/octet-stream"
size = 321
[parcel.conditions]
memberOf = ["third"]
"#;
let inv: crate::Invoice = toml::from_str(toml).expect("test invoice parsed");
{
let filter = BindleFilter::new(&inv).filter();
assert_eq!(3, filter.len());
}
{
let filter = BindleFilter::new(&inv).without_group("first").filter();
assert_eq!(0, filter.len());
}
}
const TEST_BINDLE_FILTERS_INVOICE: &str = r#"
bindleVersion = "1.0.0"
[bindle]
name = "example/weather-progressive"
version = "0.1.0"
authors = ["Matt Butcher <matt.butcher@microsoft.com>"]
description = "Weather Prediction"
[[group]]
name = "entrypoint"
satisfiedBy = "oneOf"
required = true
[[group]]
name = "ui-support"
satisfiedBy = "allOf"
required = false
[[parcel]]
[parcel.label]
sha256 = "4cb048264cef43e4fead1701e48f3287d3538647"
mediaType = "application/wasm"
name = "weather-ui.wasm"
size = 1710256
[parcel.label.feature.wasm]
ui-kit = "electron+sgu"
[parcel.conditions]
memberOf = ["entrypoint"]
requires = ["ui-support"]
[[parcel]]
[parcel.label]
sha256 = "048264cef43e4fead1701e48f3287d35386474cb"
mediaType = "application/wasm"
name = "weather-cli.wasm"
size = 1410256
[parcel.conditions]
memberOf = ["entrypoint"]
[[parcel]]
[parcel.label]
sha256 = "4cb048264cef43e4fead1701e48f3287d3538647"
mediaType = "application/wasm"
name = "libalmanac.wasm"
size = 2561710
[parcel.label.feature.wasm]
type = "library"
[[parcel]]
[parcel.label]
sha256 = "4cb048264cef43e4fead1701e48f3287d3538647"
mediaType = "text/html"
name = "almanac-ui.html"
size = 2561710
[parcel.label.feature.wasm]
type = "data"
[parcel.conditions]
memberOf = ["ui-support"]
[[parcel]]
[parcel.label]
sha256 = "4cb048264cef43e4fead1701e48f3287d3538647"
mediaType = "text/css"
name = "styles.css"
size = 2561710
[parcel.label.feature.wasm]
type = "data"
[parcel.conditions]
memberOf = ["ui-support"]
[[parcel]]
[parcel.label]
sha256 = "4cb048264cef43e4fead1701e48f3287d3538647"
mediaType = "application/wasm"
name = "uibuilder.wasm"
size = 2561710
[parcel.label.feature.wasm]
type = "library"
[parcel.conditions]
memberOf = ["ui-support"]
"#;
#[test]
fn test_bindle_filters() {
let inv: crate::Invoice =
toml::from_str(TEST_BINDLE_FILTERS_INVOICE).expect("test invoice parsed");
{
let filter = BindleFilter::new(&inv).filter();
assert_eq!(6, filter.len());
}
{
let filter = BindleFilter::new(&inv).without_group("entrypoint").filter();
assert_eq!(1, filter.len());
}
}
}