nagios_range/
types.rs

1use crate::error::Error;
2use std::fmt;
3
4/// A parsed Nagios range built from a literal string.
5/// A Nagios range works similar to [std::ops::RangeInclusive]
6/// in that it is bounded by the lower and upper bounds inclusively
7/// (`start..=end`).
8/// However it differs slightly in how it is used. It contains some
9/// logic to determine if an alert should be raised when it is
10/// compared to some value (either when inside or outside of the range).
11#[derive(Debug, PartialEq, Copy, Clone)]
12pub struct NagiosRange {
13    pub(crate) check_type: CheckType,
14    pub(crate) start: f64,
15    pub(crate) end: f64,
16}
17
18impl NagiosRange {
19    /// Creates a [NagiosRange] from a literal string.
20    ///
21    /// ```rust
22    /// use nagios_range::NagiosRange;
23    ///
24    /// fn main() -> Result<(), nagios_range::Error> {
25    ///     let range = NagiosRange::from("@-10:10");
26    ///     assert!(range.is_ok());
27    ///     Ok(())
28    /// }
29    /// ```
30    pub fn from(input: &str) -> Result<Self, Error> {
31        if input.is_empty() {
32            return Err(Error::EmptyRange);
33        }
34
35        if input.starts_with('@') {
36            let rem = &input[1..];
37            let (start, end) = parse_range(rem)?;
38            let inside_range = NagiosRange {
39                check_type: CheckType::Inside,
40                start,
41                end,
42            };
43            Ok(inside_range)
44        } else {
45            let (start, end) = parse_range(input)?;
46            let outside_range = NagiosRange {
47                check_type: CheckType::Outside,
48                start,
49                end,
50            };
51            Ok(outside_range)
52        }
53    }
54
55    /// Creates a [NagiosRange] from a [CheckType], lower and
56    /// (inclusive) upper bounds.
57    ///
58    /// ```rust
59    /// use nagios_range::{NagiosRange, CheckType};
60    ///
61    /// fn main() -> Result<(), nagios_range::Error> {
62    ///     let range = NagiosRange::new(CheckType::Inside, f64::NEG_INFINITY, 20.0);
63    ///     assert!(range.is_ok());
64    ///     assert_eq!(range?, NagiosRange::from("@~:20")?);
65    ///     Ok(())
66    /// }
67    /// ```
68    pub fn new(check_type: CheckType, start: f64, end: f64) -> Result<Self, Error> {
69        if start > end {
70            return Err(Error::StartGreaterThanEnd);
71        }
72
73        Ok(NagiosRange {
74            check_type,
75            start,
76            end,
77        })
78    }
79
80    /// Returns the lower bound of the range.
81    ///
82    /// ```rust
83    /// use nagios_range::NagiosRange;
84    ///
85    /// fn main() -> Result<(), nagios_range::Error> {
86    ///     let range = NagiosRange::from("@-10:10")?;
87    ///     assert_eq!(range.start(), &-10.0);
88    ///     Ok(())
89    /// }
90    /// ```
91    pub fn start(&self) -> &f64 {
92        &self.start
93    }
94
95    /// Returns the upper bound of the range.
96    ///
97    /// ```rust
98    /// use nagios_range::NagiosRange;
99    ///
100    /// fn main() -> Result<(), nagios_range::Error> {
101    ///     let range = NagiosRange::from("@-10:10")?;
102    ///     assert_eq!(range.end(), &10.0);
103    ///     Ok(())
104    /// }
105    /// ```
106    pub fn end(&self) -> &f64 {
107        &self.end
108    }
109
110    /// Returns `true` if the lower bound is negative infinity.
111    /// This is just a convenience method that calls [f64::is_infinite()]
112    /// on `start`.
113    ///
114    /// ```rust
115    /// use nagios_range::NagiosRange;
116    ///
117    /// fn main() -> Result<(), nagios_range::Error> {
118    ///     let range = NagiosRange::from("@~:10")?;
119    ///     assert!(range.start_is_infinite());
120    ///     assert_eq!(range.start_is_infinite(), range.start().is_infinite());
121    ///     Ok(())
122    /// }
123    /// ```
124    pub fn start_is_infinite(&self) -> bool {
125        self.start.is_infinite()
126    }
127
128    /// Returns `true` if the upper bound is positive infinity.
129    /// This is just a convenience method that calls [f64::is_infinite()]
130    /// on `end`.
131    ///
132    /// ```rust
133    /// use nagios_range::NagiosRange;
134    ///
135    /// fn main() -> Result<(), nagios_range::Error> {
136    ///     let range = NagiosRange::from("@10:")?;
137    ///     assert!(range.end_is_infinite());
138    ///     assert_eq!(range.end_is_infinite(), range.end().is_infinite());
139    ///     Ok(())
140    /// }
141    /// ```
142    pub fn end_is_infinite(&self) -> bool {
143        self.end.is_infinite()
144    }
145
146    /// Returns `true` if `item` is contained in the range irregardless
147    /// of the type of the NagiosRange. So this behaves just like
148    /// [std::ops::RangeInclusive::contains()].
149    ///
150    /// ```rust
151    /// use nagios_range::NagiosRange;
152    ///
153    /// fn main() -> Result<(), nagios_range::Error> {
154    ///     let range = NagiosRange::from("@~:10")?;
155    ///     assert!(range.contains(-50.0));
156    ///     assert!(range.contains(10.0));
157    ///     assert!(!range.contains(20.0));
158    ///     Ok(())
159    /// }
160    /// ```
161    pub fn contains(&self, item: f64) -> bool {
162        if item >= self.start && item <= self.end {
163            true
164        } else {
165            false
166        }
167    }
168
169    /// Returns `true` if a value is either inside or outside
170    /// of the range depending on the type of Nagios range.
171    ///
172    /// ```rust
173    /// use nagios_range::NagiosRange;
174    ///
175    /// fn main() -> Result<(), nagios_range::Error> {
176    ///     // When it is an "inside" range...
177    ///     let range = NagiosRange::from("@10:20")?;
178    ///     assert!(range.check(15.0));
179    ///
180    ///     // ...inverted behaviour when it is an "outside" range...
181    ///     let range = NagiosRange::from("10:20")?;
182    ///     assert!(range.check(30.0));
183    ///     Ok(())
184    /// }
185    /// ```
186    pub fn check(&self, item: f64) -> bool {
187        match self.check_type {
188            CheckType::Inside => {
189                if item >= self.start && item <= self.end {
190                    true
191                } else {
192                    false
193                }
194            }
195            CheckType::Outside => {
196                if item < self.start || item > self.end {
197                    true
198                } else {
199                    false
200                }
201            }
202        }
203    }
204
205    /// Returns `true` if [NagiosRange::check()] checks
206    /// if a value lies inside the range (`start <= item <= end`).
207    ///
208    /// ```rust
209    /// use nagios_range::NagiosRange;
210    ///
211    /// fn main() -> Result<(), nagios_range::Error> {
212    ///     let range = NagiosRange::from("@~:10")?;
213    ///     assert!(range.checks_inside());
214    ///     Ok(())
215    /// }
216    /// ```
217    pub fn checks_inside(&self) -> bool {
218        self.check_type == CheckType::Inside
219    }
220
221    /// Returns `true` if [NagiosRange::check()] checks
222    /// if a value lies outside the range (`start > item > end`).
223    ///
224    /// ```rust
225    /// use nagios_range::NagiosRange;
226    ///
227    /// fn main() -> Result<(), nagios_range::Error> {
228    ///     let range = NagiosRange::from("10:50")?;
229    ///     assert!(range.checks_outside());
230    ///     Ok(())
231    /// }
232    /// ```
233    pub fn checks_outside(&self) -> bool {
234        self.check_type == CheckType::Outside
235    }
236
237    /// Destructures the [NagiosRange] into a [CheckType] and
238    /// the lower and (inclusive) upper bounds.
239    ///
240    /// ```rust
241    /// use nagios_range::{NagiosRange, CheckType};
242    ///
243    /// fn main() -> Result<(), nagios_range::Error> {
244    ///     let range = NagiosRange::from("10:50")?;
245    ///     let (check_type, start, end) = range.into_inner();
246    ///     assert_eq!(check_type, CheckType::Outside);
247    ///     assert_eq!(start, 10.0);
248    ///
249    ///     let new_range = NagiosRange::new(check_type, start, end);
250    ///     assert!(new_range.is_ok());
251    ///     Ok(())
252    /// }
253    /// ```
254    pub fn into_inner(self) -> (CheckType, f64, f64) {
255        (self.check_type, self.start, self.end)
256    }
257}
258
259impl fmt::Display for NagiosRange {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        let start = if self.start.is_infinite() {
262            "~".to_string()
263        } else {
264            self.start.to_string()
265        };
266        let end = if self.end.is_infinite() {
267            "~".to_string()
268        } else {
269            self.end.to_string()
270        };
271        match self.check_type {
272            CheckType::Inside => write!(f, "@{}:{}", start, end),
273            CheckType::Outside => write!(f, "{}:{}", start, end),
274        }
275    }
276}
277
278/// This enum indicates if [NagiosRange::check()] should
279/// check if a value lies inside or outside of the range.
280#[derive(Debug, PartialEq, Copy, Clone)]
281pub enum CheckType {
282    Inside,
283    Outside,
284}
285
286fn parse_range(range: &str) -> Result<(f64, f64), Error> {
287    match range.split_once(':') {
288        Some(parts) => {
289            let start = if parts.0 == "~" {
290                f64::NEG_INFINITY
291            } else if parts.0.is_empty() {
292                0.0
293            } else {
294                parts.0.parse().map_err(Error::ParseStartPoint)?
295            };
296
297            let end = if parts.1.is_empty() {
298                f64::INFINITY
299            } else {
300                let num: f64 = parts.1.parse().map_err(Error::ParseEndPoint)?;
301
302                if start > num {
303                    return Err(Error::StartGreaterThanEnd);
304                }
305                num
306            };
307
308            Ok((start, end))
309        }
310        None => {
311            let start = 0.0;
312            let end: f64 = range.parse().map_err(Error::ParseEndPoint)?;
313            Ok((start, end))
314        }
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use crate::error::Error;
321    use crate::{CheckType, NagiosRange};
322
323    #[test]
324    fn parse_example_range_1() {
325        let result = NagiosRange::from("10");
326        let expect = NagiosRange {
327            check_type: CheckType::Outside,
328            start: 0.0,
329            end: 10.0,
330        };
331        assert_eq!(result, Ok(expect));
332    }
333
334    #[test]
335    fn parse_example_range_2() {
336        let result = NagiosRange::from("10:");
337        let expect = NagiosRange {
338            check_type: CheckType::Outside,
339            start: 10.0,
340            end: f64::INFINITY,
341        };
342        assert_eq!(result, Ok(expect));
343    }
344
345    #[test]
346    fn parse_example_range_3() {
347        let result = NagiosRange::from(":10");
348        let expect = NagiosRange {
349            check_type: CheckType::Outside,
350            start: 0.0,
351            end: 10.0,
352        };
353        assert_eq!(result, Ok(expect));
354    }
355
356    #[test]
357    fn parse_example_range_4() {
358        let result = NagiosRange::from("~:10");
359        let expect = NagiosRange {
360            check_type: CheckType::Outside,
361            start: f64::NEG_INFINITY,
362            end: 10.0,
363        };
364        assert_eq!(result, Ok(expect));
365    }
366
367    #[test]
368    fn parse_example_range_5() {
369        let result = NagiosRange::from("10:20");
370        let expect = NagiosRange {
371            check_type: CheckType::Outside,
372            start: 10.0,
373            end: 20.0,
374        };
375        assert_eq!(result, Ok(expect));
376    }
377
378    #[test]
379    fn parse_example_range_6() {
380        let result = NagiosRange::from("@10:20");
381        let expect = NagiosRange {
382            check_type: CheckType::Inside,
383            start: 10.0,
384            end: 20.0,
385        };
386        assert_eq!(result, Ok(expect));
387    }
388
389    #[test]
390    fn parse_example_range_7() {
391        let result = NagiosRange::from("@-10:20");
392        let expect = NagiosRange {
393            check_type: CheckType::Inside,
394            start: -10.0,
395            end: 20.0,
396        };
397        assert_eq!(result, Ok(expect));
398    }
399
400    #[test]
401    fn parse_example_range_8() {
402        let result = NagiosRange::from("@-10:-20");
403        let expect = Error::StartGreaterThanEnd;
404        assert_eq!(result, Err(expect));
405    }
406
407    #[test]
408    fn parse_example_range_9() {
409        let result = NagiosRange::from("@20:-20");
410        let expect = Error::StartGreaterThanEnd;
411        assert_eq!(result, Err(expect));
412    }
413
414    #[test]
415    fn parse_example_range_10() {
416        let result = NagiosRange::from("");
417        let expect = Error::EmptyRange;
418        assert_eq!(result, Err(expect));
419    }
420
421    #[test]
422    fn display_range_1() {
423        let range = NagiosRange::from("@10:20").unwrap();
424        let result = "@10:20".to_string();
425        assert_eq!(range.to_string(), result);
426    }
427
428    #[test]
429    fn display_range_2() {
430        let range = NagiosRange::from("@10:").unwrap();
431        let result = "@10:~".to_string();
432        assert_eq!(range.to_string(), result);
433    }
434
435    #[test]
436    fn display_range_3() {
437        let range = NagiosRange::from("10").unwrap();
438        let result = "0:10".to_string();
439        assert_eq!(range.to_string(), result);
440    }
441
442    #[test]
443    fn display_range_4() {
444        let range = NagiosRange::from("~:10").unwrap();
445        let result = "~:10".to_string();
446        assert_eq!(range.to_string(), result);
447    }
448}