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}