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]
}
}
pub mod rgba_hex {
use super::{RgbaColor, fmt};
use serde::{
Deserializer, Serializer,
de::{self, Visitor},
};
pub fn serialize<S>(
color: &RgbaColor,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&format!(
"{:02x}{:02x}{:02x}{:02x}",
color.red(),
color.green(),
color.blue(),
color.alpha()
))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<RgbaColor, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(RgbaHexVisitor)
}
struct RgbaHexVisitor;
impl Visitor<'_> for RgbaHexVisitor {
type Value = RgbaColor;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(
"up to six hexadecimal RGB digits or eight hexadecimal RGBA digits without a leading hash",
)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
parse_rgba_hex(value).map_err(|_| {
E::invalid_value(de::Unexpected::Str(value), &self)
})
}
}
fn parse_rgba_hex(value: &str) -> Result<RgbaColor, ()> {
let color = u32::from_str_radix(value, 16).map_err(|_| ())?;
match value.len() {
1..=6 => Ok(RgbaColor(
((color >> 16) & 0xff) as u8,
((color >> 8) & 0xff) as u8,
(color & 0xff) as u8,
0xff,
)),
7 if color <= 0x00ff_ffff => Ok(RgbaColor(
((color >> 16) & 0xff) as u8,
((color >> 8) & 0xff) as u8,
(color & 0xff) as u8,
0xff,
)),
8 => Ok(RgbaColor(
((color >> 24) & 0xff) as u8,
((color >> 16) & 0xff) as u8,
((color >> 8) & 0xff) as u8,
(color & 0xff) as u8,
)),
_ => return Err(()),
}
}
}
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 serde::{Deserialize, Serialize};
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());
}
#[derive(Deserialize, Serialize)]
struct RgbaHexFixture {
#[serde(with = "crate::model::color::rgba_hex")]
color: RgbaColor,
}
#[test]
fn deserializes_rgba_hex_without_alpha_as_opaque() {
let fixture: RgbaHexFixture =
serde_json::from_str(r#"{"color":"07ED8D"}"#).unwrap();
assert_eq!(fixture.color, RgbaColor::new(0x07, 0xed, 0x8d, 0xff));
}
#[test]
fn deserializes_rgba_hex_with_extra_leading_zero_as_opaque_rgb() {
let fixture: RgbaHexFixture =
serde_json::from_str(r#"{"color":"000008B"}"#).unwrap();
assert_eq!(fixture.color, RgbaColor::new(0x00, 0x00, 0x8b, 0xff));
}
#[test]
fn deserializes_short_rgba_hex_as_left_padded_opaque_rgb() {
let fixture: RgbaHexFixture =
serde_json::from_str(r#"{"color":"04043"}"#).unwrap();
assert_eq!(fixture.color, RgbaColor::new(0x00, 0x40, 0x43, 0xff));
}
#[test]
fn deserializes_rgba_hex_with_alpha() {
let fixture: RgbaHexFixture =
serde_json::from_str(r#"{"color":"11223344"}"#).unwrap();
assert_eq!(fixture.color, RgbaColor::new(0x11, 0x22, 0x33, 0x44));
}
#[test]
fn serializes_rgba_hex_with_alpha() {
let serialized = serde_json::to_string(&RgbaHexFixture {
color: RgbaColor::new(0x11, 0x22, 0x33, 0x44),
})
.unwrap();
assert_eq!(serialized, r#"{"color":"11223344"}"#);
}
#[test]
fn rejects_hash_prefixed_rgba_hex() {
assert!(
serde_json::from_str::<RgbaHexFixture>(r##"{"color":"#07ED8D"}"##)
.is_err()
);
}
}