Skip to content

Commit 9148682

Browse files
committed
Implement GET /crates/:crate_id/maintenance.svg endpoint
1 parent 5bb65cc commit 9148682

File tree

6 files changed

+146
-0
lines changed

6 files changed

+146
-0
lines changed

src/controllers/krate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod badges;
12
pub mod downloads;
23
pub mod follow;
34
pub mod metadata;

src/controllers/krate/badges.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//! Endpoints that provide badges based on crate metadata
2+
3+
use crate::controllers::frontend_prelude::*;
4+
5+
use crate::models::{Badge, Crate, CrateBadge, MaintenanceStatus};
6+
use crate::schema::*;
7+
8+
use conduit::{Body, Response};
9+
10+
/// Handles the `GET /crates/:crate_id/maintenance.svg` route.
11+
pub fn maintenance(req: &mut dyn RequestExt) -> EndpointResult {
12+
let name = &req.params()["crate_id"];
13+
let conn = req.db_read_only()?;
14+
15+
let krate = Crate::by_name(name).first::<Crate>(&*conn);
16+
if krate.is_err() {
17+
let response = Response::builder().status(404).body(Body::empty()).unwrap();
18+
19+
return Ok(response);
20+
}
21+
22+
let krate = krate.unwrap();
23+
24+
let maintenance_badge = CrateBadge::belonging_to(&krate)
25+
.select((badges::crate_id, badges::all_columns))
26+
.load::<CrateBadge>(&*conn)?
27+
.into_iter()
28+
.find(|cb| matches!(cb.badge, Badge::Maintenance { .. }));
29+
30+
if maintenance_badge.is_none() {
31+
return Ok(req.redirect(
32+
"https://img.shields.io/badge/maintenance-unknown-lightgrey.svg".to_owned(),
33+
));
34+
}
35+
36+
let status = match maintenance_badge {
37+
Some(CrateBadge {
38+
badge: Badge::Maintenance { status },
39+
..
40+
}) => Some(status),
41+
_ => None,
42+
};
43+
44+
let status = status.unwrap();
45+
46+
let message = match status {
47+
MaintenanceStatus::ActivelyDeveloped => "actively--developed",
48+
MaintenanceStatus::PassivelyMaintained => "passively--maintained",
49+
MaintenanceStatus::AsIs => "as--is",
50+
MaintenanceStatus::None => "unknown",
51+
MaintenanceStatus::Experimental => "experimental",
52+
MaintenanceStatus::LookingForMaintainer => "looking--for--maintainer",
53+
MaintenanceStatus::Deprecated => "deprecated",
54+
};
55+
56+
let color = match status {
57+
MaintenanceStatus::ActivelyDeveloped => "brightgreen",
58+
MaintenanceStatus::PassivelyMaintained => "yellowgreen",
59+
MaintenanceStatus::AsIs => "yellow",
60+
MaintenanceStatus::None => "lightgrey",
61+
MaintenanceStatus::Experimental => "blue",
62+
MaintenanceStatus::LookingForMaintainer => "orange",
63+
MaintenanceStatus::Deprecated => "red",
64+
};
65+
66+
let url = format!(
67+
"https://img.shields.io/badge/maintenance-{}-{}.svg",
68+
message, color
69+
);
70+
Ok(req.redirect(url))
71+
}

src/router.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ pub fn build_router(app: &App) -> R404 {
6666
"/crates/:crate_id/reverse_dependencies",
6767
C(krate::metadata::reverse_dependencies),
6868
);
69+
api_router.get(
70+
"/crates/:crate_id/maintenance.svg",
71+
C(krate::badges::maintenance),
72+
);
6973
api_router.get("/keywords", C(keyword::index));
7074
api_router.get("/keywords/:keyword_id", C(keyword::show));
7175
api_router.get("/categories", C(category::index));

src/tests/all.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ mod dump_db;
5252
mod git;
5353
mod keyword;
5454
mod krate;
55+
mod maintenance_badge;
5556
mod owners;
5657
mod read_only_mode;
5758
mod record;

src/tests/maintenance_badge.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use std::collections::HashMap;
2+
3+
use cargo_registry::models::Badge;
4+
use conduit::StatusCode;
5+
6+
use crate::util::{MockAnonymousUser, RequestHelper};
7+
use crate::{builders::CrateBuilder, TestApp};
8+
9+
fn set_up() -> MockAnonymousUser {
10+
let (app, anon, user) = TestApp::init().with_user();
11+
let user = user.as_model();
12+
13+
app.db(|conn| {
14+
let mut badges = HashMap::new();
15+
badges.insert("maintenance".to_owned(), {
16+
let mut attributes = HashMap::new();
17+
attributes.insert("status".to_owned(), "looking-for-maintainer".to_owned());
18+
attributes
19+
});
20+
21+
let krate = CrateBuilder::new("foo", user.id).expect_build(conn);
22+
Badge::update_crate(conn, &krate, Some(&badges)).unwrap();
23+
24+
CrateBuilder::new("bar", user.id).expect_build(conn);
25+
});
26+
27+
anon
28+
}
29+
30+
#[test]
31+
fn crate_with_maintenance_badge() {
32+
let anon = set_up();
33+
34+
anon.get::<()>("/api/v1/crates/foo/maintenance.svg")
35+
.assert_status(StatusCode::FOUND)
36+
.assert_redirects_to(
37+
"https://img.shields.io/badge/maintenance-looking--for--maintainer-orange.svg",
38+
);
39+
}
40+
41+
#[test]
42+
fn crate_without_maintenance_badge() {
43+
let anon = set_up();
44+
45+
anon.get::<()>("/api/v1/crates/bar/maintenance.svg")
46+
.assert_status(StatusCode::FOUND)
47+
.assert_redirects_to("https://img.shields.io/badge/maintenance-unknown-lightgrey.svg");
48+
}
49+
50+
#[test]
51+
fn unknown_crate() {
52+
let anon = set_up();
53+
54+
anon.get::<()>("/api/v1/crates/unknown/maintenance.svg")
55+
.assert_status(StatusCode::NOT_FOUND);
56+
}

src/tests/util.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,19 @@ where
649649
.ends_with(target));
650650
self
651651
}
652+
653+
pub fn assert_redirects_to(&self, target: &str) -> &Self {
654+
assert_eq!(
655+
self.response
656+
.headers()
657+
.get(header::LOCATION)
658+
.unwrap()
659+
.to_str()
660+
.unwrap(),
661+
target
662+
);
663+
self
664+
}
652665
}
653666

654667
impl Response<()> {

0 commit comments

Comments
 (0)