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