Skip to main content

serde_saphyr/de/
indentation.rs

1/// Requirements for indentation validation during YAML deserialization.
2///
3/// When set via [`options!`](crate::options!), the deserializer validates every
4/// parser-reported indentation level against the chosen policy.
5///
6/// # Variants
7///
8/// | Variant | Accepts |
9/// |---------|---------|
10/// | [`Unchecked`](Self::Unchecked) | Any indentation (default) |
11/// | [`Divisible(n)`](Self::Divisible) | Indentation divisible by `n` |
12/// | [`Even`](Self::Even) | Even indentation (0, 2, 4, …) |
13/// | [`Uniform`](Self::Uniform) | Consistent indentation throughout the document |
14///
15/// *Important:* if there are multiple documents in the YAML input, or added with !include,
16/// indentation requirements apply to all of them. For Uniform, indentation is required to be
17/// consistent across all included documents as well.
18///
19/// # Examples
20///
21/// ## Enforcing even indentation
22///
23/// ```rust
24/// use serde_json::Value;
25///
26/// // This YAML uses 2-space indentation — accepted by `Even`.
27/// let yaml = r#"
28/// server:
29///   host: localhost
30///   port: 8080
31/// "#;
32/// let options = serde_saphyr::options! {
33///     require_indent: serde_saphyr::RequireIndent::Even,
34/// };
35/// let result = serde_saphyr::from_str_with_options::<Value>(yaml, options);
36/// assert!(result.is_ok());
37/// ```
38///
39/// ```rust
40/// use serde_json::Value;
41///
42/// // This YAML uses 3-space indentation — rejected by `Even`.
43/// let yaml = r#"
44/// server:
45///    host: localhost
46/// "#;
47/// let options = serde_saphyr::options! {
48///     require_indent: serde_saphyr::RequireIndent::Even,
49/// };
50/// let result = serde_saphyr::from_str_with_options::<Value>(yaml, options);
51/// assert!(result.is_err());
52/// ```
53///
54/// ## Enforcing indentation divisible by 4
55///
56/// ```rust
57/// use serde_json::Value;
58///
59/// // 4-space indentation — accepted by `Divisible(4)`.
60/// let yaml = r#"
61/// database:
62///     host: db.local
63///     port: 5432
64/// "#;
65/// let options = serde_saphyr::options! {
66///     require_indent: serde_saphyr::RequireIndent::Divisible(4),
67/// };
68/// assert!(serde_saphyr::from_str_with_options::<Value>(yaml, options).is_ok());
69/// ```
70///
71/// ## Enforcing uniform indentation
72///
73/// ```rust
74/// use serde_json::Value;
75///
76/// // Consistent 2-space indentation throughout.
77/// let yaml = r#"
78/// app:
79///   name: demo
80///   version: 1
81/// "#;
82/// let options = serde_saphyr::options! {
83///     require_indent: serde_saphyr::RequireIndent::Uniform(None),
84/// };
85/// assert!(serde_saphyr::from_str_with_options::<Value>(yaml, options).is_ok());
86/// ```
87///
88/// ## YAML sample that triggers an indentation error
89///
90/// ```yaml
91/// # With RequireIndent::Even, this document would fail:
92/// config:
93///    debug: true    # 3-space indent — odd, rejected
94/// ```
95#[non_exhaustive]
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97#[cfg_attr(
98    feature = "serde_derived_types",
99    derive(serde::Serialize, serde::Deserialize)
100)]
101pub enum RequireIndent {
102    /// No indentation checking is performed.
103    Unchecked,
104    /// Indentation must be divisible by `n`.
105    Divisible(usize),
106    /// Indentation must be even.
107    Even,
108    /// Indentation must be uniform throughout the document.
109    Uniform(Option<usize>),
110}
111
112impl RequireIndent {
113    pub(crate) fn validate(&self) -> Result<(), crate::de_error::Error> {
114        if let RequireIndent::Divisible(0) = self {
115            return Err(divisible_zero_error());
116        }
117        Ok(())
118    }
119
120    /// Checks whether the given indentation `n` is valid with respect to this requirement.
121    ///
122    /// For [`Uniform`](RequireIndent::Uniform), the first non-zero indentation encountered is
123    /// remembered, and subsequent calls compare against that stored value.
124    ///
125    /// Returns `Ok(())` if valid, or an [`Error::IndentationError`](crate::Error::IndentationError)
126    /// with [`Location::UNKNOWN`](crate::Location). Deserialization entry points attach the
127    /// source location before returning this error.
128    pub fn is_valid(&mut self, n: usize) -> Result<(), crate::de_error::Error> {
129        let ok = match self {
130            RequireIndent::Unchecked => true,
131            RequireIndent::Divisible(0) => return Err(divisible_zero_error()),
132            RequireIndent::Divisible(d) => n.is_multiple_of(*d),
133            RequireIndent::Even => n.is_multiple_of(2),
134            RequireIndent::Uniform(remembered) => {
135                if n == 0 {
136                    return Ok(());
137                }
138                match *remembered {
139                    None => {
140                        *remembered = Some(n);
141                        true
142                    }
143                    Some(expected) => n.is_multiple_of(expected),
144                }
145            }
146        };
147        if ok {
148            Ok(())
149        } else {
150            Err(crate::de_error::Error::IndentationError {
151                required: *self,
152                actual: n,
153                location: crate::location::Location::UNKNOWN,
154            })
155        }
156    }
157}
158
159fn divisible_zero_error() -> crate::de_error::Error {
160    crate::de_error::Error::invalid_options(
161        "invalid deserialization options: require_indent Divisible(0) is not allowed; \
162         indentation divisor must be non-zero",
163    )
164}
165
166impl std::fmt::Display for RequireIndent {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self {
169            RequireIndent::Unchecked => write!(f, "unchecked"),
170            RequireIndent::Divisible(n) => write!(f, "divisible by {n}"),
171            RequireIndent::Even => write!(f, "even"),
172            RequireIndent::Uniform(Some(n)) => write!(f, "uniform ({n} spaces)"),
173            RequireIndent::Uniform(None) => write!(f, "uniform"),
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    // --- is_valid: Unchecked ---
183
184    #[test]
185    fn unchecked_always_valid() {
186        let mut r = RequireIndent::Unchecked;
187        r.is_valid(0).unwrap();
188        r.is_valid(1).unwrap();
189        r.is_valid(7).unwrap();
190        r.is_valid(100).unwrap();
191    }
192
193    // --- is_valid: Divisible ---
194
195    #[test]
196    fn divisible_by_4() {
197        let mut r = RequireIndent::Divisible(4);
198        r.is_valid(0).unwrap();
199        r.is_valid(4).unwrap();
200        r.is_valid(8).unwrap();
201        r.is_valid(1).unwrap_err();
202        r.is_valid(3).unwrap_err();
203        r.is_valid(5).unwrap_err();
204    }
205
206    #[test]
207    fn divisible_by_1() {
208        let mut r = RequireIndent::Divisible(1);
209        r.is_valid(0).unwrap();
210        r.is_valid(1).unwrap();
211        r.is_valid(999).unwrap();
212    }
213
214    // --- is_valid: Even ---
215
216    #[test]
217    fn even_accepts_even_numbers() {
218        let mut r = RequireIndent::Even;
219        r.is_valid(0).unwrap();
220        r.is_valid(2).unwrap();
221        r.is_valid(4).unwrap();
222        r.is_valid(100).unwrap();
223    }
224
225    #[test]
226    fn even_rejects_odd_numbers() {
227        let mut r = RequireIndent::Even;
228        r.is_valid(1).unwrap_err();
229        r.is_valid(3).unwrap_err();
230        r.is_valid(99).unwrap_err();
231    }
232
233    // --- is_valid: Uniform ---
234
235    #[test]
236    fn uniform_remembers_first_nonzero() {
237        let mut r = RequireIndent::Uniform(None);
238        r.is_valid(0).unwrap(); // zero always passes, doesn't set
239        assert_eq!(r, RequireIndent::Uniform(None));
240        r.is_valid(4).unwrap(); // sets remembered to 4
241        assert_eq!(r, RequireIndent::Uniform(Some(4)));
242    }
243
244    #[test]
245    fn uniform_accepts_multiples_of_remembered() {
246        let mut r = RequireIndent::Uniform(Some(2));
247        r.is_valid(0).unwrap();
248        r.is_valid(2).unwrap();
249        r.is_valid(4).unwrap();
250        r.is_valid(6).unwrap();
251    }
252
253    #[test]
254    fn uniform_rejects_non_multiples() {
255        let mut r = RequireIndent::Uniform(Some(4));
256        r.is_valid(3).unwrap_err();
257        r.is_valid(5).unwrap_err();
258        r.is_valid(7).unwrap_err();
259    }
260
261    #[test]
262    fn uniform_zero_always_valid_even_after_set() {
263        let mut r = RequireIndent::Uniform(Some(3));
264        r.is_valid(0).unwrap();
265    }
266
267    // --- Display ---
268
269    #[test]
270    fn display_unchecked() {
271        assert_eq!(RequireIndent::Unchecked.to_string(), "unchecked");
272    }
273
274    #[test]
275    fn display_divisible() {
276        assert_eq!(RequireIndent::Divisible(4).to_string(), "divisible by 4");
277    }
278
279    #[test]
280    fn display_even() {
281        assert_eq!(RequireIndent::Even.to_string(), "even");
282    }
283
284    #[test]
285    fn display_uniform_none() {
286        assert_eq!(RequireIndent::Uniform(None).to_string(), "uniform");
287    }
288
289    #[test]
290    fn display_uniform_some() {
291        assert_eq!(
292            RequireIndent::Uniform(Some(2)).to_string(),
293            "uniform (2 spaces)"
294        );
295    }
296}