use chrono::{
DateTime,
Utc,
};
use pipedash_plugin_api::{
FeedFormat,
HealthLevel,
};
use serde::{
Deserialize,
Deserializer,
};
use serde_json::Value;
use super::text::{
to_plain,
NAME_MAX_CHARS,
UPDATE_MAX_CHARS,
};
use super::{
decode,
lenient_date,
note_unknown,
FeedComponent,
FeedError,
FeedIncident,
FeedMaintenance,
ParsedFeed,
};
const SECURITY_EVENT: &str = "Security event";
const STATE_RESOLVED: u32 = 400;
#[derive(Deserialize)]
struct Envelope {
result: Feed,
}
#[derive(Deserialize)]
struct Feed {
status: Vec<RawComponent>,
#[serde(default)]
incidents: Vec<RawIncident>,
#[serde(default)]
maintenance: RawMaintenances,
}
#[derive(Deserialize)]
struct RawComponent {
id: String,
name: String,
#[serde(default, deserialize_with = "code")]
status_code: Option<u32>,
#[serde(default)]
containers: Vec<RawContainer>,
}
#[derive(Deserialize)]
struct RawContainer {
#[serde(default, deserialize_with = "code")]
status_code: Option<u32>,
}
#[derive(Deserialize)]
struct RawIncident {
#[serde(rename = "_id", alias = "id")]
id: String,
name: String,
#[serde(default, deserialize_with = "lenient_date")]
datetime_open: Option<DateTime<Utc>>,
#[serde(default)]
messages: Vec<RawMessage>,
#[serde(default)]
components_affected: Vec<Affected>,
}
#[derive(Deserialize)]
struct RawMessage {
#[serde(default)]
details: Option<String>,
#[serde(default, deserialize_with = "code")]
state: Option<u32>,
#[serde(default, deserialize_with = "code")]
status: Option<u32>,
#[serde(default, deserialize_with = "lenient_date")]
datetime: Option<DateTime<Utc>>,
}
#[derive(Deserialize)]
struct Affected {
#[serde(rename = "_id", alias = "id")]
id: String,
}
#[derive(Deserialize, Default)]
struct RawMaintenances {
#[serde(default)]
active: Vec<RawMaintenance>,
#[serde(default)]
upcoming: Vec<RawMaintenance>,
}
#[derive(Deserialize)]
struct RawMaintenance {
#[serde(rename = "_id", alias = "id")]
id: String,
name: String,
#[serde(default, deserialize_with = "lenient_date")]
datetime_planned_start: Option<DateTime<Utc>>,
#[serde(default, deserialize_with = "lenient_date")]
datetime_planned_end: Option<DateTime<Utc>>,
#[serde(default)]
components_affected: Vec<Affected>,
}
fn code<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<u32>, D::Error> {
Ok(match Option::<Value>::deserialize(deserializer)? {
Some(Value::Number(n)) => n.as_u64().and_then(|n| u32::try_from(n).ok()),
Some(Value::String(s)) => s.trim().parse().ok(),
_ => None,
})
}
pub fn parse_statusio(body: &[u8]) -> Result<ParsedFeed, FeedError> {
let Envelope { result } = decode(body)?;
let mut unknown = Vec::new();
let components = result
.status
.into_iter()
.map(|c| {
let (level, detail) = component_level(&c, &mut unknown);
FeedComponent {
id: c.id,
name: to_plain(&c.name, NAME_MAX_CHARS),
level,
detail,
children: Vec::new(),
}
})
.collect();
let incidents = result
.incidents
.into_iter()
.filter_map(|raw| incident(raw, &mut unknown))
.collect();
let maintenances = result
.maintenance
.active
.into_iter()
.map(|raw| maintenance(raw, true))
.chain(
result
.maintenance
.upcoming
.into_iter()
.map(|raw| maintenance(raw, false)),
)
.collect();
Ok(ParsedFeed {
format: FeedFormat::StatusIo,
components,
incidents,
maintenances,
unknown,
})
}
fn code_level(code: u32) -> Option<(HealthLevel, Option<&'static str>)> {
match code {
100 => Some((HealthLevel::Operational, None)),
200 => Some((HealthLevel::Maintenance, None)),
300 => Some((HealthLevel::Degraded, None)),
400 => Some((HealthLevel::PartialOutage, None)),
500 => Some((HealthLevel::MajorOutage, None)),
600 => Some((HealthLevel::Degraded, Some(SECURITY_EVENT))),
_ => None,
}
}
fn known_level(
code: Option<u32>, field: &'static str, unknown: &mut Vec<&'static str>,
) -> Option<(HealthLevel, Option<&'static str>)> {
let level = code.and_then(code_level);
if level.is_none() {
note_unknown(unknown, field);
}
level
}
fn component_level(
component: &RawComponent, unknown: &mut Vec<&'static str>,
) -> (HealthLevel, Option<&'static str>) {
std::iter::once(component.status_code)
.chain(component.containers.iter().map(|c| c.status_code))
.filter_map(|code| known_level(code, "component status_code", unknown))
.max_by_key(|(level, detail)| (*level, detail.is_some()))
.unwrap_or((HealthLevel::Operational, None))
}
fn incident(raw: RawIncident, unknown: &mut Vec<&'static str>) -> Option<FeedIncident> {
let newest = raw.messages.iter().max_by_key(|m| m.datetime);
let state = newest.and_then(|m| m.state);
let phase = match state {
Some(STATE_RESOLVED) => return None,
Some(100) => "Investigating",
Some(200) => "Identified",
Some(300) => "Monitoring a fix",
Some(_) => {
note_unknown(unknown, "incident state");
"Ongoing"
}
None => "Ongoing",
};
let level = match newest {
Some(message) => known_level(message.status, "incident status", unknown)
.map_or(HealthLevel::Operational, |(level, _)| level),
None => HealthLevel::Operational,
};
Some(FeedIncident {
level,
phase,
latest_update: newest
.and_then(|m| m.details.as_deref())
.map(|details| to_plain(details, UPDATE_MAX_CHARS))
.filter(|text| !text.is_empty()),
updated_at: newest.and_then(|m| m.datetime),
id: raw.id,
name: to_plain(&raw.name, NAME_MAX_CHARS),
shortlink: None,
started_at: raw.datetime_open,
components: raw.components_affected.into_iter().map(|c| c.id).collect(),
})
}
fn maintenance(raw: RawMaintenance, in_progress: bool) -> FeedMaintenance {
FeedMaintenance {
id: raw.id,
name: to_plain(&raw.name, NAME_MAX_CHARS),
in_progress,
shortlink: None,
starts_at: raw.datetime_planned_start,
ends_at: raw.datetime_planned_end,
components: raw.components_affected.into_iter().map(|c| c.id).collect(),
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
fn feed(result: serde_json::Value) -> ParsedFeed {
parse_statusio(json!({ "result": result }).to_string().as_bytes()).unwrap()
}
fn component(id: &str, code: u32, containers: &[u32]) -> serde_json::Value {
json!({
"id": id,
"name": id,
"status_code": code,
"containers": containers.iter().map(|c| json!({"id": "c", "status_code": c})).collect::<Vec<_>>(),
})
}
#[test]
fn status_codes_map_to_levels() {
let parsed = feed(json!({"status": [
component("a", 100, &[]),
component("b", 200, &[]),
component("c", 300, &[]),
component("d", 400, &[]),
component("e", 500, &[]),
component("f", 600, &[]),
]}));
let seen: Vec<_> = parsed
.components
.iter()
.map(|c| (c.level, c.detail))
.collect();
assert_eq!(
seen,
[
(HealthLevel::Operational, None),
(HealthLevel::Maintenance, None),
(HealthLevel::Degraded, None),
(HealthLevel::PartialOutage, None),
(HealthLevel::MajorOutage, None),
(HealthLevel::Degraded, Some("Security event")),
]
);
}
#[test]
fn component_level_is_the_worst_of_itself_and_its_containers() {
let parsed = feed(json!({"status": [
component("a", 100, &[100, 400, 300]),
component("b", 500, &[100]),
component("c", 100, &[100]),
]}));
let levels: Vec<_> = parsed.components.iter().map(|c| c.level).collect();
assert_eq!(
levels,
[
HealthLevel::PartialOutage,
HealthLevel::MajorOutage,
HealthLevel::Operational
]
);
}
#[test]
fn incidents_use_the_newest_message_and_skip_resolved_ones() {
let message = |state: u32, status: u32, at: &str, details: &str| json!({"state": state, "status": status, "datetime": at, "details": details});
let parsed = feed(json!({
"status": [],
"incidents": [
{
"_id": "open",
"name": "Runners & queues",
"datetime_open": "2026-10-22T10:00:00.000Z",
"messages": [
message(300, 500, "2026-10-22T12:00:00.000Z", "Watching<br>closely"),
message(100, 300, "2026-10-22T10:00:00.000Z", "first"),
],
"components_affected": [{"_id": "x", "name": "X"}],
},
{
"_id": "done",
"name": "Done",
"messages": [
message(100, 500, "2026-10-22T10:00:00.000Z", "first"),
message(400, 100, "2026-10-22T12:00:00.000Z", "fixed"),
],
},
{
"_id": "bare",
"name": "No messages",
},
],
}));
assert_eq!(parsed.incidents.len(), 2);
let open = &parsed.incidents[0];
assert_eq!(open.id, "open");
assert_eq!(open.name, "Runners & queues");
assert_eq!(open.level, HealthLevel::MajorOutage);
assert_eq!(open.phase, "Monitoring a fix");
assert_eq!(open.latest_update.as_deref(), Some("Watching\nclosely"));
assert_eq!(open.components, ["x"]);
assert_eq!(parsed.incidents[1].id, "bare");
assert_eq!(parsed.incidents[1].level, HealthLevel::Operational);
}
#[test]
fn maintenance_lists_follow_active_and_upcoming() {
let parsed = feed(json!({
"status": [],
"maintenance": {
"active": [{"_id": "a", "name": "A", "datetime_planned_start": "2026-10-22T10:00:00.000Z"}],
"upcoming": [{"_id": "u", "name": "U", "datetime_planned_end": "2026-10-23T10:00:00.000Z"}],
},
}));
let seen: Vec<_> = parsed
.maintenances
.iter()
.map(|m| (m.id.as_str(), m.in_progress))
.collect();
assert_eq!(seen, [("a", true), ("u", false)]);
}
#[test]
fn string_codes_are_accepted_and_unknown_codes_recorded_once() {
let parsed = feed(json!({"status": [
{"id": "a", "name": "A", "status_code": "300"},
component("b", 900, &[901]),
]}));
assert_eq!(parsed.components[0].level, HealthLevel::Degraded);
assert_eq!(parsed.components[1].level, HealthLevel::Operational);
assert_eq!(parsed.unknown, ["component status_code"]);
}
#[test]
fn payload_without_result_is_rejected() {
assert!(matches!(parse_statusio(b"{}"), Err(FeedError::Invalid(_))));
}
}