From 82074a3ae33ffe2991f7d4d8b1e8a5e501b5fb09 Mon Sep 17 00:00:00 2001 From: Suntharesan Mohan Date: Fri, 24 Jul 2020 20:25:34 -0400 Subject: [PATCH] Optional subject (#11) * Make subject optional - switch clap (Better cli) - rename bin to run as cargo sucommand Signed-off-by: Suntharesan Mohan * Update badge on api Signed-off-by: Suntharesan Mohan --- Cargo.toml | 2 +- benches/badge_benchmark.rs | 34 ++++++--- examples/badge_default.rs | 2 +- merit-api/src/badge_routes.rs | 6 +- merit-api/src/main.rs | 6 +- merit-api/src/utils/error.rs | 5 +- src/badge/content.rs | 25 +++++++ src/badge/mod.rs | 108 +++++++++++---------------- src/bin/{badge.rs => cargo-badge.rs} | 66 +++++++++------- src/lib.rs | 6 +- 10 files changed, 145 insertions(+), 115 deletions(-) rename src/bin/{badge.rs => cargo-badge.rs} (70%) diff --git a/Cargo.toml b/Cargo.toml index 636df85..f286004 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ harness = false [dependencies] ab_glyph = "0.2" -argh = "0.1" +clap = { version = "3.0.0-beta.1", features = ["derive", "suggestions", "color"] } cssparser = "0.27" maud = "0.22" phf = "0.8" diff --git a/benches/badge_benchmark.rs b/benches/badge_benchmark.rs index 2883d74..d4e46f8 100644 --- a/benches/badge_benchmark.rs +++ b/benches/badge_benchmark.rs @@ -3,7 +3,8 @@ use merit::{Badge, Color, Icon, Size, Styles, DEFAULT_BLUE, DEFAULT_WHITE}; use std::convert::TryFrom; pub fn criterion_benchmark(c: &mut Criterion) { - let all_text = Badge::new("Hello") + let all_text = Badge::new() + .subject("Hello") .color(Color("#6f42c1".to_string())) .style(Styles::Flat) .icon(Icon::try_from("github").unwrap()) @@ -11,7 +12,8 @@ pub fn criterion_benchmark(c: &mut Criterion) { .size(Size::Large) .text("text content"); - let all_data = Badge::new("Hello") + let all_data = Badge::new() + .subject("Hello") .color(Color("#6f42c1".to_string())) .style(Styles::Flat) .icon(Icon::try_from("github").unwrap()) @@ -19,59 +21,67 @@ pub fn criterion_benchmark(c: &mut Criterion) { .size(Size::Large) .data(vec![7, 5, 2, 4, 8, 3, 7]); - let subject = Badge::new("Hello") + let subject = Badge::new() .color(DEFAULT_BLUE.parse().unwrap()) .style(Styles::Classic) .icon_color(DEFAULT_WHITE.parse().unwrap()) - .subject(); + .text("Hello"); - let with_text = Badge::new("Hello") + let with_text = Badge::new() + .subject("Hello") .color(DEFAULT_BLUE.parse().unwrap()) .style(Styles::Classic) .icon_color(DEFAULT_WHITE.parse().unwrap()) .text("text content"); - let medium_size = Badge::new("Hello") + let medium_size = Badge::new() + .subject("Hello") .color(DEFAULT_BLUE.parse().unwrap()) .style(Styles::Classic) .icon_color(DEFAULT_WHITE.parse().unwrap()) .size(Size::Medium) .text("text content"); - let large_size = Badge::new("Hello") + let large_size = Badge::new() + .subject("Hello") .color(DEFAULT_BLUE.parse().unwrap()) .style(Styles::Classic) .icon_color(DEFAULT_WHITE.parse().unwrap()) .size(Size::Large) .text("text content"); - let red = Badge::new("Hello") + let red = Badge::new() + .subject("Hello") .color(Color("ff0000".to_string())) .style(Styles::Classic) .icon_color(DEFAULT_WHITE.parse().unwrap()) .text("red"); - let icon_brand = Badge::new("Hello") + let icon_brand = Badge::new() + .subject("Hello") .color(DEFAULT_BLUE.parse().unwrap()) .style(Styles::Classic) .icon(Icon::try_from("github").unwrap()) .icon_color(DEFAULT_WHITE.parse().unwrap()) .text("brand"); - let icon_solid = Badge::new("Hello") + let icon_solid = Badge::new() + .subject("Hello") .color(DEFAULT_BLUE.parse().unwrap()) .style(Styles::Classic) .icon(Icon::try_from("code").unwrap()) .icon_color(DEFAULT_WHITE.parse().unwrap()) .text("solid"); - let data = Badge::new("Hello") + let data = Badge::new() + .subject("Hello") .color(DEFAULT_BLUE.parse().unwrap()) .style(Styles::Classic) .icon_color(DEFAULT_WHITE.parse().unwrap()) .data(vec![1, 5, 2, 4, 8, 3, 7]); - let flat = Badge::new("Hello") + let flat = Badge::new() + .subject("Hello") .color(DEFAULT_BLUE.parse().unwrap()) .style(Styles::Classic) .icon_color(DEFAULT_WHITE.parse().unwrap()) diff --git a/examples/badge_default.rs b/examples/badge_default.rs index b789d43..722403a 100644 --- a/examples/badge_default.rs +++ b/examples/badge_default.rs @@ -1,7 +1,7 @@ use merit::Badge; fn main() { - let badge = Badge::new("Badge Maker"); + let badge = Badge::new().text("Badge Maker"); println!("{}", badge); } diff --git a/merit-api/src/badge_routes.rs b/merit-api/src/badge_routes.rs index 325e270..065d21e 100644 --- a/merit-api/src/badge_routes.rs +++ b/merit-api/src/badge_routes.rs @@ -42,7 +42,8 @@ async fn url_badge_handler( let data: BadgeOptions = b.json().await?; - let mut badge = Badge::new(&data.subject); + let mut badge = Badge::new(); + badge.subject(&data.subject); match (data.color, query.color) { (_, Some(c)) => { @@ -95,7 +96,8 @@ struct BadgeInfo { fn badge_handler((params, query): (web::Path, web::Query)) -> HttpResponse { let query = query.into_inner(); - let mut req_badge = Badge::new(¶ms.subject); + let mut req_badge = Badge::new(); + req_badge.subject(¶ms.subject); if let Some(c) = query.color { req_badge.color(c); } diff --git a/merit-api/src/main.rs b/merit-api/src/main.rs index ce233d5..4a16323 100644 --- a/merit-api/src/main.rs +++ b/merit-api/src/main.rs @@ -6,7 +6,6 @@ extern crate actix_web; mod badge_routes; mod utils; -use actix_files::Files; use actix_web::{ http::{header, StatusCode}, middleware, web, App, HttpResponse, HttpServer, Responder, @@ -31,8 +30,8 @@ async fn favicon() -> impl Responder { } async fn default_404() -> impl Responder { - let mut badge = Badge::new("Error"); - badge.color(DEFAULT_GRAY.parse().unwrap()); + let mut badge = Badge::new(); + badge.subject("Error").color(DEFAULT_GRAY.parse().unwrap()); HttpResponse::NotFound() .content_type("image/svg+xml") @@ -56,7 +55,6 @@ async fn main() -> io::Result<()> { .service(index) .service(favicon) .configure(badge_routes::config) - .service(Files::new("/", "./merit-web/dist").index_file("index.html")) }); server = if let Some(l) = listenfd.take_tcp_listener(0)? { diff --git a/merit-api/src/utils/error.rs b/merit-api/src/utils/error.rs index 5f2ff62..251b634 100644 --- a/merit-api/src/utils/error.rs +++ b/merit-api/src/utils/error.rs @@ -31,9 +31,8 @@ impl Default for BadgeError { impl BadgeError { pub fn err_badge(&self) -> String { let icon: Icon = Icon::try_from("exclamation-circle").unwrap(); - let mut badge = Badge::new("Error"); - badge.icon(icon); - badge.color("red".parse().unwrap()); + let mut badge = Badge::new(); + badge.subject("Error").icon(icon).color("red".parse().unwrap()); let text = match self { BadgeError::Http { diff --git a/src/badge/content.rs b/src/badge/content.rs index 1591b33..34baaff 100644 --- a/src/badge/content.rs +++ b/src/badge/content.rs @@ -79,6 +79,7 @@ impl Content for &str { } } +#[derive(Default)] pub(super) struct ContentSize { pub(super) x: usize, pub(super) y: usize, @@ -110,4 +111,28 @@ mod tests { let bc = s.content(20); assert!(bc.width > 0); } + #[test] + fn content_text_has_width() { + let text = "".content(20); + assert_eq!(text.width, 0); + let text = "npm".content(20); + assert_eq!(text.width, 36); + let text = "long text".content(20); + assert_eq!(text.width, 73); + } + + #[test] + fn content_data_has_width() { + let d1 = vec![].content(20); + assert_eq!(d1.width, 0); + let d2 = vec![2, 4, 3, 2].content(20); + assert_eq!(d2.width, 100); + } + + #[test] + fn content_data_is_same() { + let d1 = vec![2, 4, 3, 2].content(20); + let d2 = &vec![2, 4, 3, 2].content(20); + assert_eq!(d1.content, d2.content); + } } diff --git a/src/badge/mod.rs b/src/badge/mod.rs index 3091f5d..fb9e595 100644 --- a/src/badge/mod.rs +++ b/src/badge/mod.rs @@ -87,7 +87,7 @@ pub enum BadgeType<'a, const S: BadgeTypeState> { #[derive(Debug)] pub struct Badge<'a, const S: BadgeTypeState> { - pub subject: &'a str, + pub subject: Option<&'a str>, pub color: Color, pub style: Styles, pub icon: Option>, @@ -97,9 +97,9 @@ pub struct Badge<'a, const S: BadgeTypeState> { } impl<'a> Badge<'a, { BadgeTypeState::Init }> { - pub fn new(subject: &'a str) -> Self { + pub fn new() -> Self { Badge { - subject, + subject: None, color: DEFAULT_BLUE.parse().unwrap(), style: Styles::Classic, icon: None, @@ -109,6 +109,11 @@ impl<'a> Badge<'a, { BadgeTypeState::Init }> { } } + pub fn subject(&mut self, subject: &'a str) -> &mut Self { + self.subject = Some(subject); + self + } + pub fn color(&mut self, color: Color) -> &mut Self { self.color = color; self @@ -160,22 +165,10 @@ impl<'a> Badge<'a, { BadgeTypeState::Init }> { content: BadgeType::Data(data), } } - pub fn subject(&mut self) -> Badge<'a, { BadgeTypeState::Init }> { - Badge { - subject: self.subject, - color: self.color.clone(), - style: self.style, - icon: self.icon.clone(), - icon_color: self.icon_color.clone(), - height: self.height, - content: BadgeType::Init, - } - } } const SVG_FONT_MULTIPLIER: f32 = 0.65; const FONT_CALC_MULTIPLIER: f32 = 0.8; -// const PADDING_MULTIPLIER: f32 = 0.5; impl<'a, const T: BadgeTypeState> Display for Badge<'a, { T }> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -192,8 +185,16 @@ impl<'a, const T: BadgeTypeState> Display for Badge<'a, { T }> { x_offset = 5; } - let subject = self.subject.content(calc_font_size); - let subject_size = subject.content_size(icon_width, padding, height, x_offset); + let subject = self.subject.as_ref().map(|s| s.content(calc_font_size)); + let subject_size: ContentSize = match &subject { + Some(s) => s.content_size(icon_width, padding, height, x_offset), + None if self.icon.is_some() => ContentSize { + rw: icon_width + x_offset * 2, + x: x_offset, + y: height, + }, + _ => ContentSize::default(), + }; let content = match &self.content { BadgeType::Data(d) => Some((d.content(height), BadgeTypeState::Data)), @@ -213,7 +214,7 @@ impl<'a, const T: BadgeTypeState> Display for Badge<'a, { T }> { rw: c.width + 5, }, (Some((c, _)), _) => c.content_size(0, padding, height, 0), - (_, _) => ContentSize { x: 0, y: 0, rw: 0 }, + (_, _) => ContentSize::default(), }; let width = subject_size.rw + content_size.rw; @@ -248,11 +249,13 @@ impl<'a, const T: BadgeTypeState> Display for Badge<'a, { T }> { } g#bg mask=@if self.style == Styles::Classic { "url(#m)" } { rect fill=@if self.style == Styles::Flat { (DEFAULT_GRAY) } @else { "url(#a)" } height=(height) width=(width) {} - rect#subject - fill=@if content.is_some() { (DEFAULT_GRAY_DARK) } @else { (self.color.to_string()) } - height=(height) - width=(subject_size.rw) - {} + @if subject.is_some() || self.icon.is_some() { + rect#subject + fill=@if content.is_some() { (DEFAULT_GRAY_DARK) } @else { (self.color.to_string()) } + height=(height) + width=(subject_size.rw) + {} + } rect#content fill=@match &content{ Some((_, s)) if s == &BadgeTypeState::Data => { (DEFAULT_GRAY) }, @@ -268,14 +271,14 @@ impl<'a, const T: BadgeTypeState> Display for Badge<'a, { T }> { font-family="Verdana,sans-serif" font-size=(font_size) transform="translate(0, 0)" { - @if subject.content.len() > 0 { + @if let Some(s) = subject { text dominant-baseline="central" text-anchor="middle" x=(subject_size.x) y=(subject_size.y) filter="url(#shadow)" - { (subject.content) } + { (s.content) } } @match &content { Some((c, s)) if s == &BadgeTypeState::Data => { @@ -327,7 +330,7 @@ impl<'a, const T: BadgeTypeState> Display for Badge<'a, { T }> { #[cfg(test)] mod tests { - use super::{Badge, Color, Content, Size, Styles, DEFAULT_BLUE}; + use super::{Badge, Color, Size, Styles, DEFAULT_BLUE}; use scraper::{Html, Selector}; use crate::Icon; @@ -335,7 +338,8 @@ mod tests { #[test] fn default_badge_has_classic_style() { - let badge = Badge::new("just text"); + let mut badge = Badge::new(); + &badge.subject("just text"); let badge_svg = badge.to_string(); let doc = Html::parse_fragment(&badge_svg); assert_eq!(badge.style, Styles::Classic, "style not Classic"); @@ -344,7 +348,8 @@ mod tests { } #[test] fn default_badge_has_20px_height() { - let badge = Badge::new("just text"); + let mut badge = Badge::new(); + &badge.subject("just text"); let badge_svg = badge.to_string(); let doc = Html::parse_fragment(&badge_svg); let selector = Selector::parse("svg").unwrap(); @@ -353,7 +358,8 @@ mod tests { } #[test] fn default_badge_only_has_subject() { - let badge = Badge::new("just subject"); + let mut badge = Badge::new(); + &badge.subject("just subject"); let badge_svg = badge.to_string(); let doc = Html::parse_fragment(&badge_svg); let text_sel = Selector::parse("g#text > text").unwrap(); @@ -364,7 +370,8 @@ mod tests { } #[test] fn default_badge_has_333_as_background_color() { - let mut badge = Badge::new("just text"); + let mut badge = Badge::new(); + &badge.subject("just text"); badge.color(DEFAULT_BLUE.parse::().unwrap()); let def_color: Color = DEFAULT_BLUE.parse().unwrap(); let badge_svg = badge.to_string(); @@ -376,7 +383,7 @@ mod tests { #[test] fn badge_with_text() { - let badge = Badge::new("with subject").text("badge text"); + let badge = Badge::new().subject("with subject").text("badge text"); let doc = Html::parse_fragment(&badge.to_string()); let subject_sel = Selector::parse("g#text > text:last-child").unwrap(); let subject = doc.select(&subject_sel).next().unwrap(); @@ -386,8 +393,8 @@ mod tests { #[test] fn badge_with_icon() { let icon = Icon::try_from("git").unwrap(); - let mut badge = Badge::new("with icon"); - &badge.icon(icon); + let mut badge = Badge::new(); + &badge.subject("with icon").icon(icon); let icon = &badge.icon; assert!(icon.is_some()); @@ -399,8 +406,8 @@ mod tests { } #[test] fn badge_has_medium_icon() { - let mut badge = Badge::new("with icon"); - badge.size(Size::Medium); + let mut badge = Badge::new(); + &badge.subject("with icon").size(Size::Medium); let doc = Html::parse_fragment(&badge.to_string()); let svg_sel = Selector::parse("svg").unwrap(); let svg = doc.select(&svg_sel).next().unwrap(); @@ -408,8 +415,8 @@ mod tests { } #[test] fn badge_has_large_icon() { - let mut badge = Badge::new("with icon"); - badge.size(Size::Large); + let mut badge = Badge::new(); + &badge.subject("with icon").size(Size::Large); let doc = Html::parse_fragment(&badge.to_string()); let svg_sel = Selector::parse("svg").unwrap(); let svg = doc.select(&svg_sel).next().unwrap(); @@ -418,7 +425,7 @@ mod tests { #[test] fn badge_with_data() { - let badge = Badge::new("Some data").data(vec![1, 2, 3, 4, 5]); + let badge = Badge::new().subject("Some data").data(vec![1, 2, 3, 4, 5]); let doc = Html::parse_fragment(&badge.to_string()); println!("{:?}", &badge.to_string()); @@ -426,29 +433,4 @@ mod tests { let svg = doc.select(&line_sel).next().unwrap(); assert!(svg.value().attr("d").is_some()); } - - #[test] - fn content_text_has_width() { - let text = "".content(20); - assert_eq!(text.width, 0); - let text = "npm".content(20); - assert_eq!(text.width, 43); - let text = "long text".content(20); - assert_eq!(text.width, 90); - } - - #[test] - fn content_data_has_width() { - let d1 = vec![].content(20); - assert_eq!(d1.width, 0); - let d2 = vec![2, 4, 3, 2].content(20); - assert_eq!(d2.width, 100); - } - - #[test] - fn content_data_is_same() { - let d1 = vec![2, 4, 3, 2].content(20); - let d2 = &vec![2, 4, 3, 2].content(20); - assert_eq!(d1.content, d2.content); - } } diff --git a/src/bin/badge.rs b/src/bin/cargo-badge.rs similarity index 70% rename from src/bin/badge.rs rename to src/bin/cargo-badge.rs index 1c3772b..357183a 100644 --- a/src/bin/badge.rs +++ b/src/bin/cargo-badge.rs @@ -21,61 +21,76 @@ //! ``` //! -use argh::FromArgs; +// use argh::FromArgs; +use clap::Clap; use merit::{icon_exists, Badge, BadgeData, Color, Icon, Size, Styles}; -use std::{convert::TryFrom, error::Error, fs::File, io::prelude::*, path::PathBuf}; +use std::{convert::TryFrom, error::Error, fs::File, io::prelude::*, path::PathBuf, str::FromStr}; + +#[derive(Debug, PartialEq)] +enum Content { + Text(String), + Data(BadgeData), +} + +impl FromStr for Content { + type Err = String; + fn from_str(s: &str) -> Result { + Ok(BadgeData::from_str(s).map_or(Content::Text(s.to_string()), |d| Content::Data(d))) + } +} -#[derive(FromArgs)] /// Fast badge generator for any purpose +#[derive(Debug, Clap)] +#[clap(version)] struct Opt { /// badge subject - #[argh(option, short = 's')] - subject: String, + #[clap(short, long)] + subject: Option, /// badge style. [possible values: flat | f, classic | c] - #[argh(option)] + #[clap(long)] style: Option, /// badge size. [possible values: large | l, medium | m, small | s] - #[argh(option)] + #[clap(long)] size: Option, /// badge color. Must be a valid css color - #[argh(option)] + #[clap(long)] color: Option, /// badge icon. icon can be any Brand or Solid icons from fontawesome - #[argh(option)] + #[clap(long)] icon: Option, /// icon color. Must be a valid css color - #[argh(option)] + #[clap(long)] icon_color: Option, /// output svg to file - #[argh(option)] + #[clap(short, long)] out: Option, - /// badge text - #[argh(option, short = 't')] - text: Option, - - /// data for badge chart. - #[argh(option)] - data: Option, + /// badge content + #[clap()] + content: Content, } fn main() -> Result<(), Box> { - let opt: Opt = argh::from_env(); + let opt = Opt::parse(); if let Some(icon) = &opt.icon { if !icon_exists(icon) { - eprintln!("Icon does not exists. Try using a fontawesome icon name"); - std::process::exit(1); + eprintln!(""); + return Err("Icon does not exists. Try using a fontawesome icon name".into()); } } - let mut badge = Badge::new(&opt.subject); + let mut badge = Badge::new(); + + if let Some(sub) = &opt.subject { + &badge.subject(sub); + } if let Some(col) = opt.color { badge.color(col); } @@ -96,10 +111,9 @@ fn main() -> Result<(), Box> { } } - let svg = match (opt.data, opt.text) { - (Some(d), _) => badge.data(d.0.into()).to_string(), - (_, Some(t)) => badge.text(&t).to_string(), - (_, _) => badge.to_string(), + let svg = match opt.content { + Content::Data(d) => badge.data(d.0.into()).to_string(), + Content::Text(t) => badge.text(&t).to_string(), }; if let Some(out_file) = opt.out { diff --git a/src/lib.rs b/src/lib.rs index b2b092b..e9fe968 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ //! use merit::{Badge}; //! //! fn badge() { -//! let mut badge = Badge::new("Subject").text("Text"); +//! let mut badge = Badge::new().subject("Subject").text("Text"); //! println!("{}", badge.to_string()); //! } //! ``` @@ -25,7 +25,7 @@ //! use merit::{Badge}; //! //! fn badge_with_data() { -//! let mut badge = Badge::new("Subject").data(vec![12, 34, 23,56,45]); +//! let mut badge = Badge::new().subject("Subject").data(vec![12, 34, 23,56,45]); //! println!("{}", badge.to_string()); //! } //! ``` @@ -88,7 +88,7 @@ impl<'de> Deserialize<'de> for Color { } } -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde_de", derive(Serialize, Deserialize))] pub struct BadgeData(pub Vec);