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}