continuity-runtime 0.1.0

Typed continuity and incident surface crate with bounded recovery profiles; not an incident-command runtime
Documentation
use crate::error::{ContinuityValidationError, ContinuityValidationResult};
use crate::validation::{require_non_empty, require_non_empty_slice, require_schema_version};
use crate::vocab::IncidentSeverityV1;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct IncidentClassRuleV1 {
    pub incident_class: String,
    pub default_severity: IncidentSeverityV1,
}

impl IncidentClassRuleV1 {
    pub fn validate(&self) -> ContinuityValidationResult {
        require_non_empty(&self.incident_class, "incident_class")
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct IncidentRouteRuleV1 {
    pub incident_class: String,
    pub route: String,
}

impl IncidentRouteRuleV1 {
    pub fn validate(&self) -> ContinuityValidationResult {
        require_non_empty(&self.incident_class, "incident_class")?;
        require_non_empty(&self.route, "route")
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SeverityRuleV1 {
    pub condition: String,
    pub severity: IncidentSeverityV1,
}

impl SeverityRuleV1 {
    pub fn validate(&self) -> ContinuityValidationResult {
        require_non_empty(&self.condition, "condition")
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SeverityResponseClockV1 {
    pub severity: IncidentSeverityV1,
    pub minutes: i64,
}

impl SeverityResponseClockV1 {
    pub fn validate(&self) -> ContinuityValidationResult {
        if self.minutes <= 0 {
            return Err(ContinuityValidationError::InvalidState("minutes"));
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SeverityPostmortemClockV1 {
    pub severity: IncidentSeverityV1,
    pub hours: i64,
}

impl SeverityPostmortemClockV1 {
    pub fn validate(&self) -> ContinuityValidationResult {
        if self.hours <= 0 {
            return Err(ContinuityValidationError::InvalidState("hours"));
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct IncidentTaxonomyV1 {
    pub schema_version: String,
    pub incident_taxonomy_id: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub incident_classes: Vec<IncidentClassRuleV1>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub default_routes: Vec<IncidentRouteRuleV1>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub required_artifact_families: Vec<String>,
}

impl IncidentTaxonomyV1 {
    pub const SCHEMA_VERSION: &'static str = "IncidentTaxonomyV1";

    pub fn new(
        incident_taxonomy_id: impl Into<String>,
        incident_classes: Vec<IncidentClassRuleV1>,
        default_routes: Vec<IncidentRouteRuleV1>,
        required_artifact_families: Vec<String>,
    ) -> Result<Self, ContinuityValidationError> {
        let value = Self {
            schema_version: Self::SCHEMA_VERSION.to_string(),
            incident_taxonomy_id: incident_taxonomy_id.into(),
            incident_classes,
            default_routes,
            required_artifact_families,
        };
        value.validate()?;
        Ok(value)
    }

    pub fn validate(&self) -> ContinuityValidationResult {
        require_schema_version(&self.schema_version, Self::SCHEMA_VERSION)?;
        require_non_empty(&self.incident_taxonomy_id, "incident_taxonomy_id")?;
        require_non_empty_slice(&self.incident_classes, "incident_classes")?;
        require_non_empty_slice(&self.default_routes, "default_routes")?;
        require_non_empty_slice(
            &self.required_artifact_families,
            "required_artifact_families",
        )?;
        for rule in &self.incident_classes {
            rule.validate()?;
        }
        for route in &self.default_routes {
            route.validate()?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SeverityMatrixV1 {
    pub schema_version: String,
    pub severity_matrix_id: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub severity_rules: Vec<SeverityRuleV1>,
    pub customer_impact_rubric: String,
    pub internal_impact_rubric: String,
    pub override_rule: String,
}

impl SeverityMatrixV1 {
    pub const SCHEMA_VERSION: &'static str = "SeverityMatrixV1";

    pub fn new(
        severity_matrix_id: impl Into<String>,
        severity_rules: Vec<SeverityRuleV1>,
        customer_impact_rubric: impl Into<String>,
        internal_impact_rubric: impl Into<String>,
        override_rule: impl Into<String>,
    ) -> Result<Self, ContinuityValidationError> {
        let value = Self {
            schema_version: Self::SCHEMA_VERSION.to_string(),
            severity_matrix_id: severity_matrix_id.into(),
            severity_rules,
            customer_impact_rubric: customer_impact_rubric.into(),
            internal_impact_rubric: internal_impact_rubric.into(),
            override_rule: override_rule.into(),
        };
        value.validate()?;
        Ok(value)
    }

    pub fn validate(&self) -> ContinuityValidationResult {
        require_schema_version(&self.schema_version, Self::SCHEMA_VERSION)?;
        require_non_empty(&self.severity_matrix_id, "severity_matrix_id")?;
        require_non_empty_slice(&self.severity_rules, "severity_rules")?;
        require_non_empty(&self.customer_impact_rubric, "customer_impact_rubric")?;
        require_non_empty(&self.internal_impact_rubric, "internal_impact_rubric")?;
        require_non_empty(&self.override_rule, "override_rule")?;
        for rule in &self.severity_rules {
            rule.validate()?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PagerRouteProfileV1 {
    pub schema_version: String,
    pub pager_route_profile_id: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub rotation_refs: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub handoff_rules: Vec<String>,
    pub ack_timeout_minutes: i64,
    pub max_levels: i64,
}

impl PagerRouteProfileV1 {
    pub const SCHEMA_VERSION: &'static str = "PagerRouteProfileV1";

    pub fn new(
        pager_route_profile_id: impl Into<String>,
        rotation_refs: Vec<String>,
        handoff_rules: Vec<String>,
        ack_timeout_minutes: i64,
        max_levels: i64,
    ) -> Result<Self, ContinuityValidationError> {
        let value = Self {
            schema_version: Self::SCHEMA_VERSION.to_string(),
            pager_route_profile_id: pager_route_profile_id.into(),
            rotation_refs,
            handoff_rules,
            ack_timeout_minutes,
            max_levels,
        };
        value.validate()?;
        Ok(value)
    }

    pub fn validate(&self) -> ContinuityValidationResult {
        require_schema_version(&self.schema_version, Self::SCHEMA_VERSION)?;
        require_non_empty(&self.pager_route_profile_id, "pager_route_profile_id")?;
        require_non_empty_slice(&self.rotation_refs, "rotation_refs")?;
        require_non_empty_slice(&self.handoff_rules, "handoff_rules")?;
        if self.ack_timeout_minutes <= 0 {
            return Err(ContinuityValidationError::InvalidState(
                "ack_timeout_minutes",
            ));
        }
        if self.max_levels <= 0 {
            return Err(ContinuityValidationError::InvalidState("max_levels"));
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct EscalationClockPolicyV1 {
    pub schema_version: String,
    pub escalation_clock_policy_id: String,
    pub severity_matrix_id: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub response_clock_minutes: Vec<SeverityResponseClockV1>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub postmortem_clock_hours: Vec<SeverityPostmortemClockV1>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub pause_rules: Vec<String>,
    pub exception_path: String,
}

impl EscalationClockPolicyV1 {
    pub const SCHEMA_VERSION: &'static str = "EscalationClockPolicyV1";

    pub fn new(
        escalation_clock_policy_id: impl Into<String>,
        severity_matrix_id: impl Into<String>,
        response_clock_minutes: Vec<SeverityResponseClockV1>,
        postmortem_clock_hours: Vec<SeverityPostmortemClockV1>,
        pause_rules: Vec<String>,
        exception_path: impl Into<String>,
    ) -> Result<Self, ContinuityValidationError> {
        let value = Self {
            schema_version: Self::SCHEMA_VERSION.to_string(),
            escalation_clock_policy_id: escalation_clock_policy_id.into(),
            severity_matrix_id: severity_matrix_id.into(),
            response_clock_minutes,
            postmortem_clock_hours,
            pause_rules,
            exception_path: exception_path.into(),
        };
        value.validate()?;
        Ok(value)
    }

    pub fn validate(&self) -> ContinuityValidationResult {
        require_schema_version(&self.schema_version, Self::SCHEMA_VERSION)?;
        require_non_empty(
            &self.escalation_clock_policy_id,
            "escalation_clock_policy_id",
        )?;
        require_non_empty(&self.severity_matrix_id, "severity_matrix_id")?;
        require_non_empty_slice(&self.response_clock_minutes, "response_clock_minutes")?;
        require_non_empty_slice(&self.postmortem_clock_hours, "postmortem_clock_hours")?;
        require_non_empty(&self.exception_path, "exception_path")?;
        for clock in &self.response_clock_minutes {
            clock.validate()?;
        }
        for clock in &self.postmortem_clock_hours {
            clock.validate()?;
        }
        Ok(())
    }
}