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)]
9#[cfg_attr(feature = "unstable-schemas", derive(schemars::JsonSchema))]
10pub struct CacheZoomRange {
11    /// Default minimum zoom level (inclusive) for tile caching.
12    /// Tiles further zoomed out than this will bypass the cache entirely.
13    /// Can be overridden per-source (e.g. `cache.minzoom` on a type of source or an individual source).
14    /// default: null (no lower bound, all zoom levels cached)
15    minzoom: Option<u8>,
16    /// Default maximum zoom level (inclusive) for tile caching.
17    /// Tiles further zoomed in than this will bypass the cache entirely.
18    /// Can be overridden per-source.
19    /// default: null (no upper bound, all zoom levels cached)
20    maxzoom: Option<u8>,
21}
22
23impl CacheZoomRange {
24    /// Creates a new `CacheZoomRange` with the given bounds.
25    #[must_use]
26    pub fn new(minzoom: Option<u8>, maxzoom: Option<u8>) -> Self {
27        Self { minzoom, maxzoom }
28    }
29
30    /// Creates a disabled `CacheZoomRange` where `minzoom > maxzoom`,
31    /// so `contains()` always returns `false`.
32    #[must_use]
33    pub fn disabled() -> Self {
34        Self {
35            minzoom: Some(u8::MAX),
36            maxzoom: Some(0),
37        }
38    }
39
40    /// Returns `true` if neither bound is set.
41    #[must_use]
42    pub fn is_empty(self) -> bool {
43        self.minzoom.is_none() && self.maxzoom.is_none()
44    }
45
46    /// Returns `true` if `zoom` is within the configured bounds (inclusive).
47    /// Missing bounds are treated as unbounded.
48    #[must_use]
49    pub fn contains(self, zoom: u8) -> bool {
50        self.minzoom.is_none_or(|m| zoom >= m) && self.maxzoom.is_none_or(|m| zoom <= m)
51    }
52
53    /// Fills in any `None` fields from `other`.
54    #[must_use]
55    pub fn or(self, other: Self) -> Self {
56        Self {
57            minzoom: self.minzoom.or(other.minzoom),
58            maxzoom: self.maxzoom.or(other.maxzoom),
59        }
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn disabled_never_contains() {
69        let disabled = CacheZoomRange::disabled();
70        assert!(!disabled.contains(0));
71        assert!(!disabled.contains(10));
72        assert!(!disabled.contains(u8::MAX));
73    }
74
75    #[test]
76    fn disabled_is_not_empty() {
77        assert!(!CacheZoomRange::disabled().is_empty());
78    }
79
80    #[test]
81    fn disabled_not_overridden_by_or() {
82        let disabled = CacheZoomRange::disabled();
83        let defaults = CacheZoomRange::new(Some(0), Some(20));
84        // disabled has both fields set, so `or` won't replace them
85        let merged = disabled.or(defaults);
86        assert!(!merged.contains(0));
87        assert!(!merged.contains(10));
88    }
89
90    #[test]
91    fn default_contains_all() {
92        let range = CacheZoomRange::default();
93        assert!(range.contains(0));
94        assert!(range.contains(u8::MAX));
95    }
96
97    #[test]
98    fn bounded_range() {
99        let range = CacheZoomRange::new(Some(2), Some(10));
100        assert!(!range.contains(1));
101        assert!(range.contains(2));
102        assert!(range.contains(10));
103        assert!(!range.contains(11));
104    }
105}