Skip to content

Commit e0fdb1e

Browse files
authored
feat: Add shields.io endpoint for dependency status badges (#261)
1 parent c841745 commit e0fdb1e

File tree

3 files changed

+90
-0
lines changed

3 files changed

+90
-0
lines changed

src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,12 @@ async fn main() {
7575
.service(server::index)
7676
.service(server::crate_redirect)
7777
.service(server::crate_latest_status_svg)
78+
.service(server::crate_latest_status_shield_json)
7879
.service(server::crate_status_svg)
80+
.service(server::crate_status_shield_json)
7981
.service(server::crate_status_html)
8082
.service(server::repo_status_svg)
83+
.service(server::repo_status_shield_json)
8184
.service(server::repo_status_html)
8285
.configure(server::static_files)
8386
.default_service(web::to(server::not_found))

src/server/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ const MAX_SUBJECT_WIDTH: usize = 100;
4545
enum StatusFormat {
4646
Html,
4747
Svg,
48+
/// Renders the analysis status as a JSON object compatible with the shields.io endpoint badge.
49+
/// See: https://shields.io/badges/endpoint-badge
50+
ShieldJson,
4851
}
4952

5053
#[get("/")]
@@ -71,6 +74,15 @@ pub(crate) async fn repo_status_svg(
7174
repo_status(engine, uri, params, StatusFormat::Svg).await
7275
}
7376

77+
#[get("/repo/{site:.+?}/{qual}/{name}/shield.json")]
78+
pub(crate) async fn repo_status_shield_json(
79+
ThinData(engine): ThinData<Engine>,
80+
uri: Uri,
81+
Path(params): Path<(String, String, String)>,
82+
) -> actix_web::Result<impl Responder> {
83+
repo_status(engine, uri, params, StatusFormat::ShieldJson).await
84+
}
85+
7486
#[get("/repo/{site:.+?}/{qual}/{name}")]
7587
pub(crate) async fn repo_status_html(
7688
ThinData(engine): ThinData<Engine>,
@@ -178,6 +190,15 @@ async fn crate_latest_status_svg(
178190
crate_status(engine, uri, (name, None), StatusFormat::Svg).await
179191
}
180192

193+
#[get("/crate/{name}/latest/shield.json")]
194+
async fn crate_latest_status_shield_json(
195+
ThinData(engine): ThinData<Engine>,
196+
uri: Uri,
197+
Path((name,)): Path<(String,)>,
198+
) -> actix_web::Result<impl Responder> {
199+
crate_status(engine, uri, (name, None), StatusFormat::ShieldJson).await
200+
}
201+
181202
#[get("/crate/{name}/{version}/status.svg")]
182203
async fn crate_status_svg(
183204
ThinData(engine): ThinData<Engine>,
@@ -187,6 +208,15 @@ async fn crate_status_svg(
187208
crate_status(engine, uri, (name, Some(version)), StatusFormat::Svg).await
188209
}
189210

211+
#[get("/crate/{name}/{version}/shield.json")]
212+
async fn crate_status_shield_json(
213+
ThinData(engine): ThinData<Engine>,
214+
uri: Uri,
215+
Path((name, version)): Path<(String, String)>,
216+
) -> actix_web::Result<impl Responder> {
217+
crate_status(engine, uri, (name, Some(version)), StatusFormat::ShieldJson).await
218+
}
219+
190220
async fn crate_status(
191221
engine: Engine,
192222
uri: Uri,
@@ -264,6 +294,10 @@ fn status_format_analysis(
264294
subject_path,
265295
badge_knobs,
266296
)),
297+
StatusFormat::ShieldJson => Either::Left(views::badge::shield_json_response(
298+
analysis_outcome.as_ref(),
299+
badge_knobs,
300+
)),
267301
}
268302
}
269303

src/server/views/badge.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use actix_web::{http::header::ContentType, HttpResponse};
22
use badge::{Badge, BadgeOptions};
3+
use serde::Serialize;
34

45
use crate::{engine::AnalyzeDependenciesOutcome, server::ExtraConfig};
56

@@ -65,6 +66,58 @@ pub fn badge(
6566
Badge::new(opts)
6667
}
6768

69+
#[derive(Serialize)]
70+
struct ShieldIoJson {
71+
#[serde(rename = "schemaVersion")]
72+
schema_version: u8,
73+
label: String,
74+
message: String,
75+
color: String,
76+
}
77+
78+
pub fn shield_json_response(
79+
analysis_outcome: Option<&AnalyzeDependenciesOutcome>,
80+
badge_knobs: ExtraConfig,
81+
) -> HttpResponse {
82+
let subject = badge_knobs.subject().to_owned();
83+
84+
let (status, color_hex) = match analysis_outcome {
85+
Some(outcome) => {
86+
if outcome.any_always_insecure() {
87+
("insecure".to_string(), "#e05d44".to_string())
88+
} else {
89+
let (outdated, total) = outcome.outdated_ratio();
90+
if outdated > 0 {
91+
(
92+
format!("{outdated} of {total} outdated"),
93+
"#dfb317".to_string(),
94+
)
95+
} else if total > 0 {
96+
if outcome.any_insecure() {
97+
("maybe insecure".to_string(), "#8b1".to_string())
98+
} else {
99+
("up to date".to_string(), "#4c1".to_string())
100+
}
101+
} else {
102+
("none".to_string(), "#4c1".to_string())
103+
}
104+
}
105+
}
106+
None => ("unknown".to_string(), "#9f9f9f".to_string()),
107+
};
108+
109+
let shield_data = ShieldIoJson {
110+
schema_version: 1,
111+
label: subject,
112+
message: status,
113+
color: color_hex,
114+
};
115+
116+
HttpResponse::Ok()
117+
.content_type(ContentType::json())
118+
.json(shield_data)
119+
}
120+
68121
pub fn response(
69122
analysis_outcome: Option<&AnalyzeDependenciesOutcome>,
70123
badge_knobs: ExtraConfig,

0 commit comments

Comments
 (0)