Functional Weave
Code in Python

monitor.http-check-result@1.0.0

impl/rust.rs

6,275 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

fn error_message(error: &str) -> &'static str {
    match error {
        "timeout" => "timed out",
        "dns" => "DNS lookup failed",
        "connection" => "connection failed",
        "tls" => "TLS handshake failed",
        "protocol" => "protocol error",
        other => panic!("unknown probe error: {}", other),
    }
}

fn expected_text(statuses: Option<&[i64]>) -> String {
    match statuses {
        None => "2xx".to_string(),
        Some([one]) => one.to_string(),
        Some(list) => {
            let head: Vec<String> = list[..list.len() - 1].iter().map(|s| s.to_string()).collect();
            format!("{} or {}", head.join(", "), list[list.len() - 1])
        }
    }
}

fn threshold(name: &str, v: Option<i64>) {
    if let Some(t) = v {
        if t < 1 {
            panic!("{} must be a whole number 1 or more, received {}", name, t);
        }
    }
}

fn outcome(status: &str, reason: &str, message: String) -> CheckOutcome {
    CheckOutcome { status: status.to_string(), reason: reason.to_string(), message }
}

/// One probe, one verdict, checked in a fixed order so the reason is the most
/// fundamental thing wrong: a transport error, then the status code, then the
/// body, then latency. A 503 that also took ten seconds is reported as the
/// 503, because that is what someone has to fix.
///
/// # Panics
/// Panics on a probe with neither status nor error, out-of-range numbers, or
/// inconsistent thresholds.
pub fn classify_http_check(probe: &HttpProbe, expect: &HttpExpectation) -> CheckOutcome {
    if let Some(code) = probe.status_code {
        if !(100..=599).contains(&code) {
            panic!("statusCode must be 100 to 599, received {}", code);
        }
    }
    let error_text = probe.error.as_deref().map(error_message);
    if probe.status_code.is_none() && probe.error.is_none() {
        panic!("probe must have a statusCode or an error");
    }
    if let Some(l) = probe.latency_ms {
        if l < 0 {
            panic!("latencyMs must be a whole number 0 or more, received {}", l);
        }
    }
    if let Some(list) = &expect.statuses {
        if list.is_empty() {
            panic!("statuses must not be empty; use null to accept any 2xx");
        }
        for s in list {
            if !(100..=599).contains(s) {
                panic!("expected statuses must be 100 to 599, received {}", s);
            }
        }
    }
    let (degraded, down) = (expect.degraded_latency_ms, expect.down_latency_ms);
    threshold("degradedLatencyMs", degraded);
    threshold("downLatencyMs", down);
    if let (Some(dg), Some(dn)) = (degraded, down) {
        if dn < dg {
            panic!("downLatencyMs must not be below degradedLatencyMs: {} < {}", dn, dg);
        }
    }

    if let (Some(error), Some(text)) = (&probe.error, error_text) {
        return outcome("down", error, text.to_string());
    }
    let code = probe.status_code.unwrap();
    let statuses = expect.statuses.as_deref();
    let accepted = match statuses {
        None => (200..=299).contains(&code),
        Some(list) => list.contains(&code),
    };
    if !accepted {
        return outcome("down", "unexpected-status", format!("HTTP {}, expected {}", code, expected_text(statuses)));
    }
    if let Some(needle) = &expect.body_contains {
        let found = probe.body.as_deref().map_or(false, |b| b.contains(needle.as_str()));
        if !found {
            return outcome("down", "body-mismatch", format!("body does not contain \"{}\"", needle));
        }
    }
    if let Some(l) = probe.latency_ms {
        if let Some(dn) = down {
            if l >= dn {
                return outcome("down", "too-slow", format!("latency {} ms at or above {} ms", l, dn));
            }
        }
        if let Some(dg) = degraded {
            if l >= dg {
                return outcome("degraded", "slow", format!("latency {} ms at or above {} ms", l, dg));
            }
        }
    }
    let message = match probe.latency_ms {
        None => format!("HTTP {}", code),
        Some(l) => format!("HTTP {} in {} ms", code, l),
    };
    outcome("up", "ok", message)
}

/// An optional int field, refusing a fractional number with the same text the
/// other languages use for it.
fn opt_int(v: &Value, fractional: &dyn Fn(f64) -> String) -> Option<i64> {
    match v {
        Value::Null => None,
        Value::Int(i) => Some(*i),
        Value::Float(f) => panic!("{}", fractional(*f)),
        _ => panic!("{}", fractional(f64::NAN)),
    }
}

fn opt_str(v: &Value) -> Option<String> {
    if v.is_null() { None } else { Some(v.as_str().to_string()) }
}

pub fn http_probe_from_value(v: &Value) -> HttpProbe {
    HttpProbe {
        status_code: opt_int(&v.get("statusCode"), &|f| format!("statusCode must be 100 to 599, received {}", f)),
        latency_ms: opt_int(&v.get("latencyMs"), &|f| format!("latencyMs must be a whole number 0 or more, received {}", f)),
        body: opt_str(&v.get("body")),
        error: opt_str(&v.get("error")),
    }
}

pub fn http_expectation_from_value(v: &Value) -> HttpExpectation {
    let statuses = v.get("statuses");
    HttpExpectation {
        statuses: if statuses.is_null() {
            None
        } else {
            Some(
                statuses
                    .as_arr()
                    .iter()
                    .map(|s| opt_int(s, &|f| format!("expected statuses must be 100 to 599, received {}", f)).unwrap())
                    .collect(),
            )
        },
        body_contains: opt_str(&v.get("bodyContains")),
        degraded_latency_ms: opt_int(&v.get("degradedLatencyMs"), &|f| format!("degradedLatencyMs must be a whole number 1 or more, received {}", f)),
        down_latency_ms: opt_int(&v.get("downLatencyMs"), &|f| format!("downLatencyMs must be a whole number 1 or more, received {}", f)),
    }
}

pub fn check_outcome_to_value(o: &CheckOutcome) -> Value {
    Value::obj(vec![
        ("status", Value::str(&o.status)),
        ("reason", Value::str(&o.reason)),
        ("message", Value::str(&o.message)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    check_outcome_to_value(&classify_http_check(&http_probe_from_value(&args[0]), &http_expectation_from_value(&args[1])))
}