use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct Rgb {
r: u8,
g: u8,
b: u8,
}
impl Rgb {
pub const WHITE: Self = Self::new(0xff, 0xff, 0xff);
#[must_use]
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
#[must_use]
pub const fn components(self) -> (u8, u8, u8) {
(self.r, self.g, self.b)
}
#[must_use]
pub const fn packed(self) -> u32 {
(self.r as u32) << 16 | (self.g as u32) << 8 | self.b as u32
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("invalid RGB color {input:?}: expected 6 hex digits (\"RRGGBB\", no '#')")]
pub struct RgbParseError {
input: String,
}
impl FromStr for Rgb {
type Err = RgbParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let packed = (s.len() == 6)
.then(|| u32::from_str_radix(s, 16).ok())
.flatten()
.ok_or_else(|| RgbParseError { input: s.into() })?;
Ok(Self::new(
(packed >> 16 & 0xff) as u8,
(packed >> 8 & 0xff) as u8,
(packed & 0xff) as u8,
))
}
}
impl TryFrom<String> for Rgb {
type Error = RgbParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
impl From<Rgb> for String {
fn from(color: Rgb) -> Self {
color.to_string()
}
}
impl fmt::Display for Rgb {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "unwrap is idiomatic in tests")]
mod tests {
use super::Rgb;
#[test]
fn parses_and_round_trips_hex() {
let color: Rgb = "8000ff".parse().unwrap();
assert_eq!(color, Rgb::new(0x80, 0x00, 0xff));
assert_eq!(color.packed(), 0x0080_00ff);
assert_eq!(color.to_string(), "8000ff");
}
#[test]
fn accepts_uppercase_but_prints_lowercase() {
let color: Rgb = "FF3B30".parse().unwrap();
assert_eq!(color.to_string(), "ff3b30");
}
#[test]
fn rejects_wrong_length_prefix_and_non_hex() {
for bad in ["fff", "ff00aa0", "#ff00aa", "red", ""] {
assert!(bad.parse::<Rgb>().is_err(), "{bad:?} should not parse");
}
}
}