clawspec_core/client/response/
status.rs

1use std::ops::{Range, RangeInclusive};
2
3/// Expected status codes for HTTP requests.
4///
5/// Supports multiple ranges and individual status codes for flexible validation.
6#[derive(Debug, Clone)]
7pub struct ExpectedStatusCodes {
8    ranges: Vec<StatusCodeRange>,
9}
10
11/// Represents a range of status codes (inclusive or exclusive).
12#[derive(Debug, Clone)]
13enum StatusCodeRange {
14    Single(u16),
15    Inclusive(RangeInclusive<u16>),
16    Exclusive(Range<u16>),
17}
18
19impl ExpectedStatusCodes {
20    /// Creates a new set of expected status codes with default range (200..500).
21    pub fn new() -> Self {
22        Self {
23            ranges: vec![StatusCodeRange::Exclusive(200..500)],
24        }
25    }
26
27    /// Adds a single status code as valid.
28    pub fn add_single(mut self, status: u16) -> Self {
29        self.ranges.push(StatusCodeRange::Single(status));
30        self
31    }
32
33    /// Adds an inclusive range of status codes.
34    pub fn add_inclusive_range(mut self, range: RangeInclusive<u16>) -> Self {
35        self.ranges.push(StatusCodeRange::Inclusive(range));
36        self
37    }
38
39    /// Adds an exclusive range of status codes.
40    pub fn add_exclusive_range(mut self, range: Range<u16>) -> Self {
41        self.ranges.push(StatusCodeRange::Exclusive(range));
42        self
43    }
44
45    /// Creates expected status codes from a single inclusive range.
46    ///
47    /// # Panics
48    ///
49    /// Panics if the range contains invalid HTTP status codes (outside 100-599).
50    pub fn from_inclusive_range(range: RangeInclusive<u16>) -> Self {
51        assert!(
52            *range.start() >= 100 && *range.start() <= 599,
53            "HTTP status code range start must be between 100 and 599, got {}",
54            range.start()
55        );
56        assert!(
57            *range.end() >= 100 && *range.end() <= 599,
58            "HTTP status code range end must be between 100 and 599, got {}",
59            range.end()
60        );
61        assert!(
62            range.start() <= range.end(),
63            "HTTP status code range start ({}) must be less than or equal to end ({})",
64            range.start(),
65            range.end()
66        );
67
68        Self {
69            ranges: vec![StatusCodeRange::Inclusive(range)],
70        }
71    }
72
73    /// Creates expected status codes from a single exclusive range.
74    ///
75    /// # Panics
76    ///
77    /// Panics if the range contains invalid HTTP status codes (outside 100-599).
78    pub fn from_exclusive_range(range: Range<u16>) -> Self {
79        assert!(
80            range.start >= 100 && range.start <= 599,
81            "HTTP status code range start must be between 100 and 599, got {}",
82            range.start
83        );
84        assert!(
85            range.end >= 100 && range.end <= 600, // exclusive end can be 600
86            "HTTP status code range end must be between 100 and 600 (exclusive), got {}",
87            range.end
88        );
89        assert!(
90            range.start < range.end,
91            "HTTP status code range start ({}) must be less than end ({})",
92            range.start,
93            range.end
94        );
95
96        Self {
97            ranges: vec![StatusCodeRange::Exclusive(range)],
98        }
99    }
100
101    /// Creates expected status codes from a single status code.
102    ///
103    /// # Panics
104    ///
105    /// Panics if the status code is invalid (outside 100-599).
106    pub fn from_single(status: u16) -> Self {
107        assert!(
108            (100..=599).contains(&status),
109            "HTTP status code must be between 100 and 599, got {status}"
110        );
111
112        Self {
113            ranges: vec![StatusCodeRange::Single(status)],
114        }
115    }
116
117    /// Creates expected status codes from a single `http::StatusCode`.
118    ///
119    /// This method provides **compile-time validation** of status codes through the type system.
120    /// Unlike the `u16` variants, this method does not perform runtime validation since
121    /// `http::StatusCode` guarantees valid HTTP status codes at compile time.
122    ///
123    /// # Example
124    ///
125    /// ```rust
126    /// use clawspec_core::ExpectedStatusCodes;
127    /// use http::StatusCode;
128    ///
129    /// let codes = ExpectedStatusCodes::from_status_code(StatusCode::OK);
130    /// assert!(codes.contains(200));
131    /// ```
132    pub fn from_status_code(status: http::StatusCode) -> Self {
133        // No runtime validation needed - http::StatusCode guarantees validity at compile time
134        Self {
135            ranges: vec![StatusCodeRange::Single(status.as_u16())],
136        }
137    }
138
139    /// Creates expected status codes from an inclusive range of `http::StatusCode`.
140    ///
141    /// This method provides **compile-time validation** of status codes through the type system.
142    /// Unlike the `u16` variants, this method does not perform runtime validation since
143    /// `http::StatusCode` guarantees valid HTTP status codes at compile time.
144    ///
145    /// # Example
146    ///
147    /// ```rust
148    /// use clawspec_core::ExpectedStatusCodes;
149    /// use http::StatusCode;
150    ///
151    /// let codes = ExpectedStatusCodes::from_status_code_range_inclusive(
152    ///     StatusCode::OK..=StatusCode::NO_CONTENT
153    /// );
154    /// assert!(codes.contains(200));
155    /// assert!(codes.contains(204));
156    /// assert!(!codes.contains(205));
157    /// ```
158    pub fn from_status_code_range_inclusive(range: RangeInclusive<http::StatusCode>) -> Self {
159        // No runtime validation needed - http::StatusCode guarantees validity at compile time
160        let start = range.start().as_u16();
161        let end = range.end().as_u16();
162        Self {
163            ranges: vec![StatusCodeRange::Inclusive(start..=end)],
164        }
165    }
166
167    /// Creates expected status codes from an exclusive range of `http::StatusCode`.
168    ///
169    /// This method provides **compile-time validation** of status codes through the type system.
170    /// Unlike the `u16` variants, this method does not perform runtime validation since
171    /// `http::StatusCode` guarantees valid HTTP status codes at compile time.
172    ///
173    /// # Example
174    ///
175    /// ```rust
176    /// use clawspec_core::ExpectedStatusCodes;
177    /// use http::StatusCode;
178    ///
179    /// let codes = ExpectedStatusCodes::from_status_code_range_exclusive(
180    ///     StatusCode::OK..StatusCode::PARTIAL_CONTENT
181    /// );
182    /// assert!(codes.contains(200));
183    /// assert!(codes.contains(204));
184    /// assert!(!codes.contains(206));
185    /// ```
186    pub fn from_status_code_range_exclusive(range: Range<http::StatusCode>) -> Self {
187        // No runtime validation needed - http::StatusCode guarantees validity at compile time
188        let start = range.start.as_u16();
189        let end = range.end.as_u16();
190        Self {
191            ranges: vec![StatusCodeRange::Exclusive(start..end)],
192        }
193    }
194
195    /// Checks if a status code is expected/valid.
196    pub fn contains(&self, status: u16) -> bool {
197        self.ranges.iter().any(|range| match range {
198            StatusCodeRange::Single(s) => *s == status,
199            StatusCodeRange::Inclusive(r) => r.contains(&status),
200            StatusCodeRange::Exclusive(r) => r.contains(&status),
201        })
202    }
203
204    /// Checks if an `http::StatusCode` is expected/valid.
205    ///
206    /// This is a convenience method that accepts `http::StatusCode` directly.
207    ///
208    /// # Example
209    ///
210    /// ```rust
211    /// use clawspec_core::ExpectedStatusCodes;
212    /// use http::StatusCode;
213    ///
214    /// let codes = ExpectedStatusCodes::from_status_code(StatusCode::OK);
215    /// assert!(codes.contains_status_code(StatusCode::OK));
216    /// assert!(!codes.contains_status_code(StatusCode::NOT_FOUND));
217    /// ```
218    pub fn contains_status_code(&self, status: http::StatusCode) -> bool {
219        self.contains(status.as_u16())
220    }
221
222    /// Adds a single expected status code (method used by ApiCall).
223    pub fn add_expected_status(mut self, status: u16) -> Self {
224        self.ranges.push(StatusCodeRange::Single(status));
225        self
226    }
227
228    /// Adds an expected inclusive range of status codes (method used by ApiCall).
229    pub fn add_expected_range(mut self, range: RangeInclusive<u16>) -> Self {
230        self.ranges.push(StatusCodeRange::Inclusive(range));
231        self
232    }
233}
234
235impl Default for ExpectedStatusCodes {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241#[cfg(test)]
242mod status_code_tests {
243    use super::*;
244    use http::StatusCode;
245
246    #[test]
247    fn test_default_status_codes() {
248        let codes = ExpectedStatusCodes::default();
249        assert!(codes.contains(200));
250        assert!(codes.contains(299));
251        assert!(codes.contains(404));
252        assert!(codes.contains(499));
253        assert!(!codes.contains(500));
254        assert!(!codes.contains(199));
255    }
256
257    #[test]
258    fn test_single_status_code() {
259        let codes = ExpectedStatusCodes::from_single(200);
260        assert!(codes.contains(200));
261        assert!(!codes.contains(201));
262        assert!(!codes.contains(404));
263    }
264
265    #[test]
266    fn test_inclusive_range() {
267        let codes = ExpectedStatusCodes::from_inclusive_range(200..=204);
268        assert!(codes.contains(200));
269        assert!(codes.contains(202));
270        assert!(codes.contains(204));
271        assert!(!codes.contains(199));
272        assert!(!codes.contains(205));
273    }
274
275    #[test]
276    fn test_exclusive_range() {
277        let codes = ExpectedStatusCodes::from_exclusive_range(200..205);
278        assert!(codes.contains(200));
279        assert!(codes.contains(202));
280        assert!(codes.contains(204));
281        assert!(!codes.contains(199));
282        assert!(!codes.contains(205));
283    }
284
285    #[test]
286    fn test_multiple_ranges() {
287        let codes = ExpectedStatusCodes::default()
288            .add_single(201)
289            .add_inclusive_range(300..=304)
290            .add_exclusive_range(400..405);
291
292        // Default range (200..500)
293        assert!(codes.contains(200));
294        assert!(codes.contains(299));
295        assert!(codes.contains(404));
296        assert!(codes.contains(499));
297        assert!(!codes.contains(500));
298
299        // Added single status
300        assert!(codes.contains(201));
301
302        // Added inclusive range
303        assert!(codes.contains(300));
304        assert!(codes.contains(304));
305
306        // Added exclusive range (405 is still contained due to default range 200..500)
307        assert!(codes.contains(400));
308        assert!(codes.contains(404));
309        assert!(codes.contains(405)); // 405 is in the default range 200..500
310    }
311
312    #[test]
313    fn test_status_code_variants() {
314        let codes = ExpectedStatusCodes::from_status_code(StatusCode::OK);
315        assert!(codes.contains_status_code(StatusCode::OK));
316        assert!(!codes.contains_status_code(StatusCode::NOT_FOUND));
317
318        let range_codes = ExpectedStatusCodes::from_status_code_range_inclusive(
319            StatusCode::OK..=StatusCode::NO_CONTENT,
320        );
321        assert!(range_codes.contains_status_code(StatusCode::OK));
322        assert!(range_codes.contains_status_code(StatusCode::CREATED));
323        assert!(range_codes.contains_status_code(StatusCode::NO_CONTENT));
324        assert!(!range_codes.contains_status_code(StatusCode::PARTIAL_CONTENT));
325
326        let exclusive_codes = ExpectedStatusCodes::from_status_code_range_exclusive(
327            StatusCode::OK..StatusCode::PARTIAL_CONTENT,
328        );
329        assert!(exclusive_codes.contains_status_code(StatusCode::OK));
330        assert!(exclusive_codes.contains_status_code(StatusCode::NO_CONTENT));
331        assert!(!exclusive_codes.contains_status_code(StatusCode::PARTIAL_CONTENT));
332    }
333
334    #[test]
335    #[should_panic(expected = "HTTP status code must be between 100 and 599, got 99")]
336    fn test_invalid_single_status_code_low() {
337        ExpectedStatusCodes::from_single(99);
338    }
339
340    #[test]
341    #[should_panic(expected = "HTTP status code must be between 100 and 599, got 600")]
342    fn test_invalid_single_status_code_high() {
343        ExpectedStatusCodes::from_single(600);
344    }
345
346    #[test]
347    #[should_panic(expected = "HTTP status code range start must be between 100 and 599, got 99")]
348    fn test_invalid_range_start_low() {
349        ExpectedStatusCodes::from_inclusive_range(99..=200);
350    }
351
352    #[test]
353    #[should_panic(expected = "HTTP status code range end must be between 100 and 599, got 600")]
354    fn test_invalid_range_end_high() {
355        ExpectedStatusCodes::from_inclusive_range(200..=600);
356    }
357
358    #[test]
359    #[should_panic(
360        expected = "HTTP status code range start (300) must be less than or equal to end (200)"
361    )]
362    #[allow(clippy::reversed_empty_ranges)]
363    fn test_invalid_range_order() {
364        ExpectedStatusCodes::from_inclusive_range(300..=200);
365    }
366
367    #[test]
368    #[should_panic(expected = "HTTP status code range start must be between 100 and 599, got 99")]
369    fn test_invalid_exclusive_range_start() {
370        ExpectedStatusCodes::from_exclusive_range(99..200);
371    }
372
373    #[test]
374    #[should_panic(
375        expected = "HTTP status code range end must be between 100 and 600 (exclusive), got 601"
376    )]
377    fn test_invalid_exclusive_range_end() {
378        ExpectedStatusCodes::from_exclusive_range(200..601);
379    }
380
381    #[test]
382    fn test_add_invalid_status() {
383        // This should not panic because add_single doesn't validate
384        let _codes = ExpectedStatusCodes::default().add_single(99);
385    }
386
387    #[test]
388    fn test_add_invalid_range() {
389        // This should not panic because add_inclusive_range doesn't validate
390        let _codes = ExpectedStatusCodes::default().add_inclusive_range(99..=600);
391    }
392}