Skip to main content

martin_core/
cache_zoom_range.rs

1//! Zoom-level bounds for tile caching.
2
3use serde::{Deserialize, Serialize};
4
5/// Zoom-level bounds for tile caching. Used at the top level (as a global default),
6/// at backend level, and per-source to control which zoom levels are cached.
7#[serde_with::skip_serializing_none]
8#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
9pub struct CacheZoomRange {
10    minzoom: Option<u8>,
11    maxzoom: Option<u8>,
12}
13
14impl CacheZoomRange {
15    /// Creates a new `CacheZoomRange` with the given bounds.
16    #[must_use]
17    pub fn new(minzoom: Option<u8>, maxzoom: Option<u8>) -> Self {
18        Self { minzoom, maxzoom }
19    }
20
21    /// Creates a disabled `CacheZoomRange` where `minzoom > maxzoom`,
22    /// so `contains()` always returns `false`.
23    #[must_use]
24    pub fn disabled() -> Self {
25        Self {
26            minzoom: Some(u8::MAX),
27            maxzoom: Some(0),
28        }
29    }
30
31    /// Returns `true` if neither bound is set.
32    #[must_use]
33    pub fn is_empty(self) -> bool {
34        self.minzoom.is_none() && self.maxzoom.is_none()
35    }
36
37    /// Returns `true` if `zoom` is within the configured bounds (inclusive).
38    /// Missing bounds are treated as unbounded.
39    #[must_use]
40    pub fn contains(self, zoom: u8) -> bool {
41        self.minzoom.is_none_or(|m| zoom >= m) && self.maxzoom.is_none_or(|m| zoom <= m)
42    }
43
44    /// Fills in any `None` fields from `other`.
45    #[must_use]
46    pub fn or(self, other: Self) -> Self {
47        Self {
48            minzoom: self.minzoom.or(other.minzoom),
49            maxzoom: self.maxzoom.or(other.maxzoom),
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn disabled_never_contains() {
60        let disabled = CacheZoomRange::disabled();
61        assert!(!disabled.contains(0));
62        assert!(!disabled.contains(10));
63        assert!(!disabled.contains(u8::MAX));
64    }
65
66    #[test]
67    fn disabled_is_not_empty() {
68        assert!(!CacheZoomRange::disabled().is_empty());
69    }
70
71    #[test]
72    fn disabled_not_overridden_by_or() {
73        let disabled = CacheZoomRange::disabled();
74        let defaults = CacheZoomRange::new(Some(0), Some(20));
75        // disabled has both fields set, so `or` won't replace them
76        let merged = disabled.or(defaults);
77        assert!(!merged.contains(0));
78        assert!(!merged.contains(10));
79    }
80
81    #[test]
82    fn default_contains_all() {
83        let range = CacheZoomRange::default();
84        assert!(range.contains(0));
85        assert!(range.contains(u8::MAX));
86    }
87
88    #[test]
89    fn bounded_range() {
90        let range = CacheZoomRange::new(Some(2), Some(10));
91        assert!(!range.contains(1));
92        assert!(range.contains(2));
93        assert!(range.contains(10));
94        assert!(!range.contains(11));
95    }
96}