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}