diff --git a/Cargo.lock b/Cargo.lock index 5486c34..1e5a177 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -682,6 +682,8 @@ dependencies = [ "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)", "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)", ] diff --git a/Cargo.toml b/Cargo.toml index 36c7176..83a6e03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,6 @@ futures = "0.1.25" git2 = "0.8.0" openssl-probe = "0.1.2" pretty_env_logger = "0.3.0" +serde = "1.0.90" +serde_derive = "1.0.90" structopt = "0.2.15" diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..6d24f8d --- /dev/null +++ b/src/color.rs @@ -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 { + 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 { + 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 { + 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!(£_valid_long.unwrap().to_code(), "#aabb11"); + assert_eq!(£_valid_short.unwrap().to_code(), "#ab1"); + + assert!(too_short.is_err()); + assert!(too_long.is_err()); + assert!(non_hex.is_err()); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..df5bf64 --- /dev/null +++ b/src/error.rs @@ -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 for Error { + fn from(s: String) -> Self { + Error::Badge(s) + } +} + +impl From for Error { + fn from(err: git2::Error) -> Self { + Error::Git(err) + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + Error::Io(err) + } +} diff --git a/src/main.rs b/src/main.rs index dc07e9f..444af0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,29 @@ #[macro_use] extern crate actix_web; +#[macro_use] +extern crate serde_derive; +mod color; +mod error; + +use crate::{ + color::{ColorKind, ToCode}, + error::Error, +}; use actix_web::{ - error, + error::ErrorBadRequest, http::{ self, header::{CacheControl, CacheDirective, Expires}, }, - middleware, web, App, HttpResponse, HttpServer, ResponseError, + middleware, web, App, HttpResponse, HttpServer, }; use badge::{Badge, BadgeOptions}; use bytes::Bytes; use futures::{unsync::mpsc, Stream}; use git2::Repository; use std::{ + convert::TryFrom, fs::create_dir_all, path::{Path, PathBuf}, process::Command, @@ -45,47 +55,9 @@ struct Opt { host: String, } -#[derive(Debug)] -enum Error { - Git(git2::Error), - 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 for Error { - fn from(s: String) -> Self { - Error::Badge(s) - } -} - -impl From for Error { - fn from(err: git2::Error) -> Self { - Error::Git(err) - } -} - -impl From for Error { - fn from(err: std::io::Error) -> Self { - Error::Io(err) - } +#[derive(Debug, Deserialize)] +struct BadgeQuery { + color: Option, } fn pull(path: impl AsRef) -> Result<(), Error> { @@ -131,6 +103,7 @@ fn calculate_hoc( service: &str, state: web::Data, data: web::Path<(String, String)>, + color: web::Query, ) -> Result { let service_path = format!("{}/{}/{}", service, data.0, data.1); let path = format!("{}/{}", *state, service_path); @@ -143,9 +116,15 @@ fn calculate_hoc( } pull(&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 { subject: "Hits-of-Code".to_string(), - color: "#44CC11".to_string(), + color: color.to_code(), status: hoc.to_string(), }; let badge = Badge::new(badge_opt)?; @@ -163,28 +142,59 @@ fn calculate_hoc( CacheDirective::NoCache, CacheDirective::NoStore, ])) - .streaming(rx_body.map_err(|_| error::ErrorBadRequest("bad request")))) + .streaming(rx_body.map_err(|_| ErrorBadRequest("bad request")))) } fn github( state: web::Data, data: web::Path<(String, String)>, + color: web::Query, ) -> Result { - calculate_hoc("github.com", state, data) + calculate_hoc("github.com", state, data, color) } fn gitlab( state: web::Data, data: web::Path<(String, String)>, + color: web::Query, ) -> Result { - calculate_hoc("gitlab.com", state, data) + calculate_hoc("gitlab.com", state, data, color) } fn bitbucket( state: web::Data, data: web::Path<(String, String)>, + color: web::Query, ) -> Result { - calculate_hoc("bitbucket.org", state, data) + calculate_hoc("bitbucket.org", state, data, color) +} + +#[get("/badge")] +fn badge_example(col: web::Query) -> Result { + 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 { @@ -200,7 +210,7 @@ fn index() -> HttpResponse { HttpResponse::Ok() .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")] @@ -210,7 +220,7 @@ fn css() -> HttpResponse { HttpResponse::Ok() .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<()> { @@ -226,6 +236,7 @@ fn main() -> std::io::Result<()> { .wrap(middleware::Logger::default()) .service(index) .service(css) + .service(badge_example) .service(web::resource("/github/{user}/{repo}").to(github)) .service(web::resource("/gitlab/{user}/{repo}").to(gitlab)) .service(web::resource("/bitbucket/{user}/{repo}").to(bitbucket)) diff --git a/static/index.html b/static/index.html index 5b55909..a5e5a2e 100644 --- a/static/index.html +++ b/static/index.html @@ -51,6 +51,42 @@ would render this badge: +

Colors

+ +
+

+You can generate badges with custom colors via the color query parameter. The following predefined colors +are supported: +

+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +

+You can also pass HTML color codes: +

+ +
+ 
+
+
+

Source Code