Skip to content

Add JSON output for badges #1256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/web/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ pub(super) fn build_routes() -> Routes {

routes.rustdoc_page("/:crate", super::rustdoc::rustdoc_redirector_handler);
routes.rustdoc_page("/:crate/", super::rustdoc::rustdoc_redirector_handler);
routes.rustdoc_page("/:crate/badge.svg", super::rustdoc::badge_handler);
routes.rustdoc_page("/:crate/badge.svg", super::rustdoc::badge_handler_svg);
routes.rustdoc_page("/:crate/badge.json", super::rustdoc::badge_handler_json);
routes.rustdoc_page(
"/:crate/:version",
super::rustdoc::rustdoc_redirector_handler,
Expand Down
142 changes: 90 additions & 52 deletions src/web/rustdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
};
use iron::url::percent_encoding::percent_decode;
use iron::{
headers::{CacheControl, CacheDirective, Expires, HttpDate},
headers::{CacheControl, CacheDirective, ContentType, Expires, HttpDate},
modifiers::Redirect,
status, Handler, IronResult, Request, Response, Url,
};
Expand Down Expand Up @@ -206,7 +206,7 @@ impl RustdocPage {
req: &mut Request,
file_path: &str,
) -> IronResult<Response> {
use iron::{headers::ContentType, status::Status};
use iron::status::Status;

let templates = req
.extensions
Expand Down Expand Up @@ -557,9 +557,16 @@ pub fn target_redirect_handler(req: &mut Request) -> IronResult<Response> {
Ok(resp)
}

pub fn badge_handler(req: &mut Request) -> IronResult<Response> {
use badge::{Badge, BadgeOptions};
use iron::headers::ContentType;
fn badge_handler_common<F, E>(
req: &mut Request,
ext: &str,
content_type: ContentType,
builder: F,
) -> IronResult<Response>
where
F: Fn(String, String, String) -> Result<String, E>,
E: std::fmt::Display,
{
let version = {
let mut params = req.url.as_ref().query_pairs();
match params.find(|(key, _)| key == "version") {
Expand All @@ -571,59 +578,51 @@ pub fn badge_handler(req: &mut Request) -> IronResult<Response> {
let name = cexpect!(req, extension!(req, Router).find("crate"));
let mut conn = extension!(req, Pool).get()?;

let options =
match match_version(&mut conn, &name, Some(&version)).and_then(|m| m.assume_exact()) {
Ok(MatchSemver::Exact((version, id))) => {
let rows = ctry!(
req,
conn.query(
"SELECT rustdoc_status
const SUCCESS_COLOR: &str = "#4d76ae";
const FAILURE_COLOR: &str = "#e05d44";

let out = match match_version(&mut conn, &name, Some(&version)).and_then(|m| m.assume_exact()) {
Ok(MatchSemver::Exact((version, id))) => {
let rows = ctry!(
req,
conn.query(
"SELECT rustdoc_status
FROM releases
WHERE releases.id = $1",
&[&id]
),
);
if !rows.is_empty() && rows[0].get(0) {
BadgeOptions {
subject: "docs".to_owned(),
status: version,
color: "#4d76ae".to_owned(),
}
} else {
BadgeOptions {
subject: "docs".to_owned(),
status: version,
color: "#e05d44".to_owned(),
}
}
&[&id]
),
);
if !rows.is_empty() && rows[0].get(0) {
builder("docs".to_string(), version, SUCCESS_COLOR.to_string())
} else {
builder("docs".to_string(), version, FAILURE_COLOR.to_string())
}
}

Ok(MatchSemver::Semver((version, _))) => {
let base_url = format!("{}/{}/badge.svg", redirect_base(req), name);
let url = ctry!(
req,
iron::url::Url::parse_with_params(&base_url, &[("version", version)]),
);
let iron_url = ctry!(req, Url::from_generic_url(url));
return Ok(super::redirect(iron_url));
}
Ok(MatchSemver::Semver((version, _))) => {
let base_url = format!("{}/{}/badge.{}", redirect_base(req), name, ext);
let url = ctry!(
req,
iron::url::Url::parse_with_params(&base_url, &[("version", version)]),
);
let iron_url = ctry!(req, Url::from_generic_url(url));
return Ok(super::redirect(iron_url));
}

Err(Nope::VersionNotFound) => BadgeOptions {
subject: "docs".to_owned(),
status: "version not found".to_owned(),
color: "#e05d44".to_owned(),
},

Err(_) => BadgeOptions {
subject: "docs".to_owned(),
status: "no builds".to_owned(),
color: "#e05d44".to_owned(),
},
};
Err(Nope::VersionNotFound) => builder(
"docs".to_string(),
"version not found".to_string(),
FAILURE_COLOR.to_string(),
),
Err(_) => builder(
"docs".to_string(),
"no builds".to_string(),
FAILURE_COLOR.to_string(),
),
};

let mut resp = Response::with((status::Ok, ctry!(req, Badge::new(options)).to_svg()));
resp.headers
.set(ContentType("image/svg+xml".parse().unwrap()));
let mut resp = Response::with((status::Ok, ctry!(req, out)));
resp.headers.set(content_type);
resp.headers.set(Expires(HttpDate(time::now())));
resp.headers.set(CacheControl(vec![
CacheDirective::NoCache,
Expand All @@ -633,6 +632,45 @@ pub fn badge_handler(req: &mut Request) -> IronResult<Response> {
Ok(resp)
}

pub fn badge_handler_svg(req: &mut Request) -> IronResult<Response> {
use badge::{Badge, BadgeOptions};

let builder = |subject, status, color| {
let options = BadgeOptions {
subject,
status,
color,
};
Badge::new(options).map(|badge| badge.to_svg())
};
badge_handler_common(
req,
"svg",
ContentType("image/svg+xml".parse().unwrap()),
builder,
)
}

pub fn badge_handler_json(req: &mut Request) -> IronResult<Response> {
use serde_json::Value;
use std::collections::HashMap;

let builder = |label, message, color| {
let mut out = HashMap::new();
out.insert("schemaVersion", Value::Number(1.into()));
out.insert("label", Value::String(label));
out.insert("message", Value::String(message));
out.insert("color", Value::String(color));
serde_json::to_string(&out)
};
badge_handler_common(
req,
"json",
ContentType("application/json".parse().unwrap()),
builder,
)
}

/// Serves shared web resources used by rustdoc-generated documentation.
///
/// This includes common `css` and `js` files that only change when the compiler is updated, but are
Expand Down
20 changes: 18 additions & 2 deletions templates/core/about/badges.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ <h1>Badges</h1>
<p>
Badge will display in blue if docs.rs is successfully hosting your crate
documentation, and red if building documentation failing.
</p>
</p>

<p>Example badges for mio crate:</p>
<table class="pure-table pure-table-horizontal">
Expand Down Expand Up @@ -48,6 +48,22 @@ <h1>Badges</h1>
<td><img src="{{ mio_badge | safe }}?version=0.1.0" alt="mio" /></td>
</tr>
</tbody>
</table>
</table>

<p>
If you want more customization, you can also get the badge data as JSON that
matches the <a href="https://shields.io/endpoint">the shields.io endpoint spec</a>
by changing the extension from <code>.svg</code> to <code>.json</code> .
</p>

<p>
{%- set rand_badge = "https://docs.rs/rand/badge.json" -%}

For example, the image at
<code>https://img.shields.io/endpoint?url={{ rand_badge }}&label=Documentation&color=yellow</code>
outputs
<img src="https://img.shields.io/endpoint?url={{ rand_badge | safe}}&label=Documentation&color=yellow" alt="rand"/>
</p>

</div>
{%- endblock body %}