Skip to main content

dbrest_core/api_request/
range.rs

1//! Range parsing for limit/offset and HTTP Range header
2//!
3//! Range types for handling pagination
4//! through Range headers and limit/offset query parameters.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9/// A non-negative range representing rows to return.
10///
11/// A non-negative range for pagination. Uses `Option<i64>` for boundaries
12/// where `None` means unbounded.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub struct Range {
15    /// Lower bound (0-based, inclusive). Always present.
16    pub offset: i64,
17    /// Upper bound (inclusive). None means unbounded (all remaining rows).
18    pub limit_to: Option<i64>,
19}
20
21impl Default for Range {
22    /// The default range: all rows starting from 0.
23    fn default() -> Self {
24        Self::all()
25    }
26}
27
28impl Range {
29    /// Create a range representing all rows (offset=0, unbounded).
30    pub fn all() -> Self {
31        Self {
32            offset: 0,
33            limit_to: None,
34        }
35    }
36
37    /// Create a range from offset to upper bound (inclusive).
38    pub fn new(offset: i64, limit_to: i64) -> Self {
39        Self {
40            offset,
41            limit_to: Some(limit_to),
42        }
43    }
44
45    /// Create a range starting at the given offset with no upper bound.
46    pub fn from_offset(offset: i64) -> Self {
47        Self {
48            offset,
49            limit_to: None,
50        }
51    }
52
53    /// The special limit-zero range (0 <= x <= -1).
54    /// Used to allow `limit=0` queries per the API spec.
55    pub fn limit_zero() -> Self {
56        Self {
57            offset: 0,
58            limit_to: Some(-1),
59        }
60    }
61
62    /// Check if this is the limit-zero range.
63    pub fn has_limit_zero(&self) -> bool {
64        self.limit_to == Some(-1)
65    }
66
67    /// Get the number of rows this range covers, if bounded.
68    pub fn limit(&self) -> Option<i64> {
69        self.limit_to.map(|upper| 1 + upper - self.offset)
70    }
71
72    /// Get the offset.
73    pub fn offset(&self) -> i64 {
74        self.offset
75    }
76
77    /// Check if this range is unbounded (no upper limit).
78    pub fn is_all(&self) -> bool {
79        self.offset == 0 && self.limit_to.is_none()
80    }
81
82    /// Check if this range is empty (lower > upper).
83    pub fn is_empty_range(&self) -> bool {
84        match self.limit_to {
85            Some(upper) => self.offset > upper && !self.has_limit_zero(),
86            None => false,
87        }
88    }
89
90    /// Restrict this range by applying a limit.
91    ///
92    /// If `max_rows` is Some, ensures the range covers at most that many rows.
93    pub fn restrict(&self, max_rows: Option<i64>) -> Self {
94        match max_rows {
95            None => *self,
96            Some(limit) => {
97                let new_upper = self.offset + limit - 1;
98                match self.limit_to {
99                    Some(upper) => Self {
100                        offset: self.offset,
101                        limit_to: Some(upper.min(new_upper)),
102                    },
103                    None => Self {
104                        offset: self.offset,
105                        limit_to: Some(new_upper),
106                    },
107                }
108            }
109        }
110    }
111
112    /// Apply a limit (number of rows).
113    pub fn with_limit(&self, limit: i64) -> Self {
114        Self {
115            offset: self.offset,
116            limit_to: Some(self.offset + limit - 1),
117        }
118    }
119
120    /// Apply an offset.
121    pub fn with_offset(&self, offset: i64) -> Self {
122        Self {
123            offset,
124            limit_to: self.limit_to,
125        }
126    }
127
128    /// Intersect this range with another.
129    pub fn intersect(&self, other: &Range) -> Self {
130        let new_offset = self.offset.max(other.offset);
131        let new_upper = match (self.limit_to, other.limit_to) {
132            (Some(a), Some(b)) => Some(a.min(b)),
133            (Some(a), None) => Some(a),
134            (None, Some(b)) => Some(b),
135            (None, None) => None,
136        };
137        Self {
138            offset: new_offset,
139            limit_to: new_upper,
140        }
141    }
142
143    /// Convert to limit-zero range if it has limit=0, else use fallback.
144    pub fn convert_to_limit_zero(&self, fallback: &Range) -> Self {
145        if self.has_limit_zero() {
146            Self::limit_zero()
147        } else {
148            *fallback
149        }
150    }
151}
152
153impl fmt::Display for Range {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        match self.limit_to {
156            Some(upper) => write!(f, "{}-{}", self.offset, upper),
157            None => write!(f, "{}-", self.offset),
158        }
159    }
160}
161
162/// Parse the HTTP `Range` header value.
163///
164/// Expects format: `items=<start>-<end>` where end is optional.
165///
166/// # Examples
167///
168/// ```
169/// use dbrest::api_request::range::parse_range_header;
170///
171/// let r = parse_range_header("items=0-24").unwrap();
172/// assert_eq!(r.offset, 0);
173/// assert_eq!(r.limit_to, Some(24));
174///
175/// let r = parse_range_header("items=10-").unwrap();
176/// assert_eq!(r.offset, 10);
177/// assert_eq!(r.limit_to, None);
178/// ```
179pub fn parse_range_header(header: &str) -> Option<Range> {
180    // Strip "items=" prefix (case-insensitive)
181    let range_str = header
182        .strip_prefix("items=")
183        .or_else(|| header.strip_prefix("Items="))?;
184
185    let (start_str, end_str) = range_str.split_once('-')?;
186
187    let start: i64 = start_str.parse().ok()?;
188
189    if end_str.is_empty() {
190        Some(Range::from_offset(start))
191    } else {
192        let end: i64 = end_str.parse().ok()?;
193        Some(Range::new(start, end))
194    }
195}
196
197/// Build Content-Range header value.
198///
199/// Format: `<lower>-<upper>/<total>` or `*/<total>` or `<lower>-<upper>/*`
200pub fn content_range_header(lower: i64, upper: i64, total: Option<i64>) -> String {
201    let total_str = match total {
202        Some(t) => t.to_string(),
203        None => "*".to_string(),
204    };
205
206    let total_not_zero = total != Some(0);
207    let from_in_range = lower <= upper;
208
209    if total_not_zero && from_in_range {
210        format!("{}-{}/{}", lower, upper, total_str)
211    } else {
212        format!("*/{}", total_str)
213    }
214}
215
216// ==========================================================================
217// Tests
218// ==========================================================================
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_range_all() {
226        let r = Range::all();
227        assert_eq!(r.offset, 0);
228        assert_eq!(r.limit_to, None);
229        assert!(r.is_all());
230        assert!(!r.is_empty_range());
231    }
232
233    #[test]
234    fn test_range_new() {
235        let r = Range::new(0, 24);
236        assert_eq!(r.offset, 0);
237        assert_eq!(r.limit_to, Some(24));
238        assert!(!r.is_all());
239        assert_eq!(r.limit(), Some(25));
240    }
241
242    #[test]
243    fn test_range_from_offset() {
244        let r = Range::from_offset(10);
245        assert_eq!(r.offset, 10);
246        assert_eq!(r.limit_to, None);
247        assert!(!r.is_all());
248    }
249
250    #[test]
251    fn test_range_limit_zero() {
252        let r = Range::limit_zero();
253        assert!(r.has_limit_zero());
254        assert_eq!(r.limit(), Some(0));
255    }
256
257    #[test]
258    fn test_range_empty() {
259        let r = Range::new(10, 5); // lower > upper
260        assert!(r.is_empty_range());
261    }
262
263    #[test]
264    fn test_range_restrict() {
265        let r = Range::all();
266        let restricted = r.restrict(Some(25));
267        assert_eq!(restricted.offset, 0);
268        assert_eq!(restricted.limit_to, Some(24));
269
270        // Restrict with None does nothing
271        let same = r.restrict(None);
272        assert_eq!(same, r);
273    }
274
275    #[test]
276    fn test_range_restrict_existing() {
277        let r = Range::new(0, 100);
278        let restricted = r.restrict(Some(25));
279        assert_eq!(restricted.limit_to, Some(24));
280
281        // Restrict larger than current doesn't expand
282        let larger = r.restrict(Some(200));
283        assert_eq!(larger.limit_to, Some(100));
284    }
285
286    #[test]
287    fn test_range_with_limit() {
288        let r = Range::from_offset(5);
289        let limited = r.with_limit(10);
290        assert_eq!(limited.offset, 5);
291        assert_eq!(limited.limit_to, Some(14));
292        assert_eq!(limited.limit(), Some(10));
293    }
294
295    #[test]
296    fn test_range_with_offset() {
297        let r = Range::new(0, 24);
298        let offset = r.with_offset(10);
299        assert_eq!(offset.offset, 10);
300        assert_eq!(offset.limit_to, Some(24));
301    }
302
303    #[test]
304    fn test_range_intersect() {
305        let a = Range::new(0, 100);
306        let b = Range::new(10, 50);
307        let c = a.intersect(&b);
308        assert_eq!(c.offset, 10);
309        assert_eq!(c.limit_to, Some(50));
310
311        // Intersect with unbounded
312        let d = Range::all();
313        let e = a.intersect(&d);
314        assert_eq!(e, a);
315    }
316
317    #[test]
318    fn test_range_display() {
319        assert_eq!(Range::new(0, 24).to_string(), "0-24");
320        assert_eq!(Range::from_offset(10).to_string(), "10-");
321    }
322
323    #[test]
324    fn test_parse_range_header() {
325        let r = parse_range_header("items=0-24").unwrap();
326        assert_eq!(r.offset, 0);
327        assert_eq!(r.limit_to, Some(24));
328
329        let r = parse_range_header("items=10-").unwrap();
330        assert_eq!(r.offset, 10);
331        assert_eq!(r.limit_to, None);
332
333        let r = parse_range_header("Items=5-10").unwrap();
334        assert_eq!(r.offset, 5);
335        assert_eq!(r.limit_to, Some(10));
336
337        // Invalid
338        assert!(parse_range_header("bytes=0-24").is_none());
339        assert!(parse_range_header("items=abc-def").is_none());
340        assert!(parse_range_header("garbage").is_none());
341    }
342
343    #[test]
344    fn test_content_range_header() {
345        assert_eq!(content_range_header(0, 24, Some(100)), "0-24/100");
346        assert_eq!(content_range_header(0, 24, None), "0-24/*");
347        assert_eq!(content_range_header(10, 5, Some(100)), "*/100"); // lower > upper
348        assert_eq!(content_range_header(0, 0, Some(0)), "*/0"); // total is zero
349    }
350
351    #[test]
352    fn test_range_default() {
353        let r = Range::default();
354        assert!(r.is_all());
355    }
356
357    #[test]
358    fn test_convert_to_limit_zero() {
359        let limit_range = Range::limit_zero();
360        let fallback = Range::new(0, 24);
361        let result = limit_range.convert_to_limit_zero(&fallback);
362        assert!(result.has_limit_zero());
363
364        let normal = Range::new(0, 10);
365        let result2 = normal.convert_to_limit_zero(&fallback);
366        assert_eq!(result2, fallback);
367    }
368}