use regex::Regex;
use std::sync::{LazyLock, OnceLock};
pub fn extract_http_status(message: &str) -> Option<u16> {
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| {
Regex::new(r"(?i)\bhttp[^0-9]{0,64}(\d{3})\b|\bstatus:\s*(\d{3})\b").expect("valid status regex")
});
let caps = re.captures(message)?;
caps.get(1).or_else(|| caps.get(2)).and_then(|m| m.as_str().parse().ok())
}
static HTML_DEC_ENTITY: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"&#([0-9]{1,7});").expect("valid decimal entity regex"));
static HTML_HEX_ENTITY: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)&#x([0-9a-f]{1,6});").expect("valid hex entity regex"));
fn decode_numeric_entity(caps: ®ex::Captures, radix: u32) -> String {
let parsed = caps.get(1).and_then(|m| {
if radix == 16 {
u32::from_str_radix(m.as_str(), 16).ok()
} else {
m.as_str().parse::<u32>().ok()
}
});
parsed
.and_then(char::from_u32)
.map(|ch| ch.to_string())
.unwrap_or_else(|| caps.get(0).map(|m| m.as_str()).unwrap_or_default().to_string())
}
pub(crate) fn decode_html_entities(input: &str) -> String {
let hex_decoded = HTML_HEX_ENTITY.replace_all(input, |caps: ®ex::Captures| decode_numeric_entity(caps, 16));
let num_decoded =
HTML_DEC_ENTITY.replace_all(&hex_decoded, |caps: ®ex::Captures| decode_numeric_entity(caps, 10));
num_decoded
.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'")
.replace("'", "'")
.replace("'", "'")
.replace("/", "/")
.replace(" ", " ")
.replace("…", "…")
.replace("’", "’")
.replace("‘", "‘")
.replace("”", "”")
.replace("“", "“")
.replace("–", "–")
.replace("—", "—")
.replace("©", "©")
.replace("®", "®")
.replace("×", "×")
.replace("·", "·")
.replace("&", "&")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_status_handles_common_shapes() {
assert_eq!(
extract_http_status("HTTP status server error (503 Service Unavailable) for url (https://example.com/)"),
Some(503)
);
assert_eq!(extract_http_status("error sending request: status: 403 Forbidden"), Some(403));
assert_eq!(extract_http_status("server returned 404"), None);
assert_eq!(extract_http_status("request timed out after 30s"), None);
assert_eq!(extract_http_status("no error here"), None);
}
#[test]
fn extract_status_ignores_non_three_digit_numbers() {
assert_eq!(extract_http_status("timed out after 30000ms"), None);
assert_eq!(extract_http_status("response body 1234 bytes"), None);
}
#[test]
fn extract_status_handles_status_with_spaces() {
assert_eq!(extract_http_status("error: status: 429 Too Many Requests"), Some(429));
}
}