norad/
shared_types.rs

1use std::str::FromStr;
2
3use serde::de::Deserializer;
4use serde::ser::Serializer;
5use serde::{Deserialize, Serialize};
6
7pub static PUBLIC_OBJECT_LIBS_KEY: &str = "public.objectLibs";
8
9/// A Plist dictionary.
10pub type Plist = plist::Dictionary;
11
12/// A color in RGBA (Red-Green-Blue-Alpha) format.
13///
14/// See <https://unifiedfontobject.org/versions/ufo3/conventions/#colors>.
15#[derive(Debug, Clone, PartialEq, Copy)]
16pub struct Color {
17    /// Red channel value. Must be in the range 0 to 1, inclusive.
18    red: f64,
19    /// Green channel value. Must be in the range 0 to 1, inclusive.
20    green: f64,
21    /// Blue channel value. Must be in the range 0 to 1, inclusive.
22    blue: f64,
23    /// Alpha (transparency) channel value. Must be in the range 0 to 1, inclusive.
24    alpha: f64,
25}
26
27impl Color {
28    /// Create a color with RGBA values in the range `0..=1.0`.
29    ///
30    /// Returns an error if any of the provided values are not in the allowed range.
31    pub fn new(red: f64, green: f64, blue: f64, alpha: f64) -> Result<Self, ColorError> {
32        if [red, green, blue, alpha].iter().all(|v| (0.0..=1.0).contains(v)) {
33            Ok(Self { red, green, blue, alpha })
34        } else {
35            Err(ColorError::Value)
36        }
37    }
38
39    /// Returns the RGBA channel values.
40    pub fn channels(&self) -> (f64, f64, f64, f64) {
41        (self.red, self.green, self.blue, self.alpha)
42    }
43}
44
45/// An error representing an invalid [`Color`] string.
46///
47/// [`Color`]: crate::Color
48#[derive(Debug, thiserror::Error)]
49pub enum ColorError {
50    /// The color string was malformed.
51    #[error("failed to parse color string '{0}'")]
52    Parse(String),
53    /// A channel value was not between 0 and 1, inclusive.
54    #[error("color channel values must be between 0 and 1, inclusive")]
55    Value,
56}
57
58impl FromStr for Color {
59    type Err = ColorError;
60
61    fn from_str(s: &str) -> Result<Self, Self::Err> {
62        let mut iter =
63            s.split(',').map(|v| v.parse::<f64>().map_err(|_| ColorError::Parse(s.to_owned())));
64        let red = iter.next().unwrap_or_else(|| Err(ColorError::Parse(s.to_owned())))?;
65        let green = iter.next().unwrap_or_else(|| Err(ColorError::Parse(s.to_owned())))?;
66        let blue = iter.next().unwrap_or_else(|| Err(ColorError::Parse(s.to_owned())))?;
67        let alpha = iter.next().unwrap_or_else(|| Err(ColorError::Parse(s.to_owned())))?;
68        if iter.next().is_some() {
69            Err(ColorError::Parse(s.to_owned()))
70        } else {
71            Color::new(red, green, blue, alpha)
72        }
73    }
74}
75
76impl Serialize for Color {
77    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
78    where
79        S: Serializer,
80    {
81        let color_string = self.to_rgba_string();
82        serializer.serialize_str(&color_string)
83    }
84}
85
86impl<'de> Deserialize<'de> for Color {
87    fn deserialize<D>(deserializer: D) -> Result<Color, D::Error>
88    where
89        D: Deserializer<'de>,
90    {
91        let string = String::deserialize(deserializer)?;
92        Color::from_str(&string).map_err(serde::de::Error::custom)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use serde_test::{assert_de_tokens, assert_ser_tokens, assert_tokens, Token};
99
100    use super::*;
101
102    #[test]
103    fn color_parsing() {
104        let c1 = Color { red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0 };
105        assert_tokens(&c1, &[Token::Str("1,0,0,1")]);
106
107        let c2 = Color { red: 0.0, green: 0.5, blue: 0.0, alpha: 0.5 };
108        assert_tokens(&c2, &[Token::Str("0,0.5,0,0.5")]);
109
110        let c3 = Color { red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0 };
111        assert_tokens(&c3, &[Token::Str("0,0,0,0")]);
112
113        let c4 = Color { red: 0.123, green: 0.456, blue: 0.789, alpha: 0.159 };
114        assert_tokens(&c4, &[Token::Str("0.123,0.456,0.789,0.159")]);
115
116        #[allow(clippy::excessive_precision)]
117        let c5 = Color { red: 0.123456789, green: 0.456789123, blue: 0.789123456, alpha: 0.1 };
118        assert_ser_tokens(&c5, &[Token::Str("0.123,0.457,0.789,0.1")]);
119
120        #[allow(clippy::excessive_precision)]
121        let c6 = Color { red: 0.123456789, green: 0.456789123, blue: 0.789123456, alpha: 0.1 };
122        assert_de_tokens(&c6, &[Token::Str("0.123456789,0.456789123,0.789123456,0.1")]);
123    }
124}