commit
36d85b6eef
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -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
147
src/color.rs
Normal 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!(£_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());
|
||||||
|
}
|
||||||
|
}
|
46
src/error.rs
Normal file
46
src/error.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
111
src/main.rs
111
src/main.rs
@ -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))
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user