Skip to main content

lmn_core/threshold/
parse.rs

1use serde::Deserialize;
2
3use super::error::ThresholdError;
4use super::types::{Metric, Threshold};
5
6/// Internal wrapper for deserializing the `{ "thresholds": [...] }` envelope.
7#[derive(Deserialize)]
8struct ThresholdEnvelope {
9    thresholds: Vec<Threshold>,
10}
11
12/// Parses a list of thresholds from a JSON or YAML string.
13///
14/// The input must be an object with a `"thresholds"` key whose value is an
15/// array of threshold objects:
16///
17/// ```json
18/// { "thresholds": [{ "metric": "latency_p99", "operator": "lt", "value": 200.0 }] }
19/// ```
20///
21/// or equivalently in YAML:
22///
23/// ```yaml
24/// thresholds:
25///   - metric: latency_p99
26///     operator: lt
27///     value: 200.0
28/// ```
29///
30/// JSON is attempted first; if that fails, YAML is attempted. If both fail,
31/// the JSON error is returned wrapped in `ThresholdError::ParseError`.
32///
33/// Each threshold is then validated:
34/// - `value` must be finite
35/// - For `error_rate`, `value` must be in [0.0, 1.0]
36pub fn parse_thresholds(json_or_yaml: &str) -> Result<Vec<Threshold>, ThresholdError> {
37    let envelope: ThresholdEnvelope = serde_json::from_str(json_or_yaml).or_else(|json_err| {
38        serde_norway::from_str(json_or_yaml)
39            .map_err(|_yaml_err| ThresholdError::ParseError(json_err.to_string()))
40    })?;
41
42    validate_thresholds(envelope.thresholds)
43}
44
45pub(crate) fn validate_thresholds(
46    thresholds: Vec<Threshold>,
47) -> Result<Vec<Threshold>, ThresholdError> {
48    for t in &thresholds {
49        if !t.value.is_finite() {
50            return Err(ThresholdError::ValidationError(format!(
51                "threshold value must be finite, got: {}",
52                t.value
53            )));
54        }
55
56        if t.metric == Metric::ErrorRate && !(0.0..=1.0).contains(&t.value) {
57            return Err(ThresholdError::ValidationError(format!(
58                "error_rate threshold value must be in [0.0, 1.0], got: {}",
59                t.value
60            )));
61        }
62    }
63
64    Ok(thresholds)
65}
66
67// ── Tests ─────────────────────────────────────────────────────────────────────
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use crate::threshold::types::{Metric, Operator};
73
74    #[test]
75    fn parse_valid_json_thresholds() {
76        let json = r#"{
77            "thresholds": [
78                { "metric": "latency_p99", "operator": "lt", "value": 200.0 },
79                { "metric": "error_rate", "operator": "lte", "value": 0.05 }
80            ]
81        }"#;
82        let thresholds = parse_thresholds(json).expect("should parse");
83        assert_eq!(thresholds.len(), 2);
84        assert_eq!(thresholds[0].metric, Metric::LatencyP99);
85        assert_eq!(thresholds[0].operator, Operator::Lt);
86        assert!((thresholds[0].value - 200.0).abs() < f64::EPSILON);
87        assert_eq!(thresholds[1].metric, Metric::ErrorRate);
88        assert_eq!(thresholds[1].operator, Operator::Lte);
89    }
90
91    #[test]
92    fn parse_valid_yaml_thresholds() {
93        let yaml = "thresholds:\n  - metric: latency_p99\n    operator: lt\n    value: 200.0\n  - metric: throughput_rps\n    operator: gte\n    value: 10.0\n";
94        let thresholds = parse_thresholds(yaml).expect("should parse YAML");
95        assert_eq!(thresholds.len(), 2);
96        assert_eq!(thresholds[0].metric, Metric::LatencyP99);
97        assert_eq!(thresholds[1].metric, Metric::ThroughputRps);
98    }
99
100    #[test]
101    fn parse_invalid_metric_returns_error() {
102        let json = r#"{ "thresholds": [{ "metric": "does_not_exist", "operator": "lt", "value": 100.0 }] }"#;
103        let result = parse_thresholds(json);
104        assert!(result.is_err());
105    }
106
107    #[test]
108    fn parse_invalid_operator_returns_error() {
109        let json = r#"{ "thresholds": [{ "metric": "latency_p99", "operator": "not_an_op", "value": 100.0 }] }"#;
110        let result = parse_thresholds(json);
111        assert!(result.is_err());
112    }
113
114    #[test]
115    fn parse_error_rate_above_1_returns_error() {
116        let json =
117            r#"{ "thresholds": [{ "metric": "error_rate", "operator": "lte", "value": 1.5 }] }"#;
118        let result = parse_thresholds(json);
119        assert!(matches!(result, Err(ThresholdError::ValidationError(_))));
120    }
121
122    #[test]
123    fn parse_infinite_value_returns_error() {
124        // We can't express infinity in JSON directly, but we can test via a crafted f64.
125        // Build thresholds manually and call the private validator.
126        // Instead, test via YAML which supports .inf
127        let yaml = "thresholds:\n  - metric: latency_p99\n    operator: lt\n    value: .inf\n";
128        let result = parse_thresholds(yaml);
129        assert!(matches!(result, Err(ThresholdError::ValidationError(_))));
130    }
131
132    #[test]
133    fn parse_empty_thresholds_array_ok() {
134        let json = r#"{ "thresholds": [] }"#;
135        let thresholds = parse_thresholds(json).expect("empty array is valid");
136        assert!(thresholds.is_empty());
137    }
138}