Skip to content

Commit cc4883b

Browse files
committed
Add menu item called Feature flags
Feature flags should show all available features for current crate version with default being first is specified.
1 parent 89336f1 commit cc4883b

File tree

10 files changed

+191
-10
lines changed

10 files changed

+191
-10
lines changed

Cargo.lock

+19-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ lol_html = "0.2"
5353
font-awesome-as-a-crate = { path = "crates/font-awesome-as-a-crate" }
5454
dashmap = "3.11.10"
5555
string_cache = "0.8.0"
56+
postgres-types = { version = "0.1.3", features = ["derive"] }
5657

5758
# Async
5859
tokio = { version = "0.2.22", features = ["rt-threaded"] }

src/db/add_package.rs

+36-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::{
55
};
66

77
use crate::{
8+
db::types::Feature,
89
docbuilder::{BuildResult, DocCoverage},
910
error::Result,
1011
index::api::{CrateData, CrateOwner, ReleaseData},
@@ -42,6 +43,7 @@ pub(crate) fn add_package_into_database(
4243
let dependencies = convert_dependencies(metadata_pkg);
4344
let rustdoc = get_rustdoc(metadata_pkg, source_dir).unwrap_or(None);
4445
let readme = get_readme(metadata_pkg, source_dir).unwrap_or(None);
46+
let features = get_features(metadata_pkg);
4547
let is_library = metadata_pkg.is_library();
4648

4749
let rows = conn.query(
@@ -52,12 +54,12 @@ pub(crate) fn add_package_into_database(
5254
homepage_url, description, description_long, readme,
5355
authors, keywords, have_examples, downloads, files,
5456
doc_targets, is_library, doc_rustc_version,
55-
documentation_url, default_target
57+
documentation_url, default_target, features
5658
)
5759
VALUES (
5860
$1, $2, $3, $4, $5, $6, $7, $8, $9,
5961
$10, $11, $12, $13, $14, $15, $16, $17, $18,
60-
$19, $20, $21, $22, $23, $24, $25
62+
$19, $20, $21, $22, $23, $24, $25, $26
6163
)
6264
ON CONFLICT (crate_id, version) DO UPDATE
6365
SET release_time = $3,
@@ -82,7 +84,8 @@ pub(crate) fn add_package_into_database(
8284
is_library = $22,
8385
doc_rustc_version = $23,
8486
documentation_url = $24,
85-
default_target = $25
87+
default_target = $25,
88+
features = $26
8689
RETURNING id",
8790
&[
8891
&crate_id,
@@ -110,6 +113,7 @@ pub(crate) fn add_package_into_database(
110113
&res.rustc_version,
111114
&metadata_pkg.documentation,
112115
&default_target,
116+
&features,
113117
],
114118
)?;
115119

@@ -213,6 +217,35 @@ fn convert_dependencies(pkg: &MetadataPackage) -> Vec<(String, String, String)>
213217
.collect()
214218
}
215219

220+
/// Reads features and converts them to Vec<Feature> with default being first
221+
fn get_features(pkg: &MetadataPackage) -> Vec<Feature> {
222+
let mut features = Vec::with_capacity(pkg.features.len());
223+
if let Some(subfeatures) = pkg.features.get("default") {
224+
features.push(Feature::new("default".into(), subfeatures.clone()));
225+
};
226+
features.extend(
227+
pkg.features
228+
.iter()
229+
.filter(|(name, _)| !name.eq(&"default"))
230+
.map(|(name, subfeatures)| Feature::new(name.clone(), subfeatures.clone())),
231+
);
232+
features.extend(get_optional_features(pkg));
233+
features
234+
}
235+
236+
fn get_optional_features(pkg: &MetadataPackage) -> Vec<Feature> {
237+
pkg.dependencies
238+
.iter()
239+
.filter(|dep| dep.optional)
240+
.filter(|dep| {
241+
pkg.features
242+
.values()
243+
.any(|features| features.iter().any(|sub| sub.eq(&dep.name)))
244+
})
245+
.map(|dep| Feature::new(dep.name.clone(), Vec::new()))
246+
.collect()
247+
}
248+
216249
/// Reads readme if there is any read defined in Cargo.toml of a Package
217250
fn get_readme(pkg: &MetadataPackage, source_dir: &Path) -> Result<Option<String>> {
218251
let readme_path = source_dir.join(pkg.readme.as_deref().unwrap_or("README.md"));

src/db/migrate.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,24 @@ pub fn migrate(version: Option<Version>, conn: &mut Client) -> CratesfyiResult<(
457457
DROP COLUMN total_items_needing_examples,
458458
DROP COLUMN items_with_examples;
459459
"
460-
)
460+
),
461+
migration!(
462+
context,
463+
// version
464+
19,
465+
// description
466+
"Add features that are available for given release",
467+
// upgrade query
468+
"
469+
CREATE TYPE feature AS (name TEXT, subfeatures TEXT[]);
470+
ALTER TABLE releases ADD COLUMN features feature[];
471+
",
472+
// downgrade query
473+
"
474+
ALTER TABLE releases DROP COLUMN features;
475+
DROP TYPE feature;
476+
"
477+
),
461478
];
462479

463480
for migration in migrations {

src/db/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ mod delete;
1515
pub(crate) mod file;
1616
mod migrate;
1717
mod pool;
18+
pub(crate) mod types;

src/db/types.rs

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use failure::_core::fmt::Formatter;
2+
use postgres_types::{FromSql, ToSql};
3+
use serde::Serialize;
4+
5+
#[derive(Debug, Clone, PartialEq, Serialize, FromSql, ToSql)]
6+
#[postgres(name = "feature")]
7+
pub struct Feature {
8+
name: String,
9+
subfeatures: Vec<String>,
10+
}
11+
12+
impl Feature {
13+
pub fn new(name: String, subfeatures: Vec<String>) -> Self {
14+
Feature { name, subfeatures }
15+
}
16+
17+
pub fn is_private(&self) -> bool {
18+
self.name.starts_with('_')
19+
}
20+
}
21+
22+
impl std::fmt::Display for Feature {
23+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
24+
write!(f, "{} = {:?}", self.name, self.subfeatures)
25+
}
26+
}

src/test/fakes.rs

+16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::storage::Storage;
55
use crate::utils::{Dependency, MetadataPackage, Target};
66
use chrono::{DateTime, Utc};
77
use failure::Error;
8+
use std::collections::HashMap;
89
use std::sync::Arc;
910

1011
#[must_use = "FakeRelease does nothing until you call .create()"]
@@ -48,11 +49,21 @@ impl<'a> FakeRelease<'a> {
4849
name: "fake-dependency".into(),
4950
req: "^1.0.0".into(),
5051
kind: None,
52+
optional: false,
5153
}],
5254
targets: vec![Target::dummy_lib("fake_package".into(), None)],
5355
readme: None,
5456
keywords: vec!["fake".into(), "package".into()],
5557
authors: vec!["Fake Person <[email protected]>".into()],
58+
features: [
59+
("default".into(), vec!["feature1".into(), "feature3".into()]),
60+
("feature1".into(), Vec::new()),
61+
("feature2".into(), vec!["feature1".into()]),
62+
("feature3".into(), Vec::new()),
63+
]
64+
.iter()
65+
.cloned()
66+
.collect::<HashMap<String, Vec<String>>>(),
5667
},
5768
build_result: BuildResult {
5869
rustc_version: "rustc 2.0.0-nightly (000000000 1970-01-01)".into(),
@@ -199,6 +210,11 @@ impl<'a> FakeRelease<'a> {
199210
self
200211
}
201212

213+
pub(crate) fn features(mut self, features: HashMap<String, Vec<String>>) -> Self {
214+
self.package.features = features;
215+
self
216+
}
217+
202218
/// Returns the release_id
203219
pub(crate) fn create(self) -> Result<i32, Error> {
204220
use std::fs;

src/utils/cargo_metadata.rs

+2
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pub(crate) struct Package {
7777
pub(crate) readme: Option<String>,
7878
pub(crate) keywords: Vec<String>,
7979
pub(crate) authors: Vec<String>,
80+
pub(crate) features: HashMap<String, Vec<String>>,
8081
}
8182

8283
impl Package {
@@ -131,6 +132,7 @@ pub(crate) struct Dependency {
131132
pub(crate) name: String,
132133
pub(crate) req: String,
133134
pub(crate) kind: Option<String>,
135+
pub(crate) optional: bool,
134136
}
135137

136138
#[derive(Deserialize, Serialize)]

src/web/crate_details.rs

+52-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{match_version, redirect_base, render_markdown, MatchSemver, MetaData};
2-
use crate::{db::Pool, impl_webpage, web::page::WebPage};
2+
use crate::{db::types::Feature, db::Pool, impl_webpage, web::page::WebPage};
33
use chrono::{DateTime, NaiveDateTime, Utc};
44
use iron::prelude::*;
55
use iron::Url;
@@ -45,6 +45,7 @@ pub struct CrateDetails {
4545
documented_items: Option<f32>,
4646
total_items_needing_examples: Option<f32>,
4747
items_with_examples: Option<f32>,
48+
features: Option<Vec<String>>,
4849
/// Database id for this crate
4950
pub(crate) crate_id: i32,
5051
/// Database id for this release
@@ -103,6 +104,7 @@ impl CrateDetails {
103104
releases.license,
104105
releases.documentation_url,
105106
releases.default_target,
107+
releases.features,
106108
doc_coverage.total_items,
107109
doc_coverage.documented_items,
108110
doc_coverage.total_items_needing_examples,
@@ -187,6 +189,7 @@ impl CrateDetails {
187189
total_items: total_items.map(|v| v as f32),
188190
total_items_needing_examples: total_items_needing_examples.map(|v| v as f32),
189191
items_with_examples: items_with_examples.map(|v| v as f32),
192+
features: get_features(krate.get("features")),
190193
crate_id,
191194
release_id,
192195
};
@@ -278,6 +281,15 @@ fn map_to_release(conn: &mut Client, crate_id: i32, version: semver::Version) ->
278281
}
279282
}
280283

284+
fn get_features(features: Option<Vec<Feature>>) -> Option<Vec<String>> {
285+
features.map(|vec| {
286+
vec.iter()
287+
.filter(|feature| !feature.is_private())
288+
.map(|feature| format!("{}", feature))
289+
.collect()
290+
})
291+
}
292+
281293
#[derive(Debug, Clone, PartialEq, Serialize)]
282294
struct CrateDetailsPage {
283295
details: CrateDetails,
@@ -325,6 +337,7 @@ mod tests {
325337
use crate::test::{wrapper, TestDatabase};
326338
use failure::Error;
327339
use kuchiki::traits::TendrilSink;
340+
use std::collections::HashMap;
328341

329342
fn assert_last_successful_build_equals(
330343
db: &TestDatabase,
@@ -741,4 +754,42 @@ mod tests {
741754
Ok(())
742755
});
743756
}
757+
758+
#[test]
759+
fn feature_flags_is_hidden_when_empty() {
760+
wrapper(|env| {
761+
env.fake_release()
762+
.name("binary")
763+
.version("0.1.0")
764+
.binary(true)
765+
.features(HashMap::new())
766+
.create()?;
767+
768+
let page = kuchiki::parse_html()
769+
.one(env.frontend().get("/crate/binary/0.1.0").send()?.text()?);
770+
assert!(page.select_first(r#"a[aria-label="Feature"]"#).is_err());
771+
Ok(())
772+
});
773+
}
774+
775+
#[test]
776+
fn feature_private_feature_flags_are_hidden() {
777+
wrapper(|env| {
778+
let features = [("_private".into(), Vec::new())]
779+
.iter()
780+
.cloned()
781+
.collect::<HashMap<String, Vec<String>>>();
782+
env.fake_release()
783+
.name("binary")
784+
.version("0.1.0")
785+
.binary(true)
786+
.features(features)
787+
.create()?;
788+
789+
let page = kuchiki::parse_html()
790+
.one(env.frontend().get("/crate/binary/0.1.0").send()?.text()?);
791+
assert!(page.select_first(r#"a[aria-label="Feature"]"#).is_err());
792+
Ok(())
793+
});
794+
}
744795
}

templates/rustdoc/topbar.html

+20-1
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,26 @@
226226
</li>
227227
{%- endfor -%}
228228
</ul>
229-
</li>
229+
</li>{#
230+
Display the features available in current build
231+
#}
232+
{% if krate.features %}
233+
<li class="pure-menu-item pure-menu-has-children">
234+
<a href="#" class="pure-menu-link" aria-label="Features">
235+
{{ "flag" | fas }}
236+
<span class="title">Feature flags</span>
237+
</a>
238+
239+
{# Build the dropdown list showing available features #}
240+
<ul class="pure-menu-children">
241+
{%- for feature in krate.features -%}
242+
<li class="pure-menu-item">
243+
<span class="pure-menu-link">{{ feature }}</span>
244+
</li>
245+
{%- endfor -%}
246+
</ul>
247+
</li>
248+
{%- endif -%}
230249
</ul>
231250

232251
{%- include "header/topbar_end.html" -%}

0 commit comments

Comments
 (0)