ci

CI for all possible combinations of features in Cargo.toml.
git clone https://git.philomathiclife.com/repos/ci
Log | Files | Refs | README

commit df7e4b016c9aad07aa08e7abe9573b6d9fc26631
Author: Zack Newman <zack@philomathiclife.com>
Date:   Tue,  6 Aug 2024 18:13:29 -0600

init

Diffstat:
A.gitignore | 2++
ACargo.toml | 24++++++++++++++++++++++++
ALICENSE-APACHE | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE-MIT | 20++++++++++++++++++++
AREADME.md | 33+++++++++++++++++++++++++++++++++
Asrc/args.rs | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main.rs | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/manifest.rs | 278+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 823 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/** diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +authors = ["Zack Newman <zack@philomathiclife.com>"] +categories = ["command-line-utilities", "development-tools::testing", "rust-patterns"] +description = "Continuous integration for Clippy, unit tests, and doc tests for all possible features." +documentation = "https://git.philomathiclife.com/ci/file/README.md.html" +edition = "2021" +keywords = ["cargo", "ci", "rust"] +license = "MIT OR Apache-2.0" +name = "ci" +readme = "README.md" +repository = "https://git.philomathiclife.com/repos/ci/" +version = "0.1.0" + +[badges] +maintenance = { status = "actively-developed" } + +[dependencies] +serde = { version = "1.0.204", default-features = false } +toml = { version = "0.8.19", default-features = false, features = ["parse"] } + +[profile.release] +lto = true +panic = 'abort' +strip = true diff --git a/LICENSE-APACHE b/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT @@ -0,0 +1,20 @@ +Copyright © 2024 Zack Newman + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +“Software”), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md @@ -0,0 +1,33 @@ +# `ci` + +`ci` is a CLI application that runs [`cargo`](https://doc.rust-lang.org/cargo/index.html) with `clippy -q`, +`t -q --doc`, and `t -q --tests` with all possible combinations of features defined in `Cargo.toml`. + +`ci` avoids superfluous combinations of features. For example if feature `foo` automatically enables +feature `bar` and `bar` automatically enables feature `fizz`, then no combination of features that contains +`foo` and `bar`, `foo` and `fizz`, or `bar` and `fizz` will be tested. + +`ci` writes to `stdout` iff an error arises. The error is written as well as the command and features +that caused the error. + +## Why is this useful? + +The number of possible configurations grows exponentially based on the number of features in `Cargo.toml`. +This can easily cause a crate to not be tested with certain combinations of features. Instead of manually +invoking `cargo` with each possible combination of features, this handles it automatically. + +## Options + +* `clippy`: `cargo clippy -q` is invoked for each combination of features. +* `doc_tests`: `cargo t -q --doc` is invoked for each combination of features. +* `tests`: `cargo t -q --tests` is invoked for each combination of features. + +When no options are passed, then all three options above are invoked. + +## Limitations + +All possible combinations of features are tested; thus if `compile_error` is used to prevent certain +combinations, `ci` will error. Since `ci` is designed for testing and development, it should be easy to +temporarily comment out instances of `compile_error`. + +`ci` must be run in the same directory as the `Cargo.toml` file that contains the features. diff --git a/src/args.rs b/src/args.rs @@ -0,0 +1,163 @@ +use super::E; +use core::fmt::{self, Display, Formatter}; +use std::{ + env, + error::Error, + process::{Command, Stdio}, +}; +/// Error returned when parsing arguments passed to the application. +#[allow(clippy::module_name_repetitions)] +#[derive(Clone, Debug)] +pub enum ArgsErr { + /// Error when no arguments exist. + NoArgs, + /// Error when an invalid option is passed. The contained [`String`] + /// is the value of the invalid option. + InvalidOption(String), +} +impl Display for ArgsErr { + #[allow(clippy::ref_patterns)] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::NoArgs => write!(f, "no arguments exist including the name of the process itself"), + Self::InvalidOption(ref arg) => write!(f, "{arg} is an invalid option. No arguments or exactly one of 'clippy', 'doc_tests', or 'tests' is allowed"), + } + } +} +impl Error for ArgsErr {} +/// The options passed to the application. +#[derive(Clone, Copy, Debug)] +pub enum Opts { + /// Variant when no arguments were passed. + None, + /// Variant when `clippy` is passed. + Clippy, + /// Variant when `doc_tests` is passed. + DocTests, + /// Variant when `tests` is passed. + Tests, +} +impl Opts { + /// Returns the string representation of `self`. + const fn as_str(self) -> &'static str { + match self { + Self::None => "", + Self::Clippy => "clippy", + Self::DocTests => "doc_tests", + Self::Tests => "tests", + } + } + /// Runs `cargo` with argument based on `self` and features of `features`. + #[allow(clippy::unreachable)] + pub fn run_cmd(self, features: &str) -> Result<(), E> { + match self { + Self::None => unreachable!("Opts::run_cmd must not be called on Opts::None"), + Self::Clippy => { + let mut args = vec!["clippy", "-q", "--no-default-features"]; + if !features.is_empty() { + args.push("--features"); + args.push(features); + } + let output = Command::new("cargo") + .stderr(Stdio::inherit()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .args(args) + .output() + .map_err(E::Io)?; + if output.status.success() { + Ok(()) + } else { + String::from_utf8(output.stderr) + .map_err(E::Utf8) + .and_then(|mut err| { + err.push_str("\ncargo clippy -q --no-default-features"); + if !features.is_empty() { + err.push_str(" --features "); + err.push_str(features); + } + Err(E::Cmd(err)) + }) + } + } + Self::DocTests => { + let mut args = vec!["t", "-q", "--doc", "--no-default-features"]; + if !features.is_empty() { + args.push("--features"); + args.push(features); + } + let output = Command::new("cargo") + .stderr(Stdio::null()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .args(args) + .output() + .map_err(E::Io)?; + if output.status.success() { + Ok(()) + } else { + String::from_utf8(output.stdout) + .map_err(E::Utf8) + .and_then(|mut err| { + err.push_str("\ncargo t -q --doc --no-default-features"); + if !features.is_empty() { + err.push_str(" --features "); + err.push_str(features); + } + Err(E::Cmd(err)) + }) + } + } + Self::Tests => { + let mut args = vec!["t", "-q", "--tests", "--no-default-features"]; + if !features.is_empty() { + args.push("--features"); + args.push(features); + } + let output = Command::new("cargo") + .stderr(Stdio::null()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .args(args) + .output() + .map_err(E::Io)?; + if output.status.success() { + Ok(()) + } else { + String::from_utf8(output.stdout) + .map_err(E::Utf8) + .and_then(|mut err| { + err.push_str("\ncargo t -q --tests --no-default-features"); + if !features.is_empty() { + err.push_str(" --features "); + err.push_str(features); + } + Err(E::Cmd(err)) + }) + } + } + } + } + /// Returns `Opts` based on arguments passed to the application. + pub fn from_args() -> Result<Self, ArgsErr> { + let mut args = env::args(); + if args.next().is_some() { + let mut opts = Self::None; + for arg in args { + if matches!(opts, Self::None) { + match arg.as_str() { + "clippy" => opts = Self::Clippy, + "doc_tests" => opts = Self::DocTests, + "tests" => opts = Self::Tests, + _ => return Err(ArgsErr::InvalidOption(arg)), + } + } else { + return Err(ArgsErr::InvalidOption(format!("{} {arg}", opts.as_str()))); + } + } + Ok(opts) + } else { + Err(ArgsErr::NoArgs) + } + } +} diff --git a/src/main.rs b/src/main.rs @@ -0,0 +1,126 @@ +//! # `ci` +//! +//! `ci` is a CLI application that runs [`cargo`](https://doc.rust-lang.org/cargo/index.html) with `clippy -q`, +//! `t -q --doc`, and `t -q --tests` with all possible combinations of features defined in `Cargo.toml`. +//! +//! `ci` avoids superfluous combinations of features. For example if feature `foo` automatically enables +//! feature `bar` and `bar` automatically enables feature `fizz`, then no combination of features that contains +//! `foo` and `bar`, `foo` and `fizz`, or `bar` and `fizz` will be tested. +//! +//! `ci` writes to `stdout` iff an error arises. The error is written as well as the command and features +//! that caused the error. +//! +//! ## Why is this useful? +//! +//! The number of possible configurations grows exponentially based on the number of features in `Cargo.toml`. +//! This can easily cause a crate to not be tested with certain combinations of features. Instead of manually +//! invoking `cargo` with each possible combination of features, this handles it automatically. +//! +//! ## Options +//! +//! * `clippy`: `cargo clippy -q` is invoked for each combination of features. +//! * `doc_tests`: `cargo t -q --doc` is invoked for each combination of features. +//! * `tests`: `cargo t -q --tests` is invoked for each combination of features. +//! +//! When no options are passed, then all three options above are invoked. +//! +//! ## Limitations +//! +//! All possible combinations of features are tested; thus if [`compile_error`] is used to prevent certain +//! combinations, `ci` will error. Since `ci` is designed for testing and development, it should be easy to +//! temporarily comment out instances of `compile_error`. +//! +//! `ci` must be run in the same directory as the `Cargo.toml` file that contains the features. +#![deny( + future_incompatible, + let_underscore, + missing_docs, + nonstandard_style, + rust_2018_compatibility, + rust_2018_idioms, + rust_2021_compatibility, + rust_2024_compatibility, + unsafe_code, + unused, + warnings, + clippy::all, + clippy::cargo, + clippy::complexity, + clippy::correctness, + clippy::nursery, + clippy::pedantic, + clippy::perf, + clippy::restriction, + clippy::style, + clippy::suspicious +)] +#![allow( + clippy::blanket_clippy_restriction_lints, + clippy::implicit_return, + clippy::min_ident_chars, + clippy::missing_trait_methods, + clippy::question_mark_used, + clippy::single_call_fn, + clippy::single_char_lifetime_names, + clippy::unseparated_literal_suffix +)] +extern crate alloc; +/// Functionality related to the passed arguments. +mod args; +/// Functionality related to `Cargo.toml` parsing. +mod manifest; +use alloc::string::FromUtf8Error; +use args::{ArgsErr, Opts}; +use core::fmt::{self, Display, Formatter}; +use std::{error::Error, fs, io}; +use toml::de::Error as TomlErr; +/// Application error. +enum E { + /// Error related to the passed arguments. + Args(ArgsErr), + /// Error related to running `cargo`. + Cmd(String), + /// I/O-related errors. + Io(io::Error), + /// Error related to the format of `Cargo.toml`. + Toml(TomlErr), + /// Error when `stdout` is not valid UTF-8. + Utf8(FromUtf8Error), +} +impl Display for E { + #[allow(clippy::ref_patterns)] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Args(ref err) => err.fmt(f), + Self::Cmd(ref err) => err.fmt(f), + Self::Io(ref err) => err.fmt(f), + Self::Toml(ref err) => err.fmt(f), + Self::Utf8(ref err) => err.fmt(f), + } + } +} +impl fmt::Debug for E { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + <Self as Display>::fmt(self, f) + } +} +impl Error for E {} +fn main() -> Result<(), E> { + Opts::from_args().map_err(E::Args).and_then(|opt| { + fs::read_to_string("Cargo.toml") + .map_err(E::Io) + .and_then(|toml| manifest::from_toml(toml.as_str()).map_err(E::Toml)) + .and_then(|features| match opt { + Opts::None => features.into_iter().try_fold((), |(), feature| { + Opts::Clippy.run_cmd(feature.as_str()).and_then(|()| { + Opts::DocTests + .run_cmd(feature.as_str()) + .and_then(|()| Opts::Tests.run_cmd(feature.as_str())) + }) + }), + Opts::Clippy | Opts::DocTests | Opts::Tests => features + .into_iter() + .try_fold((), |(), feature| opt.run_cmd(feature.as_str())), + }) + }) +} diff --git a/src/manifest.rs b/src/manifest.rs @@ -0,0 +1,278 @@ +use core::fmt::{self, Formatter}; +use serde::de::{Deserialize, Deserializer, Error, IgnoredAny, MapAccess, SeqAccess, Visitor}; +use std::collections::{HashMap, HashSet}; +use toml::de::Error as TomlErr; +/// Features that are enabled by another feature. +struct ImpliedFeatures(HashSet<String>); +impl ImpliedFeatures { + /// Returns `true` iff any feature in the `ImpliedFeatures` graph is `feature`. + fn contains(&self, feature: &str, features: &Features) -> bool { + /// Recursively iterates the features in `left` and checks if one is `feature`. + fn recurse<'a: 'e, 'b, 'c: 'e, 'd, 'e>( + left: &'a ImpliedFeatures, + feature: &'b str, + features: &'c Features, + cycle_detection: &'d mut HashSet<&'e str>, + ) -> Result<(), ()> { + left.0.iter().try_fold((), |(), feat| { + if feat == feature || !cycle_detection.insert(feat) { + Err(()) + } else { + features + .0 + .get(feat) + // This will be `None` iff `feat` is a dependency-related feature. + .map_or(Ok(()), |f| recurse(f, feature, features, cycle_detection)) + } + }) + } + let mut recursion_detection = HashSet::new(); + recurse(self, feature, features, &mut recursion_detection).is_err() + } +} +impl<'de> Deserialize<'de> for ImpliedFeatures { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// `Visitor` for `ImpliedFeatures`. + struct ImpliedFeaturesVisitor; + impl<'d> Visitor<'d> for ImpliedFeaturesVisitor { + type Value = ImpliedFeatures; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str("ImpliedFeatures") + } + fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> + where + A: SeqAccess<'d>, + { + let mut feats = HashSet::with_capacity(seq.size_hint().unwrap_or_default()); + while let Some(val) = seq.next_element::<String>()? { + feats.insert(val); + } + Ok(ImpliedFeatures(feats)) + } + } + deserializer.deserialize_seq(ImpliedFeaturesVisitor) + } +} +/// Features with the features that are enabled. +struct Features(HashMap<String, ImpliedFeatures>); +impl Features { + #[allow( + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::cast_possible_truncation, + clippy::indexing_slicing, + clippy::unreachable + )] + /// Returns all possible combinations of features. + fn powerset(self) -> Vec<String> { + // In the event `self.0.len() as u32` causes truncation, a `panic` will occur later due to lack of memory. + let mut init = Vec::with_capacity(2usize.pow(self.0.len() as u32)); + init.push(Vec::new()); + let mut power = self.0.keys().fold(init, |mut power, feature| { + let features = power.clone().into_iter().map(|mut features| { + features.push(feature.clone()); + features + }); + power.extend(features); + power + }); + power.retain(|features| { + features + .iter() + .enumerate() + .try_fold((), |(), (idx, feature)| { + // `idx < features.len()`, so overflow is not possible and indexing is fine. + features[idx + 1..].iter().try_fold((), |(), feat| { + if feature == feat + || self + .0 + .get(feature) + .unwrap_or_else(|| { + unreachable!("there is a bug in Features::powerset") + }) + .contains(feat, &self) + || self + .0 + .get(feat) + .unwrap_or_else(|| { + unreachable!("there is a bug in Features::powerset") + }) + .contains(feature, &self) + { + Err(()) + } else { + Ok(()) + } + }) + }) + .is_ok() + }); + let len = power.len(); + power + .into_iter() + .fold(Vec::with_capacity(len), |mut feats, features| { + let mut feat_combo = features.into_iter().fold(String::new(), |mut f, feature| { + f.push_str(feature.as_str()); + f.push(','); + f + }); + // Remove the trailing comma. + // Note a trailing comma exist iff `feat_combo` is not empty. When empty, this does nothing. + feat_combo.pop(); + feats.push(feat_combo); + feats + }) + } +} +impl<'de> Deserialize<'de> for Features { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// `Visitor` for `Features`. + struct FeaturesVisitor; + impl<'d> Visitor<'d> for FeaturesVisitor { + type Value = Features; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str("Features") + } + fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> + where + A: MapAccess<'d>, + { + /// Feature names. + enum Field { + /// `default` feature. + Default, + /// Feature with a different name. + Other(String), + } + impl<'d> Deserialize<'d> for Field { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'d>, + { + /// `Visitor` for `Field`. + struct FieldVisitor; + impl<'e> Visitor<'e> for FieldVisitor { + type Value = Field; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str("unique feature names") + } + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + Ok(match v { + "default" => Field::Default, + _ => Field::Other(v.to_owned()), + }) + } + } + deserializer.deserialize_identifier(FieldVisitor) + } + } + let mut dflt = false; + let mut features = HashMap::new(); + while let Some(key) = map.next_key()? { + match key { + Field::Default => { + if dflt { + return Err(Error::duplicate_field("default")); + } + dflt = map.next_value::<ImpliedFeatures>().map(|_| true)?; + } + Field::Other(val) => { + map.next_value().and_then(|implied| { + if features.insert(val, implied).is_none() { + Ok(()) + } else { + Err(Error::custom("duplicate features")) + } + })?; + } + } + } + Ok(Features(features)) + } + } + deserializer.deserialize_map(FeaturesVisitor) + } +} +/// `Cargo.toml`. +struct Manifest(Features); +impl<'de> Deserialize<'de> for Manifest { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// `Visitor` for `Manifest`. + struct ManifestVisitor; + impl<'d> Visitor<'d> for ManifestVisitor { + type Value = Manifest; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str("Manifest") + } + fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> + where + A: MapAccess<'d>, + { + /// `Cargo.toml` field. + enum Field { + /// `features`. + Features, + /// All other fields. + Other, + } + impl<'e> Deserialize<'e> for Field { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'e>, + { + /// `Visitor` for `Field`. + struct FieldVisitor; + impl<'f> Visitor<'f> for FieldVisitor { + type Value = Field; + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + formatter.write_str("'features'") + } + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + Ok(match v { + "features" => Field::Features, + _ => Field::Other, + }) + } + } + deserializer.deserialize_identifier(FieldVisitor) + } + } + let mut features = None; + while let Some(key) = map.next_key()? { + match key { + Field::Features => { + if features.is_some() { + return Err(Error::duplicate_field("features")); + } + features = Some(map.next_value()?); + } + Field::Other => map.next_value::<IgnoredAny>().map(|_| ())?, + } + } + Ok(Manifest( + features.unwrap_or_else(|| Features(HashMap::new())), + )) + } + } + deserializer.deserialize_map(ManifestVisitor) + } +} +/// Returns all possible combinations of features. +pub fn from_toml(val: &str) -> Result<Vec<String>, TomlErr> { + toml::from_str::<Manifest>(val).map(|man| man.0.powerset()) +}