Repository: frewsxcv/cargo-all-features (language: rust)

Issue:
Add ability to allowlist features to be mixed in Cargo.toml
My crate has a lot of features and the exponential growth is becoming a problem. I don't actually care about the full matrix, I'd like a subset of them to be cargo-all-features-tested. Would it be possible to get a metadata key for this?


Discussion / hints:
Sounds good to me 👍🏻 

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index ec7facd..c035f28 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "cargo-all-features"
-version = "1.5.0"
+version = "1.6.0"
 authors = ["Corey Farwell <coreyf@rwell.org>"]
 edition = "2018"
 description = "A Cargo subcommand to build and test all feature flag combinations"
diff --git a/README.md b/README.md
index 2800a09..638c8a5 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,13 @@ skip_optional_dependencies = true
 extra_features = [
     "log",
 ]
+
+# Exclude certain features from the build matrix
+denylist = ["foo", "bar"]
+
+# Only include certain features in the build matrix
+#(incompatible with `denylist`, `skip_optional_dependencies`, and `extra_features`)
+allowlist = ["foo", "bar"]
 ```
 
 ## License
diff --git a/src/cargo_metadata.rs b/src/cargo_metadata.rs
index 4784ad3..ab42e5c 100644
--- a/src/cargo_metadata.rs
+++ b/src/cargo_metadata.rs
@@ -1,9 +1,11 @@
+use std::collections::HashSet;
+use std::convert::TryFrom;
 use std::{error, path, process};
 
 pub fn fetch() -> Result<Metadata, Box<dyn error::Error>> {
     let json = fetch_cargo_metadata_json()?;
     let json_value = json::parse(&json)?;
-    Ok(Metadata::from(json_value))
+    Ok(Metadata::try_from(json_value)?)
 }
 
 fn fetch_cargo_metadata_json() -> Result<String, Box<dyn error::Error>> {
@@ -33,7 +35,11 @@ impl From<json::JsonValue> for Dependency {
         let rename = json_value["rename"].as_str().map(|s| s.to_string());
         let optional = json_value["optional"].as_bool().unwrap();
 
-        Dependency { name, rename, optional }
+        Dependency {
+            name,
+            rename,
+            optional,
+        }
     }
 }
 
@@ -46,11 +52,14 @@ pub struct Package {
     pub features: Vec<String>,
     pub skip_feature_sets: Vec<Vec<String>>,
     pub skip_optional_dependencies: bool,
+    pub allowlist: Vec<String>,
+    pub denylist: HashSet<String>,
     pub extra_features: Vec<String>,
 }
 
-impl From<json::JsonValue> for Package {
-    fn from(json_value: json::JsonValue) -> Self {
+impl TryFrom<json::JsonValue> for Package {
+    type Error = String;
+    fn try_from(json_value: json::JsonValue) -> Result<Self, String> {
         let id = json_value["id"].as_str().unwrap().to_owned();
         let name = json_value["name"].as_str().unwrap().to_owned();
         let manifest_path =
@@ -63,8 +72,7 @@ impl From<json::JsonValue> for Package {
             .entries()
             .map(|(k, _v)| k.to_owned())
             .collect();
-        let skip_feature_sets: Vec<Vec<String>> = json_value["metadata"]
-            ["cargo-all-features"]
+        let skip_feature_sets: Vec<Vec<String>> = json_value["metadata"]["cargo-all-features"]
             ["skip_feature_sets"]
             .members()
             .map(|member| {
@@ -74,19 +82,47 @@ impl From<json::JsonValue> for Package {
                     .collect()
             })
             .collect();
-        let skip_optional_dependencies: bool = json_value["metadata"]
-            ["cargo-all-features"]
-            ["skip_optional_dependencies"]
-            .as_bool()
-            .unwrap_or(false);
-        let extra_features: Vec<String> = json_value["metadata"]
-            ["cargo-all-features"]
+        let maybe_skip_optional =
+            json_value["metadata"]["cargo-all-features"]["skip_optional_dependencies"].as_bool();
+        let skip_optional_dependencies: bool = maybe_skip_optional.unwrap_or(false);
+        let extra_features: Vec<String> = json_value["metadata"]["cargo-all-features"]
             ["extra_features"]
             .members()
             .map(|member| member.as_str().unwrap().to_owned())
             .collect();
 
-        Package {
+        let allowlist: Vec<String> = json_value["metadata"]["cargo-all-features"]["allowlist"]
+            .members()
+            .map(|member| member.as_str().unwrap().to_owned())
+            .collect();
+
+        let denylist: HashSet<String> = json_value["metadata"]["cargo-all-features"]["denylist"]
+            .members()
+            .map(|member| member.as_str().unwrap().to_owned())
+            .collect();
+
+        if !allowlist.is_empty() {
+            if !denylist.is_empty() {
+                return Err(format!(
+                    "Package {} has both `allowlist` and `denylist` keys",
+                    name
+                ));
+            }
+            if !extra_features.is_empty() {
+                return Err(format!(
+                    "Package {} has both `allowlist` and `extra_features` keys",
+                    name
+                ));
+            }
+            if maybe_skip_optional.is_some() {
+                return Err(format!(
+                    "Package {} has both `allowlist` and `skip_optional_dependencies` keys",
+                    name
+                ));
+            }
+        }
+
+        Ok(Package {
             id,
             name,
             manifest_path,
@@ -95,7 +131,9 @@ impl From<json::JsonValue> for Package {
             skip_feature_sets,
             skip_optional_dependencies,
             extra_features,
-        }
+            allowlist,
+            denylist,
+        })
     }
 }
 
@@ -106,8 +144,9 @@ pub struct Metadata {
     pub packages: Vec<Package>,
 }
 
-impl From<json::JsonValue> for Metadata {
-    fn from(json_value: json::JsonValue) -> Self {
+impl TryFrom<json::JsonValue> for Metadata {
+    type Error = String;
+    fn try_from(json_value: json::JsonValue) -> Result<Self, String> {
         let workspace_root =
             path::PathBuf::from(json_value["workspace_root"].as_str().unwrap().to_owned());
 
@@ -118,13 +157,13 @@ impl From<json::JsonValue> for Metadata {
 
         let packages = json_value["packages"]
             .members()
-            .map(|member| Package::from(member.to_owned()))
-            .collect();
+            .map(|member| Package::try_from(member.to_owned()))
+            .collect::<Result<_, String>>()?;
 
-        Metadata {
+        Ok(Metadata {
             workspace_root,
             workspace_members,
             packages,
-        }
+        })
     }
 }
diff --git a/src/features_finder.rs b/src/features_finder.rs
index 8943bd5..e0a4cb6 100644
--- a/src/features_finder.rs
+++ b/src/features_finder.rs
@@ -2,11 +2,25 @@ use itertools::Itertools;
 
 pub fn fetch_feature_sets(package: &crate::cargo_metadata::Package) -> Vec<Vec<String>> {
     let mut features = vec![];
-    if !package.skip_optional_dependencies {
-        features.append(&mut fetch_optional_dependencies(&package));
-    }
-    features.append(&mut fetch_features(&package));
-    features.extend(package.extra_features.iter().cloned());
+    let filter_denylist = |f: &String| !package.denylist.contains(f);
+
+    if package.allowlist.is_empty() {
+        if !package.skip_optional_dependencies {
+            features.extend(fetch_optional_dependencies(&package).filter(filter_denylist));
+        }
+        features.extend(fetch_features(&package).filter(filter_denylist));
+        features.extend(
+            package
+                .extra_features
+                .iter()
+                .cloned()
+                .filter(filter_denylist),
+        );
+    } else {
+        // allowlist cannot be mixed with denylist or any of the other above options,
+        // no need to filter
+        features.extend(package.allowlist.iter().cloned())
+    };
 
     let mut feature_sets = vec![];
 
@@ -29,7 +43,9 @@ pub fn fetch_feature_sets(package: &crate::cargo_metadata::Package) -> Vec<Vec<S
     feature_sets
 }
 
-fn fetch_optional_dependencies(package: &crate::cargo_metadata::Package) -> Vec<String> {
+fn fetch_optional_dependencies(
+    package: &crate::cargo_metadata::Package,
+) -> impl Iterator<Item = String> + '_ {
     package
         .dependencies
         .iter()
@@ -41,10 +57,9 @@ fn fetch_optional_dependencies(package: &crate::cargo_metadata::Package) -> Vec<
                 dependency.name.to_string()
             }
         })
-        .collect()
 }
 
-fn fetch_features(package: &crate::cargo_metadata::Package) -> Vec<String> {
+fn fetch_features(package: &crate::cargo_metadata::Package) -> impl Iterator<Item = String> + '_ {
     package
         .features
         .iter()
@@ -52,5 +67,4 @@ fn fetch_features(package: &crate::cargo_metadata::Package) -> Vec<String> {
         // Some crates use "__" to indicate internal features
         .filter(|key| !key.starts_with("__"))
         .cloned()
-        .collect()
 }


Test patch: