diff --git a/src/models.rs b/src/models.rs index f5bfb170457..d18c576ea63 100644 --- a/src/models.rs +++ b/src/models.rs @@ -24,6 +24,7 @@ mod default_versions; pub mod dependency; mod download; mod email; +pub mod feature; mod follow; mod keyword; pub mod krate; diff --git a/src/models/feature.rs b/src/models/feature.rs new file mode 100644 index 00000000000..843e0f6e23c --- /dev/null +++ b/src/models/feature.rs @@ -0,0 +1,154 @@ +use std::collections::BTreeMap; + +pub type FeaturesMap = BTreeMap>; + +/// Splits the given [`FeaturesMap`] into two [`FeaturesMap`]s based on their +/// values. +/// +/// See . +pub fn split_features(features: FeaturesMap) -> (FeaturesMap, FeaturesMap) { + const ITERATION_LIMIT: usize = 100; + + // First, we partition the features into two groups: those that use the new + // features syntax (`features2`) and those that don't (`features`). + let (mut features, mut features2) = + features + .into_iter() + .partition::(|(_k, vals)| { + !vals + .iter() + .any(|v| v.starts_with("dep:") || v.contains("?/")) + }); + + // Then, we recursively move features from `features` to `features2` if they + // depend on features in `features2`. + for _ in 0..ITERATION_LIMIT { + let split = features + .into_iter() + .partition::(|(_k, vals)| { + !vals.iter().any(|v| features2.contains_key(v)) + }); + + features = split.0; + + if !split.1.is_empty() { + features2.extend(split.1); + } else { + break; + } + } + + (features, features2) +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::{assert_compact_debug_snapshot, assert_debug_snapshot}; + + #[test] + fn test_split_features_no_deps() { + let mut features = FeaturesMap::new(); + features.insert( + "feature1".to_string(), + vec!["val1".to_string(), "val2".to_string()], + ); + features.insert("feature2".to_string(), vec!["val3".to_string()]); + + let (features, features2) = split_features(features); + + assert_compact_debug_snapshot!(features, @r#"{"feature1": ["val1", "val2"], "feature2": ["val3"]}"#); + assert_compact_debug_snapshot!(features2, @"{}"); + } + + #[test] + fn test_split_features_with_deps() { + let mut features = FeaturesMap::new(); + features.insert( + "feature1".to_string(), + vec!["dep:val1".to_string(), "val2".to_string()], + ); + features.insert( + "feature2".to_string(), + vec!["val3".to_string(), "val4?/val5".to_string()], + ); + + let (features, features2) = split_features(features); + + assert_compact_debug_snapshot!(features, @"{}"); + assert_compact_debug_snapshot!(features2, @r#"{"feature1": ["dep:val1", "val2"], "feature2": ["val3", "val4?/val5"]}"#); + } + + #[test] + fn test_split_features_mixed() { + let mut features = FeaturesMap::new(); + features.insert( + "feature1".to_string(), + vec!["val1".to_string(), "val2".to_string()], + ); + features.insert("feature2".to_string(), vec!["dep:val3".to_string()]); + features.insert( + "feature3".to_string(), + vec!["val4".to_string(), "val5?/val6".to_string()], + ); + + let (features, features2) = split_features(features); + + assert_compact_debug_snapshot!(features, @r#"{"feature1": ["val1", "val2"]}"#); + assert_compact_debug_snapshot!(features2, @r#"{"feature2": ["dep:val3"], "feature3": ["val4", "val5?/val6"]}"#); + } + + #[test] + fn test_split_features_nested() { + let mut features = FeaturesMap::new(); + features.insert("feature1".to_string(), vec!["feature2".to_string()]); + features.insert("feature2".to_string(), vec![]); + features.insert("feature3".to_string(), vec!["feature1".to_string()]); + + let (features, features2) = split_features(features); + + assert_compact_debug_snapshot!(features, @r#"{"feature1": ["feature2"], "feature2": [], "feature3": ["feature1"]}"#); + assert_compact_debug_snapshot!(features2, @"{}"); + } + + #[test] + fn test_split_features_nested_mixed() { + let mut features = FeaturesMap::new(); + features.insert("feature1".to_string(), vec!["feature2".to_string()]); + features.insert("feature2".to_string(), vec!["feature3".to_string()]); + features.insert("feature3".to_string(), vec!["dep:foo".to_string()]); + + let (features, features2) = split_features(features); + + assert_compact_debug_snapshot!(features, @"{}"); + assert_compact_debug_snapshot!(features2, @r#"{"feature1": ["feature2"], "feature2": ["feature3"], "feature3": ["dep:foo"]}"#); + } + + #[test] + fn test_split_features_clap() { + let json = json!({ + "env": ["clap_builder/env"], + "std": ["clap_builder/std"], + "help": ["clap_builder/help"], + "cargo": ["clap_builder/cargo"], + "color": ["clap_builder/color"], + "debug": ["clap_builder/debug", "clap_derive?/debug"], + "usage": ["clap_builder/usage"], + "derive": ["dep:clap_derive"], + "string": ["clap_builder/string"], + "default": ["std", "color", "help", "usage", "error-context", "suggestions"], + "unicode": ["clap_builder/unicode"], + "wrap_help": ["clap_builder/wrap_help"], + "deprecated": ["clap_builder/deprecated", "clap_derive?/deprecated"], + "suggestions": ["clap_builder/suggestions"], + "unstable-v5": ["clap_builder/unstable-v5", "clap_derive?/unstable-v5", "deprecated"], + "unstable-doc": ["clap_builder/unstable-doc", "derive"], + "unstable-ext": ["clap_builder/unstable-ext"], + "error-context": ["clap_builder/error-context"], + "unstable-styles": ["clap_builder/unstable-styles"] + }); + + let features = serde_json::from_value::(json).unwrap(); + assert_debug_snapshot!(split_features(features)); + } +} diff --git a/src/models/krate.rs b/src/models/krate.rs index 88cec82b570..2615ff3d2bc 100644 --- a/src/models/krate.rs +++ b/src/models/krate.rs @@ -1,5 +1,3 @@ -use std::collections::BTreeMap; - use chrono::NaiveDateTime; use diesel::associations::Identifiable; use diesel::dsl; @@ -10,6 +8,7 @@ use secrecy::SecretString; use thiserror::Error; use crate::controllers::helpers::pagination::*; +use crate::models::feature::split_features; use crate::models::helpers::with_count::*; use crate::models::version::TopVersions; use crate::models::{ @@ -484,12 +483,7 @@ impl Crate { deps.sort(); let features = version.features().unwrap_or_default(); - let (features, features2): (BTreeMap<_, _>, BTreeMap<_, _>) = - features.into_iter().partition(|(_k, vals)| { - !vals - .iter() - .any(|v| v.starts_with("dep:") || v.contains("?/")) - }); + let (features, features2) = split_features(features); let (features2, v) = if features2.is_empty() { (None, None) diff --git a/src/models/snapshots/crates_io__models__feature__tests__split_features_clap.snap b/src/models/snapshots/crates_io__models__feature__tests__split_features_clap.snap new file mode 100644 index 00000000000..ba6be73039e --- /dev/null +++ b/src/models/snapshots/crates_io__models__feature__tests__split_features_clap.snap @@ -0,0 +1,77 @@ +--- +source: src/models/feature.rs +expression: split_features(features) +--- +( + { + "cargo": [ + "clap_builder/cargo", + ], + "color": [ + "clap_builder/color", + ], + "default": [ + "std", + "color", + "help", + "usage", + "error-context", + "suggestions", + ], + "env": [ + "clap_builder/env", + ], + "error-context": [ + "clap_builder/error-context", + ], + "help": [ + "clap_builder/help", + ], + "std": [ + "clap_builder/std", + ], + "string": [ + "clap_builder/string", + ], + "suggestions": [ + "clap_builder/suggestions", + ], + "unicode": [ + "clap_builder/unicode", + ], + "unstable-ext": [ + "clap_builder/unstable-ext", + ], + "unstable-styles": [ + "clap_builder/unstable-styles", + ], + "usage": [ + "clap_builder/usage", + ], + "wrap_help": [ + "clap_builder/wrap_help", + ], + }, + { + "debug": [ + "clap_builder/debug", + "clap_derive?/debug", + ], + "deprecated": [ + "clap_builder/deprecated", + "clap_derive?/deprecated", + ], + "derive": [ + "dep:clap_derive", + ], + "unstable-doc": [ + "clap_builder/unstable-doc", + "derive", + ], + "unstable-v5": [ + "clap_builder/unstable-v5", + "clap_derive?/unstable-v5", + "deprecated", + ], + }, +) diff --git a/src/models/version.rs b/src/models/version.rs index 163c54b4e0c..6db3d787334 100644 --- a/src/models/version.rs +++ b/src/models/version.rs @@ -7,6 +7,7 @@ use serde::Deserialize; use crate::util::errors::{bad_request, AppResult}; +use crate::models::feature::FeaturesMap; use crate::models::{Crate, Dependency, User}; use crate::schema::*; use crate::sql::split_part; @@ -71,7 +72,7 @@ impl Version { /// /// * `Ok(BTreeMap>)` - If the deserialization was successful. /// * `Err(serde_json::Error)` - If the deserialization failed. - pub fn features(&self) -> Result>, serde_json::Error> { + pub fn features(&self) -> Result { BTreeMap::>::deserialize(&self.features) } }