diff --git a/.sqlx/query-e0a18d6ec1e1a0d4e14a1f2e4e4e9e8985fe619d5a502d8428af509ba1b9d9b0.json b/.sqlx/query-e0a18d6ec1e1a0d4e14a1f2e4e4e9e8985fe619d5a502d8428af509ba1b9d9b0.json new file mode 100644 index 000000000..0ef0fc6d1 --- /dev/null +++ b/.sqlx/query-e0a18d6ec1e1a0d4e14a1f2e4e4e9e8985fe619d5a502d8428af509ba1b9d9b0.json @@ -0,0 +1,52 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n releases.features as \"features?: Vec\",\n releases.dependencies\n FROM releases\n INNER JOIN crates ON crates.id = releases.crate_id\n WHERE crates.name = $1 AND releases.version = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "features?: Vec", + "type_info": { + "Custom": { + "name": "_feature", + "kind": { + "Array": { + "Custom": { + "name": "feature", + "kind": { + "Composite": [ + [ + "name", + "Text" + ], + [ + "subfeatures", + "TextArray" + ] + ] + } + } + } + } + } + } + }, + { + "ordinal": 1, + "name": "dependencies", + "type_info": "Json" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + true, + true + ] + }, + "hash": "e0a18d6ec1e1a0d4e14a1f2e4e4e9e8985fe619d5a502d8428af509ba1b9d9b0" +} diff --git a/src/web/features.rs b/src/web/features.rs index 047efcc9a..4dc3c62f1 100644 --- a/src/web/features.rs +++ b/src/web/features.rs @@ -1,5 +1,5 @@ use crate::{ - db::types::Feature, + db::types::Feature as DbFeature, impl_axum_webpage, web::{ cache::CachePolicy, @@ -12,15 +12,76 @@ use crate::{ use anyhow::anyhow; use axum::response::IntoResponse; use serde::Serialize; -use std::collections::{HashMap, VecDeque}; +use serde_json::Value; +use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; const DEFAULT_NAME: &str = "default"; +#[derive(Debug, Clone, Serialize)] +struct Feature { + name: String, + subfeatures: BTreeMap, +} + +impl From for Feature { + fn from(feature: DbFeature) -> Self { + let subfeatures = feature + .subfeatures + .into_iter() + .map(|name| { + let feature = SubFeature::parse(&name); + (name, feature) + }) + .collect(); + Self { + name: feature.name, + subfeatures, + } + } +} + +/// The sub-feature enabled by a [`Feature`] +#[derive(Debug, Clone, Serialize, PartialEq)] +enum SubFeature { + /// A normal feature, like `"feature-name"`. + Feature(String), + /// A dependency, like `"dep:package-name"`. + Dependency(String), + /// A dependency feature, like `"package-name?/feature-name"`. + DependencyFeature { + dependency: String, + optional: bool, + feature: String, + }, +} + +impl SubFeature { + fn parse(s: &str) -> Self { + if let Some(dep) = s.strip_prefix("dep:") { + return Self::Dependency(dep.into()); + } + let Some((dependency, feature)) = s.split_once('/') else { + return Self::Feature(s.into()); + }; + let (dependency, optional) = match dependency.strip_suffix('?') { + Some(dep) => (dep, true), + None => (dependency, false), + }; + + Self::DependencyFeature { + dependency: dependency.into(), + optional, + feature: feature.into(), + } + } +} + #[derive(Debug, Clone, Serialize)] struct FeaturesPage { metadata: MetaData, - features: Option>, - default_len: usize, + dependencies: HashMap, + sorted_features: Option>, + default_features: HashSet, canonical_url: CanonicalUrl, is_latest_url: bool, use_direct_platform_links: bool, @@ -55,7 +116,9 @@ pub(crate) async fn build_features_handler( let row = sqlx::query!( r#" - SELECT releases.features as "features?: Vec" + SELECT + releases.features as "features?: Vec", + releases.dependencies FROM releases INNER JOIN crates ON crates.id = releases.crate_id WHERE crates.name = $1 AND releases.version = $2"#, @@ -66,19 +129,19 @@ pub(crate) async fn build_features_handler( .await? .ok_or_else(|| anyhow!("missing release"))?; - let mut features = None; - let mut default_len = 0; - - if let Some(raw_features) = row.features { - let result = order_features_and_count_default_len(raw_features); - features = Some(result.0); - default_len = result.1; - } + let dependencies = get_dependency_versions(row.dependencies); + let (sorted_features, default_features) = if let Some(raw_features) = row.features { + let (sorted_features, default_features) = get_sorted_features(raw_features); + (Some(sorted_features), default_features) + } else { + (None, Default::default()) + }; Ok(FeaturesPage { metadata, - features, - default_len, + dependencies, + sorted_features, + default_features, is_latest_url: req_version.is_latest(), canonical_url: CanonicalUrl::from_path(format!("/crate/{}/latest/features", &name)), use_direct_platform_links: true, @@ -86,41 +149,61 @@ pub(crate) async fn build_features_handler( .into_response()) } -fn order_features_and_count_default_len(raw: Vec) -> (Vec, usize) { - let mut feature_map = get_feature_map(raw); - let mut features = get_tree_structure_from_default(&mut feature_map); - let mut remaining = Vec::from_iter(feature_map.into_values()); - remaining.sort_by_key(|feature| feature.subfeatures.len()); - - let default_len = features.len(); +/// Turns the raw JSON `dependencies` into a [`HashMap`] of dependencies and their versions. +fn get_dependency_versions(raw_dependencies: Option) -> HashMap { + let mut map = HashMap::new(); + + if let Some(deps) = raw_dependencies.as_ref().and_then(Value::as_array) { + for value in deps { + let name = value.get(0).and_then(Value::as_str); + let version = value.get(1).and_then(Value::as_str); + if let (Some(name), Some(version)) = (name, version) { + map.insert(name.into(), version.into()); + } + } + } - features.extend(remaining.into_iter().rev()); - (features, default_len) + map } -fn get_tree_structure_from_default(feature_map: &mut HashMap) -> Vec { - let mut features = Vec::new(); - let mut queue: VecDeque = VecDeque::new(); - - queue.push_back(DEFAULT_NAME.into()); - while !queue.is_empty() { - let name = queue.pop_front().unwrap(); - if let Some(feature) = feature_map.remove(&name) { - feature - .subfeatures - .iter() - .for_each(|sub| queue.push_back(sub.clone())); - features.push(feature); +/// Converts raw [`DbFeature`]s into a sorted list of [`Feature`]s and a Set of default features. +/// +/// The sorting order depends on depth-first traversal starting at the `"default"` feature, +/// and falls back to alphabetic sorting for all non-default features. +fn get_sorted_features(raw_features: Vec) -> (Vec, HashSet) { + let mut all_features: HashMap<_, _> = raw_features + .into_iter() + .filter(|feature| !feature.is_private()) + .map(|feature| (feature.name.clone(), Feature::from(feature))) + .collect(); + + let mut default_features = HashSet::new(); + let mut sorted_features = Vec::new(); + + // this does a depth-first traversal starting at the special `"default"` feature + if all_features.contains_key(DEFAULT_NAME) { + let mut queue = VecDeque::new(); + queue.push_back(DEFAULT_NAME.to_owned()); + + while let Some(name) = queue.pop_front() { + if let Some(feature) = all_features.remove(&name) { + feature + .subfeatures + .keys() + .for_each(|sub| queue.push_back(sub.clone())); + + sorted_features.push(feature); + } + default_features.insert(name); } } - features -} -fn get_feature_map(raw: Vec) -> HashMap { - raw.into_iter() - .filter(|feature| !feature.is_private()) - .map(|feature| (feature.name.clone(), feature)) - .collect() + // the rest of the features not reachable from `"default"` are sorted alphabetically + let mut remaining = Vec::from_iter(all_features.into_values()); + remaining.sort_by(|f1, f2| f2.name.cmp(&f1.name)); + sorted_features.extend(remaining); + + (sorted_features, default_features) } #[cfg(test)] @@ -129,117 +212,124 @@ mod tests { use crate::test::{assert_cache_control, assert_redirect_cached, wrapper}; use reqwest::StatusCode; + #[test] + fn test_parsing_raw_features() { + let feature = SubFeature::parse("a-feature"); + assert_eq!(feature, SubFeature::Feature("a-feature".into())); + + let feature = SubFeature::parse("dep:a-dependency"); + assert_eq!(feature, SubFeature::Dependency("a-dependency".into())); + + let feature = SubFeature::parse("a-dependency/sub-feature"); + assert_eq!( + feature, + SubFeature::DependencyFeature { + dependency: "a-dependency".into(), + optional: false, + feature: "sub-feature".into() + } + ); + + let feature = SubFeature::parse("a-dependency?/sub-feature"); + assert_eq!( + feature, + SubFeature::DependencyFeature { + dependency: "a-dependency".into(), + optional: true, + feature: "sub-feature".into() + } + ); + } + #[test] fn test_feature_map_filters_private() { - let private1 = Feature::new("_private1".into(), vec!["feature1".into()]); - let feature2 = Feature::new("feature2".into(), Vec::new()); + let private1 = DbFeature::new("_private1".into(), vec!["feature1".into()]); + let feature2 = DbFeature::new("feature2".into(), Vec::new()); - let raw = vec![private1.clone(), feature2.clone()]; - let feature_map = get_feature_map(raw); + let (sorted_features, _) = get_sorted_features(vec![private1, feature2]); - assert_eq!(feature_map.len(), 1); - assert!(feature_map.contains_key(&feature2.name)); - assert!(!feature_map.contains_key(&private1.name)); + assert_eq!(sorted_features.len(), 1); + assert_eq!(sorted_features[0].name, "feature2"); } #[test] fn test_default_tree_structure_with_nested_default() { - let default = Feature::new(DEFAULT_NAME.into(), vec!["feature1".into()]); - let non_default = Feature::new("non-default".into(), Vec::new()); - let feature1 = Feature::new( + let default = DbFeature::new(DEFAULT_NAME.into(), vec!["feature1".into()]); + let non_default = DbFeature::new("non-default".into(), Vec::new()); + let feature1 = DbFeature::new( "feature1".into(), vec!["feature2".into(), "feature3".into()], ); - let feature2 = Feature::new("feature2".into(), Vec::new()); - let feature3 = Feature::new("feature3".into(), Vec::new()); - - let raw = vec![ - default.clone(), - non_default.clone(), - feature3.clone(), - feature2.clone(), - feature1.clone(), - ]; - let mut feature_map = get_feature_map(raw); - let default_tree = get_tree_structure_from_default(&mut feature_map); - - assert_eq!(feature_map.len(), 1); - assert_eq!(default_tree.len(), 4); - assert!(feature_map.contains_key(&non_default.name)); - assert!(!feature_map.contains_key(&default.name)); - assert_eq!(default_tree[0], default); - assert_eq!(default_tree[1], feature1); - assert_eq!(default_tree[2], feature2); - assert_eq!(default_tree[3], feature3); + let feature2 = DbFeature::new("feature2".into(), Vec::new()); + let feature3 = DbFeature::new("feature3".into(), Vec::new()); + + let (sorted_features, default_features) = + get_sorted_features(vec![default, non_default, feature3, feature2, feature1]); + + assert_eq!(sorted_features.len(), 5); + assert_eq!(sorted_features[0].name, "default"); + assert_eq!(sorted_features[1].name, "feature1"); + assert_eq!(sorted_features[2].name, "feature2"); + assert_eq!(sorted_features[3].name, "feature3"); + assert_eq!(sorted_features[4].name, "non-default"); + + assert!(default_features.contains("feature3")); + assert!(!default_features.contains("non-default")); } #[test] fn test_default_tree_structure_without_default() { - let feature1 = Feature::new( + let feature1 = DbFeature::new( "feature1".into(), vec!["feature2".into(), "feature3".into()], ); - let feature2 = Feature::new("feature2".into(), Vec::new()); - let feature3 = Feature::new("feature3".into(), Vec::new()); - - let raw = vec![feature3.clone(), feature2.clone(), feature1.clone()]; - let mut feature_map = get_feature_map(raw); - let default_tree = get_tree_structure_from_default(&mut feature_map); - - assert_eq!(feature_map.len(), 3); - assert_eq!(default_tree.len(), 0); - assert!(feature_map.contains_key(&feature1.name)); - assert!(feature_map.contains_key(&feature2.name)); - assert!(feature_map.contains_key(&feature3.name)); + let feature2 = DbFeature::new("feature2".into(), Vec::new()); + let feature3 = DbFeature::new("feature3".into(), Vec::new()); + + let (sorted_features, default_features) = + get_sorted_features(vec![feature3, feature2, feature1]); + + assert_eq!(sorted_features.len(), 3); + assert_eq!(sorted_features[0].name, "feature1"); + assert_eq!(sorted_features[1].name, "feature2"); + assert_eq!(sorted_features[2].name, "feature3"); + + assert_eq!(default_features.len(), 0); } #[test] fn test_default_tree_structure_single_default() { - let default = Feature::new(DEFAULT_NAME.into(), Vec::new()); - let non_default = Feature::new("non-default".into(), Vec::new()); - - let raw = vec![default.clone(), non_default.clone()]; - let mut feature_map = get_feature_map(raw); - let default_tree = get_tree_structure_from_default(&mut feature_map); - - assert_eq!(feature_map.len(), 1); - assert_eq!(default_tree.len(), 1); - assert!(feature_map.contains_key(&non_default.name)); - assert!(!feature_map.contains_key(&default.name)); - assert_eq!(default_tree[0], default); + let default = DbFeature::new(DEFAULT_NAME.into(), Vec::new()); + let non_default = DbFeature::new("non-default".into(), Vec::new()); + + let (sorted_features, default_features) = get_sorted_features(vec![default, non_default]); + + assert_eq!(sorted_features.len(), 2); + assert_eq!(sorted_features[0].name, "default"); + assert_eq!(sorted_features[1].name, "non-default"); + + assert_eq!(default_features.len(), 1); + assert!(default_features.contains("default")); } #[test] fn test_order_features_and_get_len_without_default() { - let feature1 = Feature::new( + let feature1 = DbFeature::new( "feature1".into(), vec!["feature10".into(), "feature11".into()], ); - let feature2 = Feature::new("feature2".into(), vec!["feature20".into()]); - let feature3 = Feature::new("feature3".into(), Vec::new()); - - let raw = vec![feature3.clone(), feature2.clone(), feature1.clone()]; - let (features, default_len) = order_features_and_count_default_len(raw); + let feature2 = DbFeature::new("feature2".into(), vec!["feature20".into()]); + let feature3 = DbFeature::new("feature3".into(), Vec::new()); - assert_eq!(features.len(), 3); - assert_eq!(default_len, 0); - assert_eq!(features[0], feature1); - assert_eq!(features[1], feature2); - assert_eq!(features[2], feature3); - } - - #[test] - fn test_order_features_and_get_len_single_default() { - let default = Feature::new(DEFAULT_NAME.into(), Vec::new()); - let non_default = Feature::new("non-default".into(), Vec::new()); + let (sorted_features, default_features) = + get_sorted_features(vec![feature3, feature2, feature1]); - let raw = vec![default.clone(), non_default.clone()]; - let (features, default_len) = order_features_and_count_default_len(raw); + assert_eq!(sorted_features.len(), 3); + assert_eq!(sorted_features[0].name, "feature1"); + assert_eq!(sorted_features[1].name, "feature2"); + assert_eq!(sorted_features[2].name, "feature3"); - assert_eq!(features.len(), 2); - assert_eq!(default_len, 1); - assert_eq!(features[0], default); - assert_eq!(features[1], non_default); + assert_eq!(default_features.len(), 0); } #[test] diff --git a/templates/crate/features.html b/templates/crate/features.html index c0ccce855..761a3014c 100644 --- a/templates/crate/features.html +++ b/templates/crate/features.html @@ -34,15 +34,15 @@
  • Feature flags
  • - {%- if features -%} - {%- for feature in features -%} + {%- if sorted_features -%} + {%- for feature in sorted_features -%}
  • {{ feature.name }}
  • {%- endfor -%} - {%- elif features is iterable -%} + {%- elif sorted_features is iterable -%}
  • This release does not have any feature flags.
  • @@ -68,15 +68,41 @@

    {{ metadata.name }}

    Cargo.toml in case the author documented the features in them.
- {%- if features -%} -

This version has {{ features | length }} feature flags, {{ default_len }} of them enabled by default.

- {%- for feature in features -%} -

{{ feature.name }}

+ {%- if sorted_features -%} +

This version has {{ sorted_features | length }} feature flags, {{ default_features | length }} of them enabled by default.

+ {%- for feature in sorted_features -%} + {%- set is_default = feature.name != "default" and feature.name in default_features -%} +

{{ feature.name }} {%- if is_default -%} (default){%- endif -%}

    {%- if feature.subfeatures -%} - {%- for subfeature in feature.subfeatures -%} + {%- for name, feature in feature.subfeatures -%} + {%- set is_default = name in default_features -%}
  • - {{ subfeature }} + {%- if "Feature" in feature -%} + + {{- feature.Feature -}} + + {%- elif "Dependency" in feature -%} + {%- set dependency = feature.Dependency -%} + {%- set version = dependencies | get(key=dependency, default="latest") -%} + dep: + {{- dependency -}} + + {%- else -%} + {%- set dependency = feature.DependencyFeature.dependency -%} + {%- set version = dependencies | get(key=dependency, default="latest") -%} + {%- set feature = feature.DependencyFeature.feature -%} + + {{- dependency -}} + + {%- if feature.DependencyFeature.optional -%} + ? + {%- endif -%} + / + {{- feature -}} + + {%- endif -%} + {%- if is_default %} (default){%- endif -%}
  • {%- endfor -%} {%- else -%} @@ -84,7 +110,7 @@

    {{ feature.name }}

    {%- endif -%}
{%- endfor -%} - {%- elif features is iterable -%} + {%- elif sorted_features is iterable -%}

This release does not have any feature flags.

{%- else -%}