use schemars::schema::Schema;
use schemars::{JsonSchema, SchemaGenerator};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Color {
#[must_use]
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
#[must_use]
pub const fn to_colorref(self) -> u32 {
(self.r as u32) | ((self.g as u32) << 8) | ((self.b as u32) << 16)
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
}
}
impl FromStr for Color {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let hex = s
.strip_prefix('#')
.ok_or_else(|| format!("color must start with '#': got {s:?}"))?;
if hex.len() != 6 {
return Err(format!(
"color must be 6 hex digits (#RRGGBB): got {s:?} ({} chars after '#')",
hex.len()
));
}
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|e| format!("invalid red channel in {s:?}: {e}"))?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|e| format!("invalid green channel in {s:?}: {e}"))?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|e| format!("invalid blue channel in {s:?}: {e}"))?;
Ok(Self::rgb(r, g, b))
}
}
impl Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for Color {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
impl JsonSchema for Color {
fn schema_name() -> String {
"Color".to_string()
}
fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
serde_json::from_value(serde_json::json!({
"type": "string",
"pattern": "^#[0-9A-Fa-f]{6}$",
"format": "hex-color",
"description": "RGB hex color, e.g. \"#FF8800\"."
}))
.expect("color schema is a valid JSON Schema object")
}
fn is_referenceable() -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_uppercase_hex() {
assert_eq!(
"#FF8800".parse::<Color>().unwrap(),
Color::rgb(0xFF, 0x88, 0x00)
);
}
#[test]
fn parse_lowercase_hex() {
assert_eq!(
"#ff8800".parse::<Color>().unwrap(),
Color::rgb(0xFF, 0x88, 0x00)
);
}
#[test]
fn parse_rejects_missing_hash() {
assert!("FF8800".parse::<Color>().is_err());
}
#[test]
fn parse_rejects_short_string() {
assert!("#FF88".parse::<Color>().is_err());
}
#[test]
fn parse_rejects_non_hex_digit() {
assert!("#GG8800".parse::<Color>().is_err());
}
#[test]
fn display_emits_uppercase_with_hash() {
assert_eq!(Color::rgb(0xFF, 0x88, 0x00).to_string(), "#FF8800");
}
#[test]
fn to_colorref_packs_bgr() {
assert_eq!(Color::rgb(0xFF, 0x88, 0x00).to_colorref(), 0x00_00_88_FF);
}
#[test]
fn roundtrips_through_toml() {
let original = Color::rgb(0x12, 0x34, 0x56);
let s = toml::to_string(&BorrowedColor { color: original }).unwrap();
let parsed: BorrowedColor = toml::from_str(&s).unwrap();
assert_eq!(parsed.color, original);
}
#[test]
fn serde_rejects_invalid_string() {
let bad = toml::from_str::<BorrowedColor>("color = \"magenta\"\n");
assert!(bad.is_err());
}
#[derive(Debug, Serialize, Deserialize)]
struct BorrowedColor {
color: Color,
}
}