use std::{fmt, str::FromStr};
use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::{self, Visitor},
};
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Color(pub u8, pub u8, pub u8);
#[derive(
Clone,
Copy,
Debug,
Deserialize,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
Serialize,
)]
pub struct RgbaColor(pub u8, pub u8, pub u8, pub u8);
impl Color {
pub const fn new(red: u8, green: u8, blue: u8) -> Self {
Self(red, green, blue)
}
pub const fn red(self) -> u8 {
self.0
}
pub const fn green(self) -> u8 {
self.1
}
pub const fn blue(self) -> u8 {
self.2
}
}
impl RgbaColor {
pub const fn new(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
Self(red, green, blue, alpha)
}
pub const fn red(self) -> u8 {
self.0
}
pub const fn green(self) -> u8 {
self.1
}
pub const fn blue(self) -> u8 {
self.2
}
pub const fn alpha(self) -> u8 {
self.3
}
}
impl From<[u8; 4]> for RgbaColor {
fn from(value: [u8; 4]) -> Self {
Self(value[0], value[1], value[2], value[3])
}
}
impl From<RgbaColor> for [u8; 4] {
fn from(value: RgbaColor) -> Self {
[value.0, value.1, value.2, value.3]
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:02x}{:02x}{:02x}", self.0, self.1, self.2)
}
}
impl FromStr for Color {
type Err = ParseColorError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = s.as_bytes();
if bytes.len() != 6 {
return Err(ParseColorError);
}
let red = hex_byte(bytes[0], bytes[1])?;
let green = hex_byte(bytes[2], bytes[3])?;
let blue = hex_byte(bytes[4], bytes[5])?;
Ok(Self(red, green, blue))
}
}
fn hex_byte(high: u8, low: u8) -> Result<u8, ParseColorError> {
Ok((hex_nibble(high)? << 4) | hex_nibble(low)?)
}
fn hex_nibble(byte: u8) -> Result<u8, ParseColorError> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
b'A'..=b'F' => Ok(byte - b'A' + 10),
_ => Err(ParseColorError),
}
}
impl Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Color {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(ColorVisitor)
}
}
struct ColorVisitor;
impl Visitor<'_> for ColorVisitor {
type Value = Color;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(
"a six-digit hexadecimal RGB color without a leading hash",
)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
value
.parse()
.map_err(|_| E::invalid_value(de::Unexpected::Str(value), &self))
}
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
#[error("expected a six-digit hexadecimal RGB color without a leading hash")]
pub struct ParseColorError;
#[cfg(test)]
mod tests {
use std::assert_matches;
use crate::model::color::{Color, ParseColorError, RgbaColor};
#[test]
fn deserializes_lowercase_hex_without_hash() {
let color: Color = serde_json::from_str("\"1a2b3c\"").unwrap();
assert_eq!(color, Color::new(0x1a, 0x2b, 0x3c));
}
#[test]
fn deserializes_uppercase_hex_without_hash() {
let color: Color = serde_json::from_str("\"AABBCC\"").unwrap();
assert_eq!(color, Color::new(0xaa, 0xbb, 0xcc));
}
#[test]
fn serializes_as_lowercase_hex_without_hash() {
let serialized =
serde_json::to_string(&Color::new(0xaa, 0xbb, 0xcc)).unwrap();
assert_eq!(serialized, "\"aabbcc\"");
}
#[test]
fn rejects_hash_prefixed_hex() {
assert_matches!("#aabbcc".parse::<Color>(), Err(ParseColorError));
}
#[test]
fn rejects_invalid_hex_digits() {
assert_matches!("nothex".parse::<Color>(), Err(ParseColorError));
}
#[test]
fn rejects_non_ascii_six_byte_input() {
assert_matches!("ééé".parse::<Color>(), Err(ParseColorError));
}
#[test]
fn deserializes_rgba_array() {
let color: RgbaColor = serde_json::from_str("[1,2,3,4]").unwrap();
assert_eq!(color, RgbaColor::new(1, 2, 3, 4));
}
#[test]
fn serializes_rgba_array() {
let serialized =
serde_json::to_string(&RgbaColor::new(1, 2, 3, 4)).unwrap();
assert_eq!(serialized, "[1,2,3,4]");
}
#[test]
fn rejects_rgba_array_with_wrong_length() {
assert!(serde_json::from_str::<RgbaColor>("[1,2,3]").is_err());
}
}