Merge branch 'feature/badge-colors'

Closes #1
This commit is contained in:
Valentin Brandl 2019-04-19 16:02:22 +02:00
commit 36d85b6eef
No known key found for this signature in database
GPG Key ID: 30D341DD34118D7D
6 changed files with 294 additions and 50 deletions

2
Cargo.lock generated
View File

@ -682,6 +682,8 @@ dependencies = [
"git2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "git2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"pretty_env_logger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "pretty_env_logger 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
"structopt 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)",
] ]

View File

@ -12,4 +12,6 @@ futures = "0.1.25"
git2 = "0.8.0" git2 = "0.8.0"
openssl-probe = "0.1.2" openssl-probe = "0.1.2"
pretty_env_logger = "0.3.0" pretty_env_logger = "0.3.0"
serde = "1.0.90"
serde_derive = "1.0.90"
structopt = "0.2.15" structopt = "0.2.15"

147
src/color.rs Normal file
View File

@ -0,0 +1,147 @@
use crate::error::Error;
use std::convert::TryFrom;
pub(crate) trait ToCode {
fn to_code(&self) -> String;
}
#[derive(Debug)]
pub(crate) enum ColorName {
BrightGreen,
Green,
YellowGreen,
Yellow,
Orange,
Red,
Blue,
LightGrey,
Success,
Important,
Critical,
Informational,
Inactive,
}
impl TryFrom<&str> for ColorName {
type Error = Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s.to_lowercase().as_str() {
"brightgreen" => Ok(ColorName::BrightGreen),
"green" => Ok(ColorName::Green),
"yellowgreen" => Ok(ColorName::YellowGreen),
"yellow" => Ok(ColorName::Yellow),
"orange" => Ok(ColorName::Orange),
"red" => Ok(ColorName::Red),
"blue" => Ok(ColorName::Blue),
"lightgrey" => Ok(ColorName::LightGrey),
"success" => Ok(ColorName::Success),
"important" => Ok(ColorName::Important),
"critical" => Ok(ColorName::Critical),
"informational" => Ok(ColorName::Informational),
"inactive" => Ok(ColorName::Inactive),
_ => Err(Error::ParseColor),
}
}
}
impl ToCode for ColorName {
fn to_code(&self) -> String {
use ColorName::*;
match self {
BrightGreen | Success => "#44cc11",
Green => "#97ca00",
YellowGreen => "#a4a61d",
Yellow => "#dfb317",
Orange | Important => "#fe7d37",
Red | Critical => "#e05d44",
Blue | Informational => "#007ec6",
LightGrey | Inactive => "#9f9f9f",
}
.to_string()
}
}
#[derive(Debug)]
pub(crate) struct ColorCode(String);
impl TryFrom<&str> for ColorCode {
type Error = Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
let s = if s.starts_with('#') { &s[1..] } else { s };
let len = s.len();
if (len == 3 || len == 6) && s.chars().all(|c| c.is_digit(16)) {
Ok(ColorCode(s.to_lowercase().to_string()))
} else {
Err(Error::ParseColor)
}
}
}
impl ToCode for ColorCode {
fn to_code(&self) -> String {
format!("#{}", self.0)
}
}
#[derive(Debug)]
pub(crate) enum ColorKind {
Name(ColorName),
Code(ColorCode),
}
impl TryFrom<&str> for ColorKind {
type Error = Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
ColorName::try_from(s)
.map(|c| ColorKind::Name(c))
.or_else(|_| ColorCode::try_from(s).map(|c| ColorKind::Code(c)))
}
}
impl ToCode for ColorKind {
fn to_code(&self) -> String {
match self {
ColorKind::Name(name) => name.to_code(),
ColorKind::Code(code) => code.to_code(),
}
}
}
impl Default for ColorKind {
fn default() -> Self {
ColorKind::Name(ColorName::Success)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_colorcode() {
let valid_long = "aaBB11";
let valid_short = "aB1";
let pound_valid_long: &str = &format!("#{}", valid_long);
let pound_valid_short: &str = &format!("#{}", valid_short);
let valid_long = ColorCode::try_from(valid_long);
let valid_short = ColorCode::try_from(valid_short);
let pound_valid_long = ColorCode::try_from(pound_valid_long);
let pound_valid_short = ColorCode::try_from(pound_valid_short);
let too_short = "ab";
let too_long = "aaaaaab";
let non_hex = "aag";
let too_short = ColorCode::try_from(too_short);
let too_long = ColorCode::try_from(too_long);
let non_hex = ColorCode::try_from(non_hex);
assert_eq!(&valid_long.unwrap().to_code(), "#aabb11");
assert_eq!(&valid_short.unwrap().to_code(), "#ab1");
assert_eq!(&pound_valid_long.unwrap().to_code(), "#aabb11");
assert_eq!(&pound_valid_short.unwrap().to_code(), "#ab1");
assert!(too_short.is_err());
assert!(too_long.is_err());
assert!(non_hex.is_err());
}
}

46
src/error.rs Normal file
View File

@ -0,0 +1,46 @@
use actix_web::{HttpResponse, ResponseError};
#[derive(Debug)]
pub(crate) enum Error {
Git(git2::Error),
Io(std::io::Error),
Badge(String),
ParseColor,
}
impl std::fmt::Display for Error {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::Git(e) => write!(fmt, "Git({})", e),
Error::Io(e) => write!(fmt, "Io({})", e),
Error::Badge(s) => write!(fmt, "Badge({})", s),
Error::ParseColor => write!(fmt, "Parse error"),
}
}
}
impl ResponseError for Error {
fn error_response(&self) -> HttpResponse {
HttpResponse::InternalServerError().finish()
}
}
impl std::error::Error for Error {}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Badge(s)
}
}
impl From<git2::Error> for Error {
fn from(err: git2::Error) -> Self {
Error::Git(err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::Io(err)
}
}

View File

@ -1,19 +1,29 @@
#[macro_use] #[macro_use]
extern crate actix_web; extern crate actix_web;
#[macro_use]
extern crate serde_derive;
mod color;
mod error;
use crate::{
color::{ColorKind, ToCode},
error::Error,
};
use actix_web::{ use actix_web::{
error, error::ErrorBadRequest,
http::{ http::{
self, self,
header::{CacheControl, CacheDirective, Expires}, header::{CacheControl, CacheDirective, Expires},
}, },
middleware, web, App, HttpResponse, HttpServer, ResponseError, middleware, web, App, HttpResponse, HttpServer,
}; };
use badge::{Badge, BadgeOptions}; use badge::{Badge, BadgeOptions};
use bytes::Bytes; use bytes::Bytes;
use futures::{unsync::mpsc, Stream}; use futures::{unsync::mpsc, Stream};
use git2::Repository; use git2::Repository;
use std::{ use std::{
convert::TryFrom,
fs::create_dir_all, fs::create_dir_all,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command, process::Command,
@ -45,47 +55,9 @@ struct Opt {
host: String, host: String,
} }
#[derive(Debug)] #[derive(Debug, Deserialize)]
enum Error { struct BadgeQuery {
Git(git2::Error), color: Option<String>,
Io(std::io::Error),
Badge(String),
}
impl std::fmt::Display for Error {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::Git(e) => write!(fmt, "Git({})", e),
Error::Io(e) => write!(fmt, "Io({})", e),
Error::Badge(s) => write!(fmt, "Badge({})", s),
}
}
}
impl ResponseError for Error {
fn error_response(&self) -> HttpResponse {
HttpResponse::InternalServerError().finish()
}
}
impl std::error::Error for Error {}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Badge(s)
}
}
impl From<git2::Error> for Error {
fn from(err: git2::Error) -> Self {
Error::Git(err)
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::Io(err)
}
} }
fn pull(path: impl AsRef<Path>) -> Result<(), Error> { fn pull(path: impl AsRef<Path>) -> Result<(), Error> {
@ -131,6 +103,7 @@ fn calculate_hoc(
service: &str, service: &str,
state: web::Data<State>, state: web::Data<State>,
data: web::Path<(String, String)>, data: web::Path<(String, String)>,
color: web::Query<BadgeQuery>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let service_path = format!("{}/{}/{}", service, data.0, data.1); let service_path = format!("{}/{}/{}", service, data.0, data.1);
let path = format!("{}/{}", *state, service_path); let path = format!("{}/{}", *state, service_path);
@ -143,9 +116,15 @@ fn calculate_hoc(
} }
pull(&path)?; pull(&path)?;
let hoc = hoc(&path)?; let hoc = hoc(&path)?;
let color = color
.into_inner()
.color
.map(|s| ColorKind::try_from(s.as_str()))
.and_then(Result::ok)
.unwrap_or_default();
let badge_opt = BadgeOptions { let badge_opt = BadgeOptions {
subject: "Hits-of-Code".to_string(), subject: "Hits-of-Code".to_string(),
color: "#44CC11".to_string(), color: color.to_code(),
status: hoc.to_string(), status: hoc.to_string(),
}; };
let badge = Badge::new(badge_opt)?; let badge = Badge::new(badge_opt)?;
@ -163,28 +142,59 @@ fn calculate_hoc(
CacheDirective::NoCache, CacheDirective::NoCache,
CacheDirective::NoStore, CacheDirective::NoStore,
])) ]))
.streaming(rx_body.map_err(|_| error::ErrorBadRequest("bad request")))) .streaming(rx_body.map_err(|_| ErrorBadRequest("bad request"))))
} }
fn github( fn github(
state: web::Data<State>, state: web::Data<State>,
data: web::Path<(String, String)>, data: web::Path<(String, String)>,
color: web::Query<BadgeQuery>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
calculate_hoc("github.com", state, data) calculate_hoc("github.com", state, data, color)
} }
fn gitlab( fn gitlab(
state: web::Data<State>, state: web::Data<State>,
data: web::Path<(String, String)>, data: web::Path<(String, String)>,
color: web::Query<BadgeQuery>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
calculate_hoc("gitlab.com", state, data) calculate_hoc("gitlab.com", state, data, color)
} }
fn bitbucket( fn bitbucket(
state: web::Data<State>, state: web::Data<State>,
data: web::Path<(String, String)>, data: web::Path<(String, String)>,
color: web::Query<BadgeQuery>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
calculate_hoc("bitbucket.org", state, data) calculate_hoc("bitbucket.org", state, data, color)
}
#[get("/badge")]
fn badge_example(col: web::Query<BadgeQuery>) -> Result<HttpResponse, Error> {
let col = col.into_inner();
let color = col
.color
.clone()
.map(|s| ColorKind::try_from(s.as_str()))
.transpose()?
// .and_then(Result::ok)
.unwrap_or_default();
let badge_opt = BadgeOptions {
subject: "Hits-of-Code".to_string(),
color: color.to_code(),
status: col.color.unwrap_or_else(|| "success".to_string()),
};
let badge = Badge::new(badge_opt)?;
let (tx, rx_body) = mpsc::unbounded();
let _ = tx.unbounded_send(Bytes::from(badge.to_svg().as_bytes()));
let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24 * 365);
Ok(HttpResponse::Ok()
.content_type("image/svg+xml")
.set(Expires(expiration.into()))
.set(CacheControl(vec![CacheDirective::Public]))
.streaming(rx_body.map_err(|_| ErrorBadRequest("bad request"))))
} }
fn overview(_: web::Path<(String, String)>) -> HttpResponse { fn overview(_: web::Path<(String, String)>) -> HttpResponse {
@ -200,7 +210,7 @@ fn index() -> HttpResponse {
HttpResponse::Ok() HttpResponse::Ok()
.content_type("text/html") .content_type("text/html")
.streaming(rx_body.map_err(|_| error::ErrorBadRequest("bad request"))) .streaming(rx_body.map_err(|_| ErrorBadRequest("bad request")))
} }
#[get("/tacit-css.min.css")] #[get("/tacit-css.min.css")]
@ -210,7 +220,7 @@ fn css() -> HttpResponse {
HttpResponse::Ok() HttpResponse::Ok()
.content_type("text/css") .content_type("text/css")
.streaming(rx_body.map_err(|_| error::ErrorBadRequest("bad request"))) .streaming(rx_body.map_err(|_| ErrorBadRequest("bad request")))
} }
fn main() -> std::io::Result<()> { fn main() -> std::io::Result<()> {
@ -226,6 +236,7 @@ fn main() -> std::io::Result<()> {
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.service(index) .service(index)
.service(css) .service(css)
.service(badge_example)
.service(web::resource("/github/{user}/{repo}").to(github)) .service(web::resource("/github/{user}/{repo}").to(github))
.service(web::resource("/gitlab/{user}/{repo}").to(gitlab)) .service(web::resource("/gitlab/{user}/{repo}").to(gitlab))
.service(web::resource("/bitbucket/{user}/{repo}").to(bitbucket)) .service(web::resource("/bitbucket/{user}/{repo}").to(bitbucket))

View File

@ -51,6 +51,42 @@ would render this badge:
</pre> </pre>
</div> </div>
<h2>Colors</h2>
<div>
<p>
You can generate badges with custom colors via the <code>color</code> query parameter. The following predefined colors
are supported:
</p>
<pre>
<img src="/badge?color=brightgreen" />
<img src="/badge?color=green" />
<img src="/badge?color=yellowgreen" />
<img src="/badge?color=yellow" />
<img src="/badge?color=orange" />
<img src="/badge?color=red" />
<img src="/badge?color=blue" />
<img src="/badge?color=lightgrey" />
<img src="/badge?color=success" />
<img src="/badge?color=important" />
<img src="/badge?color=critical" />
<img src="/badge?color=informational" />
<img src="/badge?color=inactive" />
</pre>
<p>
You can also pass HTML color codes:
</p>
<pre>
<img src="/badge?color=ff69b4" /> <img src="/badge?color=9cf" />
</pre>
</div>
<h2>Source Code</h2> <h2>Source Code</h2>
<div> <div>