diff --git a/src/main.rs b/src/main.rs index a31302f..564f608 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,9 +75,12 @@ async fn main() { .service(server::index) .service(server::crate_redirect) .service(server::crate_latest_status_svg) + .service(server::crate_latest_status_shield_json) .service(server::crate_status_svg) + .service(server::crate_status_shield_json) .service(server::crate_status_html) .service(server::repo_status_svg) + .service(server::repo_status_shield_json) .service(server::repo_status_html) .configure(server::static_files) .default_service(web::to(server::not_found)) diff --git a/src/server/mod.rs b/src/server/mod.rs index abbe08f..adc7a63 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -45,6 +45,9 @@ const MAX_SUBJECT_WIDTH: usize = 100; enum StatusFormat { Html, Svg, + /// Renders the analysis status as a JSON object compatible with the shields.io endpoint badge. + /// See: https://shields.io/badges/endpoint-badge + ShieldJson, } #[get("/")] @@ -71,6 +74,15 @@ pub(crate) async fn repo_status_svg( repo_status(engine, uri, params, StatusFormat::Svg).await } +#[get("/repo/{site:.+?}/{qual}/{name}/shield.json")] +pub(crate) async fn repo_status_shield_json( + ThinData(engine): ThinData, + uri: Uri, + Path(params): Path<(String, String, String)>, +) -> actix_web::Result { + repo_status(engine, uri, params, StatusFormat::ShieldJson).await +} + #[get("/repo/{site:.+?}/{qual}/{name}")] pub(crate) async fn repo_status_html( ThinData(engine): ThinData, @@ -178,6 +190,15 @@ async fn crate_latest_status_svg( crate_status(engine, uri, (name, None), StatusFormat::Svg).await } +#[get("/crate/{name}/latest/shield.json")] +async fn crate_latest_status_shield_json( + ThinData(engine): ThinData, + uri: Uri, + Path((name,)): Path<(String,)>, +) -> actix_web::Result { + crate_status(engine, uri, (name, None), StatusFormat::ShieldJson).await +} + #[get("/crate/{name}/{version}/status.svg")] async fn crate_status_svg( ThinData(engine): ThinData, @@ -187,6 +208,15 @@ async fn crate_status_svg( crate_status(engine, uri, (name, Some(version)), StatusFormat::Svg).await } +#[get("/crate/{name}/{version}/shield.json")] +async fn crate_status_shield_json( + ThinData(engine): ThinData, + uri: Uri, + Path((name, version)): Path<(String, String)>, +) -> actix_web::Result { + crate_status(engine, uri, (name, Some(version)), StatusFormat::ShieldJson).await +} + async fn crate_status( engine: Engine, uri: Uri, @@ -264,6 +294,10 @@ fn status_format_analysis( subject_path, badge_knobs, )), + StatusFormat::ShieldJson => Either::Left(views::badge::shield_json_response( + analysis_outcome.as_ref(), + badge_knobs, + )), } } diff --git a/src/server/views/badge.rs b/src/server/views/badge.rs index 9745bbc..4cdfa9c 100644 --- a/src/server/views/badge.rs +++ b/src/server/views/badge.rs @@ -1,5 +1,6 @@ use actix_web::{http::header::ContentType, HttpResponse}; use badge::{Badge, BadgeOptions}; +use serde::Serialize; use crate::{engine::AnalyzeDependenciesOutcome, server::ExtraConfig}; @@ -65,6 +66,58 @@ pub fn badge( Badge::new(opts) } +#[derive(Serialize)] +struct ShieldIoJson { + #[serde(rename = "schemaVersion")] + schema_version: u8, + label: String, + message: String, + color: String, +} + +pub fn shield_json_response( + analysis_outcome: Option<&AnalyzeDependenciesOutcome>, + badge_knobs: ExtraConfig, +) -> HttpResponse { + let subject = badge_knobs.subject().to_owned(); + + let (status, color_hex) = match analysis_outcome { + Some(outcome) => { + if outcome.any_always_insecure() { + ("insecure".to_string(), "#e05d44".to_string()) + } else { + let (outdated, total) = outcome.outdated_ratio(); + if outdated > 0 { + ( + format!("{outdated} of {total} outdated"), + "#dfb317".to_string(), + ) + } else if total > 0 { + if outcome.any_insecure() { + ("maybe insecure".to_string(), "#8b1".to_string()) + } else { + ("up to date".to_string(), "#4c1".to_string()) + } + } else { + ("none".to_string(), "#4c1".to_string()) + } + } + } + None => ("unknown".to_string(), "#9f9f9f".to_string()), + }; + + let shield_data = ShieldIoJson { + schema_version: 1, + label: subject, + message: status, + color: color_hex, + }; + + HttpResponse::Ok() + .content_type(ContentType::json()) + .json(shield_data) +} + pub fn response( analysis_outcome: Option<&AnalyzeDependenciesOutcome>, badge_knobs: ExtraConfig,