From 3e25c5094e4e7e6893a4fc92fad095116b73d49c Mon Sep 17 00:00:00 2001 From: Arpad Borsos Date: Mon, 24 Jun 2024 12:43:49 +0200 Subject: [PATCH] Add links to "features" page, and mark default features This adds links to all the features, dependencies, and dependency features for easy navigation. It also marks all the transitive features with a `(default)` marker. I changed that so the secondary sort order is alphabetic, as that might make more sense than sorting by number of sub-features. --- ...e9e8985fe619d5a502d8428af509ba1b9d9b0.json | 52 +++ src/web/features.rs | 336 +++++++++++------- templates/crate/features.html | 46 ++- 3 files changed, 301 insertions(+), 133 deletions(-) create mode 100644 .sqlx/query-e0a18d6ec1e1a0d4e14a1f2e4e4e9e8985fe619d5a502d8428af509ba1b9d9b0.json 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 -%}