use serde::{Deserialize, Deserializer, Serialize};
use crate::Error;
fn normalize_hex(value: &str) -> Option<String> {
let value = value.trim().trim_start_matches('#').to_lowercase();
if !matches!(value.len(), 3 | 6) || !value.chars().all(|character| character.is_ascii_hexdigit())
{
return None;
}
if value.len() == 3 {
Some(value.chars().flat_map(|character| [character, character]).collect())
} else {
Some(value)
}
}
pub enum Period {
Week,
Month,
Year,
Total,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Animation {
Flow,
Shine,
Aurora,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub enum Color {
#[default]
Blue,
Green,
Lime,
Yellow,
Orange,
Red,
Gray,
Black,
White,
Cyan,
Hex(String),
}
impl Color {
pub fn to_hex(&self) -> String {
match self {
Color::Blue => "3b82f6".into(), Color::Green => "16a34a".into(), Color::Lime => "65a30d".into(), Color::Yellow => "ca8a04".into(), Color::Orange => "ea580c".into(), Color::Red => "ef4444".into(), Color::Gray => "71717a".into(), Color::Black => "18181b".into(), Color::White => "f4f4f5".into(), Color::Cyan => "0891b2".into(), Color::Hex(hex) => normalize_hex(hex).unwrap_or_else(|| Color::Black.to_hex()),
}
}
pub fn to_css(&self) -> String {
format!("#{}", self.to_hex())
}
}
impl<'a> TryFrom<&'a str> for Color {
type Error = Error;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
match value.to_lowercase().trim().replace("#", "").as_ref() {
"blue" => Ok(Color::Blue),
"green" | "brightgreen" | "bright-green" => Ok(Color::Green),
"lime" | "yellowgreen" => Ok(Color::Lime),
"yellow" => Ok(Color::Yellow),
"orange" => Ok(Color::Orange),
"red" => Ok(Color::Red),
"gray" | "grey" => Ok(Color::Gray),
"black" => Ok(Color::Black),
"white" => Ok(Color::White),
"cyan" => Ok(Color::Cyan),
x => {
if (x.len() == 3 || x.len() == 6) && x.chars().all(|c| c.is_ascii_hexdigit()) {
Ok(Color::Hex(x.to_string()))
} else {
Err(Error::InvalidColor(x.to_string()))
}
}
}
}
}
impl<'de> Deserialize<'de> for Color {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::try_from(s.as_ref()).map_err(serde::de::Error::custom)
}
}
impl Serialize for Color {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_hex())
}
}
#[derive(Debug, Default, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum Style {
#[default]
Flat,
FlatSquare,
ForTheBadge,
}
impl<'a> TryFrom<&'a str> for Style {
type Error = String;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
match value.to_lowercase().replace("-", "").trim() {
"flat" => Ok(Style::Flat),
"flatsquare" => Ok(Style::FlatSquare),
"forthebadge" => Ok(Style::ForTheBadge),
x => Err(format!("unknown badge style '{}'", x)),
}
}
}
impl<'de> Deserialize<'de> for Style {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::try_from(s.as_ref()).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_invalid_color_uses_library_error() {
let error = Color::try_from("not-a-color").unwrap_err();
assert!(matches!(&error, Error::InvalidColor(color) if color == "not-a-color"));
assert_eq!(error.to_string(), "invalid color 'not-a-color'");
}
#[test]
fn test_invalid_direct_hex_falls_back_to_black() {
assert_eq!(Color::Hex("not-a-color".into()).to_hex(), Color::Black.to_hex());
}
#[test]
fn test_builtin_palette() {
for (color, expected) in [
(Color::Blue, "3b82f6"),
(Color::Green, "16a34a"),
(Color::Lime, "65a30d"),
(Color::Yellow, "ca8a04"),
(Color::Orange, "ea580c"),
(Color::Red, "ef4444"),
(Color::Gray, "71717a"),
(Color::Black, "18181b"),
(Color::White, "f4f4f5"),
(Color::Cyan, "0891b2"),
] {
assert_eq!(color.to_hex(), expected);
}
}
#[test]
fn test_bright_green_aliases() {
assert_eq!(Color::try_from("brightgreen").unwrap(), Color::Green);
assert_eq!(Color::try_from("bright-green").unwrap(), Color::Green);
}
#[test]
fn test_for_the_badge_style() {
assert_eq!(Style::try_from("for-the-badge").unwrap(), Style::ForTheBadge);
}
}
#[cfg(feature = "axum")]
#[derive(Debug, Default, Clone, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum Format {
#[default]
Svg,
Json,
}
#[cfg(feature = "axum")]
impl<'a> TryFrom<&'a str> for Format {
type Error = String;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
match value.to_lowercase().trim() {
"svg" => Ok(Format::Svg),
"json" => Ok(Format::Json),
x => Err(format!("unknown badge format '{}'", x)),
}
}
}
#[cfg(feature = "axum")]
impl<'de> Deserialize<'de> for Format {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::try_from(s.as_ref()).map_err(serde::de::Error::custom)
}
}