From 91486828b86d1fd6bc2de9a2353257f0c51bd090 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Sat, 18 Apr 2020 17:20:05 +0200 Subject: [PATCH 1/2] Implement `GET /crates/:crate_id/maintenance.svg` endpoint --- src/controllers/krate.rs | 1 + src/controllers/krate/badges.rs | 71 +++++++++++++++++++++++++++++++++ src/router.rs | 4 ++ src/tests/all.rs | 1 + src/tests/maintenance_badge.rs | 56 ++++++++++++++++++++++++++ src/tests/util.rs | 13 ++++++ 6 files changed, 146 insertions(+) create mode 100644 src/controllers/krate/badges.rs create mode 100644 src/tests/maintenance_badge.rs diff --git a/src/controllers/krate.rs b/src/controllers/krate.rs index 94af283c977..49fb0868922 100644 --- a/src/controllers/krate.rs +++ b/src/controllers/krate.rs @@ -1,3 +1,4 @@ +pub mod badges; pub mod downloads; pub mod follow; pub mod metadata; diff --git a/src/controllers/krate/badges.rs b/src/controllers/krate/badges.rs new file mode 100644 index 00000000000..cf02bf6f536 --- /dev/null +++ b/src/controllers/krate/badges.rs @@ -0,0 +1,71 @@ +//! Endpoints that provide badges based on crate metadata + +use crate::controllers::frontend_prelude::*; + +use crate::models::{Badge, Crate, CrateBadge, MaintenanceStatus}; +use crate::schema::*; + +use conduit::{Body, Response}; + +/// Handles the `GET /crates/:crate_id/maintenance.svg` route. +pub fn maintenance(req: &mut dyn RequestExt) -> EndpointResult { + let name = &req.params()["crate_id"]; + let conn = req.db_read_only()?; + + let krate = Crate::by_name(name).first::(&*conn); + if krate.is_err() { + let response = Response::builder().status(404).body(Body::empty()).unwrap(); + + return Ok(response); + } + + let krate = krate.unwrap(); + + let maintenance_badge = CrateBadge::belonging_to(&krate) + .select((badges::crate_id, badges::all_columns)) + .load::(&*conn)? + .into_iter() + .find(|cb| matches!(cb.badge, Badge::Maintenance { .. })); + + if maintenance_badge.is_none() { + return Ok(req.redirect( + "https://img.shields.io/badge/maintenance-unknown-lightgrey.svg".to_owned(), + )); + } + + let status = match maintenance_badge { + Some(CrateBadge { + badge: Badge::Maintenance { status }, + .. + }) => Some(status), + _ => None, + }; + + let status = status.unwrap(); + + let message = match status { + MaintenanceStatus::ActivelyDeveloped => "actively--developed", + MaintenanceStatus::PassivelyMaintained => "passively--maintained", + MaintenanceStatus::AsIs => "as--is", + MaintenanceStatus::None => "unknown", + MaintenanceStatus::Experimental => "experimental", + MaintenanceStatus::LookingForMaintainer => "looking--for--maintainer", + MaintenanceStatus::Deprecated => "deprecated", + }; + + let color = match status { + MaintenanceStatus::ActivelyDeveloped => "brightgreen", + MaintenanceStatus::PassivelyMaintained => "yellowgreen", + MaintenanceStatus::AsIs => "yellow", + MaintenanceStatus::None => "lightgrey", + MaintenanceStatus::Experimental => "blue", + MaintenanceStatus::LookingForMaintainer => "orange", + MaintenanceStatus::Deprecated => "red", + }; + + let url = format!( + "https://img.shields.io/badge/maintenance-{}-{}.svg", + message, color + ); + Ok(req.redirect(url)) +} diff --git a/src/router.rs b/src/router.rs index 45d5410e433..e9066fc72b3 100644 --- a/src/router.rs +++ b/src/router.rs @@ -66,6 +66,10 @@ pub fn build_router(app: &App) -> R404 { "/crates/:crate_id/reverse_dependencies", C(krate::metadata::reverse_dependencies), ); + api_router.get( + "/crates/:crate_id/maintenance.svg", + C(krate::badges::maintenance), + ); api_router.get("/keywords", C(keyword::index)); api_router.get("/keywords/:keyword_id", C(keyword::show)); api_router.get("/categories", C(category::index)); diff --git a/src/tests/all.rs b/src/tests/all.rs index afc99c7f14a..10e77dd3607 100644 --- a/src/tests/all.rs +++ b/src/tests/all.rs @@ -52,6 +52,7 @@ mod dump_db; mod git; mod keyword; mod krate; +mod maintenance_badge; mod owners; mod read_only_mode; mod record; diff --git a/src/tests/maintenance_badge.rs b/src/tests/maintenance_badge.rs new file mode 100644 index 00000000000..7225ca8d238 --- /dev/null +++ b/src/tests/maintenance_badge.rs @@ -0,0 +1,56 @@ +use std::collections::HashMap; + +use cargo_registry::models::Badge; +use conduit::StatusCode; + +use crate::util::{MockAnonymousUser, RequestHelper}; +use crate::{builders::CrateBuilder, TestApp}; + +fn set_up() -> MockAnonymousUser { + let (app, anon, user) = TestApp::init().with_user(); + let user = user.as_model(); + + app.db(|conn| { + let mut badges = HashMap::new(); + badges.insert("maintenance".to_owned(), { + let mut attributes = HashMap::new(); + attributes.insert("status".to_owned(), "looking-for-maintainer".to_owned()); + attributes + }); + + let krate = CrateBuilder::new("foo", user.id).expect_build(conn); + Badge::update_crate(conn, &krate, Some(&badges)).unwrap(); + + CrateBuilder::new("bar", user.id).expect_build(conn); + }); + + anon +} + +#[test] +fn crate_with_maintenance_badge() { + let anon = set_up(); + + anon.get::<()>("/api/v1/crates/foo/maintenance.svg") + .assert_status(StatusCode::FOUND) + .assert_redirects_to( + "https://img.shields.io/badge/maintenance-looking--for--maintainer-orange.svg", + ); +} + +#[test] +fn crate_without_maintenance_badge() { + let anon = set_up(); + + anon.get::<()>("/api/v1/crates/bar/maintenance.svg") + .assert_status(StatusCode::FOUND) + .assert_redirects_to("https://img.shields.io/badge/maintenance-unknown-lightgrey.svg"); +} + +#[test] +fn unknown_crate() { + let anon = set_up(); + + anon.get::<()>("/api/v1/crates/unknown/maintenance.svg") + .assert_status(StatusCode::NOT_FOUND); +} diff --git a/src/tests/util.rs b/src/tests/util.rs index 59956476a1e..fb890f30c5a 100644 --- a/src/tests/util.rs +++ b/src/tests/util.rs @@ -649,6 +649,19 @@ where .ends_with(target)); self } + + pub fn assert_redirects_to(&self, target: &str) -> &Self { + assert_eq!( + self.response + .headers() + .get(header::LOCATION) + .unwrap() + .to_str() + .unwrap(), + target + ); + self + } } impl Response<()> { From 1fe81c39c4d7d8f1e091a7b640b21329d9661e31 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Sun, 18 Oct 2020 00:10:39 +0200 Subject: [PATCH 2/2] Generate maintenance badges via `badge` crate --- Cargo.lock | 43 ++++++++++++++++++++ Cargo.toml | 1 + src/controllers/krate/badges.rs | 72 ++++++++++++++++++--------------- src/tests/all.rs | 16 +++++--- src/tests/maintenance_badge.rs | 20 +++++---- src/tests/util.rs | 24 +++++------ 6 files changed, 115 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fac4749c61..f3190a61a99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,11 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9fe5e32de01730eb1f6b7f5b51c17e03e2325bf40a74f754f04f130043affff" + [[package]] name = "addr2line" version = "0.13.0" @@ -155,6 +161,17 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "badge" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0228ae65b89e72921e86c19c3574da63bda0628e9d7da5e164f569bbf4e477d" +dependencies = [ + "base64 0.12.3", + "once_cell", + "rusttype", +] + [[package]] name = "base-x" version = "0.2.6" @@ -258,6 +275,7 @@ version = "0.2.2" dependencies = [ "ammonia", "anyhow", + "badge", "base64 0.13.0", "cargo-registry-s3", "chrono", @@ -1740,6 +1758,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "owned_ttf_parser" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" +dependencies = [ + "ttf-parser", +] + [[package]] name = "parking_lot" version = "0.11.0" @@ -2129,6 +2156,16 @@ dependencies = [ "semver 0.9.0", ] +[[package]] +name = "rusttype" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + [[package]] name = "ryu" version = "1.0.5" @@ -2724,6 +2761,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "ttf-parser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" + [[package]] name = "twoway" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 8c7da644aaa..d866788f1f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ rustdoc-args = [ [dependencies] ammonia = "3.0.0" anyhow = "1.0" +badge = "0.3.0" base64 = "0.13" cargo-registry-s3 = { path = "src/s3", version = "0.2.0" } chrono = { version = "0.4.0", features = ["serde"] } diff --git a/src/controllers/krate/badges.rs b/src/controllers/krate/badges.rs index cf02bf6f536..5220451eae1 100644 --- a/src/controllers/krate/badges.rs +++ b/src/controllers/krate/badges.rs @@ -21,51 +21,57 @@ pub fn maintenance(req: &mut dyn RequestExt) -> EndpointResult { let krate = krate.unwrap(); - let maintenance_badge = CrateBadge::belonging_to(&krate) + let maintenance_badge: Option = CrateBadge::belonging_to(&krate) .select((badges::crate_id, badges::all_columns)) .load::(&*conn)? .into_iter() .find(|cb| matches!(cb.badge, Badge::Maintenance { .. })); - if maintenance_badge.is_none() { - return Ok(req.redirect( - "https://img.shields.io/badge/maintenance-unknown-lightgrey.svg".to_owned(), - )); - } + let status = maintenance_badge + .map(|it| match it.badge { + Badge::Maintenance { status } => Some(status), + _ => None, + }) + .flatten(); - let status = match maintenance_badge { - Some(CrateBadge { - badge: Badge::Maintenance { status }, - .. - }) => Some(status), - _ => None, - }; + let badge = generate_badge(status); + + let response = Response::builder() + .status(200) + .body(Body::from_vec(badge.into_bytes())) + .unwrap(); - let status = status.unwrap(); + Ok(response) +} +fn generate_badge(status: Option) -> String { let message = match status { - MaintenanceStatus::ActivelyDeveloped => "actively--developed", - MaintenanceStatus::PassivelyMaintained => "passively--maintained", - MaintenanceStatus::AsIs => "as--is", - MaintenanceStatus::None => "unknown", - MaintenanceStatus::Experimental => "experimental", - MaintenanceStatus::LookingForMaintainer => "looking--for--maintainer", - MaintenanceStatus::Deprecated => "deprecated", + Some(MaintenanceStatus::ActivelyDeveloped) => "actively-developed", + Some(MaintenanceStatus::PassivelyMaintained) => "passively-maintained", + Some(MaintenanceStatus::AsIs) => "as-is", + Some(MaintenanceStatus::None) => "unknown", + Some(MaintenanceStatus::Experimental) => "experimental", + Some(MaintenanceStatus::LookingForMaintainer) => "looking-for-maintainer", + Some(MaintenanceStatus::Deprecated) => "deprecated", + None => "unknown", }; let color = match status { - MaintenanceStatus::ActivelyDeveloped => "brightgreen", - MaintenanceStatus::PassivelyMaintained => "yellowgreen", - MaintenanceStatus::AsIs => "yellow", - MaintenanceStatus::None => "lightgrey", - MaintenanceStatus::Experimental => "blue", - MaintenanceStatus::LookingForMaintainer => "orange", - MaintenanceStatus::Deprecated => "red", + Some(MaintenanceStatus::ActivelyDeveloped) => "brightgreen", + Some(MaintenanceStatus::PassivelyMaintained) => "yellowgreen", + Some(MaintenanceStatus::AsIs) => "yellow", + Some(MaintenanceStatus::None) => "lightgrey", + Some(MaintenanceStatus::Experimental) => "blue", + Some(MaintenanceStatus::LookingForMaintainer) => "orange", + Some(MaintenanceStatus::Deprecated) => "red", + None => "lightgrey", + }; + + let badge_options = badge::BadgeOptions { + subject: "maintenance".to_owned(), + status: message.to_owned(), + color: color.to_string(), }; - let url = format!( - "https://img.shields.io/badge/maintenance-{}-{}.svg", - message, color - ); - Ok(req.redirect(url)) + badge::Badge::new(badge_options).unwrap().to_svg() } diff --git a/src/tests/all.rs b/src/tests/all.rs index 10e77dd3607..e8514e2d365 100644 --- a/src/tests/all.rs +++ b/src/tests/all.rs @@ -198,10 +198,7 @@ fn bad_resp(r: &mut AppResponse) -> Option { Some(bad) } -fn json(r: &mut AppResponse) -> T -where - for<'de> T: serde::Deserialize<'de>, -{ +fn text(r: &mut AppResponse) -> String { use conduit::Body::*; let mut body = Body::empty(); @@ -212,8 +209,15 @@ where File(_) => unimplemented!(), }; - let s = std::str::from_utf8(&body).unwrap(); - match serde_json::from_str(s) { + std::str::from_utf8(&body).unwrap().to_owned() +} + +fn json(r: &mut AppResponse) -> T +where + for<'de> T: serde::Deserialize<'de>, +{ + let s = text(r); + match serde_json::from_str(&s) { Ok(t) => t, Err(e) => panic!("failed to decode: {:?}\n{}", e, s), } diff --git a/src/tests/maintenance_badge.rs b/src/tests/maintenance_badge.rs index 7225ca8d238..e15c8db337d 100644 --- a/src/tests/maintenance_badge.rs +++ b/src/tests/maintenance_badge.rs @@ -31,20 +31,24 @@ fn set_up() -> MockAnonymousUser { fn crate_with_maintenance_badge() { let anon = set_up(); - anon.get::<()>("/api/v1/crates/foo/maintenance.svg") - .assert_status(StatusCode::FOUND) - .assert_redirects_to( - "https://img.shields.io/badge/maintenance-looking--for--maintainer-orange.svg", - ); + let response = anon + .get::("/api/v1/crates/foo/maintenance.svg") + .good_text(); + + assert!(response.contains("looking-for-maintainer")); + assert!(response.contains("fill=\"orange\"")); } #[test] fn crate_without_maintenance_badge() { let anon = set_up(); - anon.get::<()>("/api/v1/crates/bar/maintenance.svg") - .assert_status(StatusCode::FOUND) - .assert_redirects_to("https://img.shields.io/badge/maintenance-unknown-lightgrey.svg"); + let response = anon + .get::("/api/v1/crates/bar/maintenance.svg") + .good_text(); + + assert!(response.contains("unknown")); + assert!(response.contains("fill=\"lightgrey\"")); } #[test] diff --git a/src/tests/util.rs b/src/tests/util.rs index fb890f30c5a..3304a002fbb 100644 --- a/src/tests/util.rs +++ b/src/tests/util.rs @@ -584,7 +584,7 @@ impl Bad { /// A type providing helper methods for working with responses #[must_use] pub struct Response { - response: AppResponse, + pub response: AppResponse, callback_on_good: Option>, } @@ -606,6 +606,15 @@ where } } + /// Assert that the response is good and deserialize the message + #[track_caller] + pub fn good_text(mut self) -> String { + if !self.response.status().is_success() { + panic!("bad response: {:?}", self.response.status()); + } + crate::text(&mut self.response) + } + /// Assert that the response is good and deserialize the message #[track_caller] pub fn good(mut self) -> T { @@ -649,19 +658,6 @@ where .ends_with(target)); self } - - pub fn assert_redirects_to(&self, target: &str) -> &Self { - assert_eq!( - self.response - .headers() - .get(header::LOCATION) - .unwrap() - .to_str() - .unwrap(), - target - ); - self - } } impl Response<()> {