rumdl_lib/types/
br_spaces.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::fmt;
3
4/// Number of trailing spaces for Markdown line breaks (≥2)
5///
6/// In Markdown, a line break requires at least 2 trailing spaces. Values of 0 or 1
7/// don't create line breaks and would silently fail. This type enforces that constraint
8/// at deserialization time, preventing broken line break configurations.
9///
10/// CommonMark specification requires exactly 2 spaces, but some flavors allow more.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct BrSpaces(usize);
13
14impl BrSpaces {
15    /// Minimum value for line breaks (CommonMark standard)
16    pub const MIN: usize = 2;
17
18    /// Create a new BrSpaces, validating it's at least 2.
19    ///
20    /// # Errors
21    /// Returns `BrSpacesError` if the value is less than 2.
22    pub fn new(value: usize) -> Result<Self, BrSpacesError> {
23        if value >= Self::MIN {
24            Ok(Self(value))
25        } else {
26            Err(BrSpacesError(value))
27        }
28    }
29
30    /// Get the underlying value (guaranteed to be ≥2).
31    pub fn get(self) -> usize {
32        self.0
33    }
34
35    /// Convert from a default value (for use in config defaults).
36    ///
37    /// # Panics
38    /// Panics if the value is less than 2. This is intended for const defaults only.
39    pub const fn from_const(value: usize) -> Self {
40        assert!(value >= Self::MIN, "BrSpaces must be at least 2 (CommonMark standard)");
41        Self(value)
42    }
43}
44
45impl Default for BrSpaces {
46    fn default() -> Self {
47        Self(2) // Safe: 2 is the CommonMark standard
48    }
49}
50
51/// Error type for invalid BrSpaces values.
52#[derive(Debug, Clone, Copy)]
53pub struct BrSpacesError(usize);
54
55impl fmt::Display for BrSpacesError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(
58            f,
59            "Line break spaces must be at least 2, got {}. \
60             Markdown requires at least 2 trailing spaces to create a line break \
61             (CommonMark specification). Values of 0 or 1 do not create line breaks.",
62            self.0
63        )
64    }
65}
66
67impl std::error::Error for BrSpacesError {}
68
69impl<'de> Deserialize<'de> for BrSpaces {
70    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71    where
72        D: Deserializer<'de>,
73    {
74        let value = usize::deserialize(deserializer)?;
75        BrSpaces::new(value).map_err(serde::de::Error::custom)
76    }
77}
78
79impl Serialize for BrSpaces {
80    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
81    where
82        S: Serializer,
83    {
84        self.0.serialize(serializer)
85    }
86}
87
88impl From<BrSpaces> for usize {
89    fn from(val: BrSpaces) -> Self {
90        val.0
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_valid_values() {
100        for value in [2, 3, 4, 10, 100] {
101            let br_spaces = BrSpaces::new(value).unwrap();
102            assert_eq!(br_spaces.get(), value);
103            assert_eq!(usize::from(br_spaces), value);
104        }
105    }
106
107    #[test]
108    fn test_invalid_values() {
109        for value in [0, 1] {
110            assert!(BrSpaces::new(value).is_err());
111        }
112    }
113
114    #[test]
115    fn test_default() {
116        assert_eq!(BrSpaces::default().get(), 2);
117    }
118
119    #[test]
120    fn test_from_const() {
121        const DEFAULT: BrSpaces = BrSpaces::from_const(2);
122        assert_eq!(DEFAULT.get(), 2);
123    }
124
125    #[test]
126    fn test_min_constant() {
127        assert_eq!(BrSpaces::MIN, 2);
128    }
129
130    #[test]
131    fn test_roundtrip() {
132        #[derive(serde::Serialize, serde::Deserialize)]
133        struct TestConfig {
134            spaces: BrSpaces,
135        }
136
137        let config = TestConfig {
138            spaces: BrSpaces::new(3).unwrap(),
139        };
140        let serialized = toml::to_string(&config).unwrap();
141        let deserialized: TestConfig = toml::from_str(&serialized).unwrap();
142        assert_eq!(deserialized.spaces.get(), 3);
143    }
144
145    #[test]
146    fn test_validation_error() {
147        #[derive(Debug, serde::Deserialize)]
148        struct TestConfig {
149            spaces: BrSpaces,
150        }
151
152        // Test value below minimum
153        let toml_str = "spaces = 1";
154        let result: Result<TestConfig, _> = toml::from_str(toml_str);
155        assert!(result.is_err());
156        let err = result.unwrap_err().to_string();
157        assert!(err.contains("must be at least 2") || err.contains("got 1"));
158
159        // Test zero
160        let toml_str = "spaces = 0";
161        let result: Result<TestConfig, _> = toml::from_str(toml_str);
162        assert!(result.is_err());
163
164        // Test valid value works
165        let valid_toml = "spaces = 2";
166        let config: TestConfig = toml::from_str(valid_toml).unwrap();
167        assert_eq!(config.spaces.get(), 2);
168    }
169}