earthmc 2.0.5

Async client for interacting with the EarthMC API
Documentation
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()
        );
    }
}