Skip to main content

assurance_runtime/
assurance.rs

1use crate::error::{
2    require_non_empty, require_non_empty_slice, AssuranceValidationError, AssuranceValidationResult,
3};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
8#[serde(rename_all = "snake_case")]
9pub enum ControlCoverageStateV1 {
10    Covered,
11    Partial,
12    GapPresent,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
16#[serde(rename_all = "snake_case")]
17pub enum ReleaseDecisionStateV1 {
18    Approved,
19    ApprovedWithMonitoring,
20    Blocked,
21}
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
24pub struct AssuranceCaseV1 {
25    pub schema_version: String,
26    pub assurance_case_id: String,
27    pub claim_summary: String,
28    pub evidence_refs: Vec<String>,
29    pub argument_structure: String,
30    pub open_obligations: Vec<String>,
31    pub ready_for_release: bool,
32}
33
34impl AssuranceCaseV1 {
35    #[allow(clippy::too_many_arguments)]
36    pub fn new(
37        assurance_case_id: impl Into<String>,
38        claim_summary: impl Into<String>,
39        evidence_refs: Vec<String>,
40        argument_structure: impl Into<String>,
41        open_obligations: Vec<String>,
42        ready_for_release: bool,
43    ) -> Result<Self, AssuranceValidationError> {
44        let value = Self {
45            schema_version: "AssuranceCaseV1".to_string(),
46            assurance_case_id: assurance_case_id.into(),
47            claim_summary: claim_summary.into(),
48            evidence_refs,
49            argument_structure: argument_structure.into(),
50            open_obligations,
51            ready_for_release,
52        };
53        value.validate()?;
54        Ok(value)
55    }
56
57    pub fn validate(&self) -> AssuranceValidationResult {
58        require_non_empty(&self.assurance_case_id, "assurance_case_id")?;
59        require_non_empty(&self.claim_summary, "claim_summary")?;
60        require_non_empty_slice(&self.evidence_refs, "evidence_refs")?;
61        require_non_empty(&self.argument_structure, "argument_structure")?;
62        Ok(())
63    }
64}
65
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
67pub struct ControlMappingV1 {
68    pub schema_version: String,
69    pub control_mapping_id: String,
70    pub hazard_register_id: String,
71    pub control_refs: Vec<String>,
72    pub coverage_state: ControlCoverageStateV1,
73    pub gaps: Vec<String>,
74}
75
76impl ControlMappingV1 {
77    #[allow(clippy::too_many_arguments)]
78    pub fn new(
79        control_mapping_id: impl Into<String>,
80        hazard_register_id: impl Into<String>,
81        control_refs: Vec<String>,
82        coverage_state: ControlCoverageStateV1,
83        gaps: Vec<String>,
84    ) -> Result<Self, AssuranceValidationError> {
85        let value = Self {
86            schema_version: "ControlMappingV1".to_string(),
87            control_mapping_id: control_mapping_id.into(),
88            hazard_register_id: hazard_register_id.into(),
89            control_refs,
90            coverage_state,
91            gaps,
92        };
93        value.validate()?;
94        Ok(value)
95    }
96
97    pub fn validate(&self) -> AssuranceValidationResult {
98        require_non_empty(&self.control_mapping_id, "control_mapping_id")?;
99        require_non_empty(&self.hazard_register_id, "hazard_register_id")?;
100        require_non_empty_slice(&self.control_refs, "control_refs")?;
101        if matches!(self.coverage_state, ControlCoverageStateV1::Covered) && !self.gaps.is_empty() {
102            return Err(AssuranceValidationError::InvalidState(
103                "covered mapping must not retain gaps",
104            ));
105        }
106        Ok(())
107    }
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
111pub struct ResidualRiskAcceptanceV1 {
112    pub schema_version: String,
113    pub residual_risk_acceptance_id: String,
114    pub deployment_profile_id: String,
115    pub accepted_by: Vec<String>,
116    pub accepted_risks: Vec<String>,
117    pub expires_at: String,
118    pub notes: Vec<String>,
119}
120
121impl ResidualRiskAcceptanceV1 {
122    #[allow(clippy::too_many_arguments)]
123    pub fn new(
124        residual_risk_acceptance_id: impl Into<String>,
125        deployment_profile_id: impl Into<String>,
126        accepted_by: Vec<String>,
127        accepted_risks: Vec<String>,
128        expires_at: impl Into<String>,
129        notes: Vec<String>,
130    ) -> Result<Self, AssuranceValidationError> {
131        let value = Self {
132            schema_version: "ResidualRiskAcceptanceV1".to_string(),
133            residual_risk_acceptance_id: residual_risk_acceptance_id.into(),
134            deployment_profile_id: deployment_profile_id.into(),
135            accepted_by,
136            accepted_risks,
137            expires_at: expires_at.into(),
138            notes,
139        };
140        value.validate()?;
141        Ok(value)
142    }
143
144    pub fn validate(&self) -> AssuranceValidationResult {
145        require_non_empty(
146            &self.residual_risk_acceptance_id,
147            "residual_risk_acceptance_id",
148        )?;
149        require_non_empty(&self.deployment_profile_id, "deployment_profile_id")?;
150        require_non_empty_slice(&self.accepted_by, "accepted_by")?;
151        require_non_empty_slice(&self.accepted_risks, "accepted_risks")?;
152        require_non_empty(&self.expires_at, "expires_at")?;
153        Ok(())
154    }
155}
156
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
158pub struct ReleaseReadinessDecisionV1 {
159    pub schema_version: String,
160    pub release_readiness_decision_id: String,
161    pub deployment_profile_id: String,
162    pub decision_state: ReleaseDecisionStateV1,
163    pub blocking_gaps: Vec<String>,
164    pub required_monitors: Vec<String>,
165    pub advisory_only: bool,
166    pub generated_at: String,
167}
168
169impl ReleaseReadinessDecisionV1 {
170    #[allow(clippy::too_many_arguments)]
171    pub fn new(
172        release_readiness_decision_id: impl Into<String>,
173        deployment_profile_id: impl Into<String>,
174        decision_state: ReleaseDecisionStateV1,
175        blocking_gaps: Vec<String>,
176        required_monitors: Vec<String>,
177        advisory_only: bool,
178        generated_at: impl Into<String>,
179    ) -> Result<Self, AssuranceValidationError> {
180        let value = Self {
181            schema_version: "ReleaseReadinessDecisionV1".to_string(),
182            release_readiness_decision_id: release_readiness_decision_id.into(),
183            deployment_profile_id: deployment_profile_id.into(),
184            decision_state,
185            blocking_gaps,
186            required_monitors,
187            advisory_only,
188            generated_at: generated_at.into(),
189        };
190        value.validate()?;
191        Ok(value)
192    }
193
194    pub fn validate(&self) -> AssuranceValidationResult {
195        require_non_empty(
196            &self.release_readiness_decision_id,
197            "release_readiness_decision_id",
198        )?;
199        require_non_empty(&self.deployment_profile_id, "deployment_profile_id")?;
200        require_non_empty(&self.generated_at, "generated_at")?;
201        match self.decision_state {
202            ReleaseDecisionStateV1::ApprovedWithMonitoring => {
203                require_non_empty_slice(&self.required_monitors, "required_monitors")?;
204            }
205            ReleaseDecisionStateV1::Blocked => {
206                require_non_empty_slice(&self.blocking_gaps, "blocking_gaps")?;
207            }
208            ReleaseDecisionStateV1::Approved => {}
209        }
210        Ok(())
211    }
212}