1use crate::CargoAllowError;
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub enum PresenceMovement {
11 Introduced,
12 Retained,
13 Removed,
14}
15
16impl PresenceMovement {
17 pub const ALL: &[Self] = &[Self::Introduced, Self::Retained, Self::Removed];
18
19 pub const fn field_name(self) -> &'static str {
21 match self {
22 Self::Introduced => "introduced",
23 Self::Retained => "retained",
24 Self::Removed => "removed",
25 }
26 }
27
28 pub const fn movement_projection(self) -> &'static str {
30 match self {
31 Self::Introduced => "new",
32 Self::Retained => "inherited",
33 Self::Removed => "resolved",
34 }
35 }
36
37 pub const fn finding_change_label(self) -> &'static str {
39 match self {
40 Self::Introduced => "new",
41 Self::Retained => "retained",
42 Self::Removed => "removed",
43 }
44 }
45
46 pub const fn display_label(self) -> &'static str {
47 match self {
48 Self::Introduced => "introduced",
49 Self::Retained => "retained",
50 Self::Removed => "removed",
51 }
52 }
53
54 pub const fn as_str(self) -> &'static str {
55 self.field_name()
56 }
57
58 pub fn parse_field_name(value: &str) -> Result<Self, CargoAllowError> {
59 Self::from_str(value)
60 }
61
62 pub fn parse_finding_change_label(value: &str) -> Result<Self, CargoAllowError> {
63 match value.trim() {
64 "new" => Ok(Self::Introduced),
65 "removed" => Ok(Self::Removed),
66 "retained" => Ok(Self::Retained),
67 other => Err(CargoAllowError::new(format!(
68 "unsupported finding posture change `{other}`"
69 ))),
70 }
71 }
72
73 pub fn parse_movement_projection(value: &str) -> Result<Self, CargoAllowError> {
74 match value.trim() {
75 "new" => Ok(Self::Introduced),
76 "inherited" => Ok(Self::Retained),
77 "resolved" => Ok(Self::Removed),
78 other => Err(CargoAllowError::new(format!(
79 "unsupported movement projection `{other}`"
80 ))),
81 }
82 }
83}
84
85impl fmt::Display for PresenceMovement {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 write!(f, "{}", self.display_label())
88 }
89}
90
91impl FromStr for PresenceMovement {
92 type Err = CargoAllowError;
93
94 fn from_str(value: &str) -> Result<Self, Self::Err> {
95 match value.trim() {
96 "introduced" => Ok(Self::Introduced),
97 "retained" => Ok(Self::Retained),
98 "removed" => Ok(Self::Removed),
99 other => Err(CargoAllowError::new(format!(
100 "unsupported presence movement `{other}`"
101 ))),
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
108pub enum PostureDelta {
109 Improved,
110 Worsened,
111 ReviewRequired,
112 Unchanged,
113}
114
115impl PostureDelta {
116 pub const ALL: &[Self] = &[
117 Self::Improved,
118 Self::Worsened,
119 Self::ReviewRequired,
120 Self::Unchanged,
121 ];
122
123 pub const fn field_name(self) -> &'static str {
124 match self {
125 Self::Improved => "improved",
126 Self::Worsened => "worsened",
127 Self::ReviewRequired => "review_required",
128 Self::Unchanged => "unchanged",
129 }
130 }
131
132 pub const fn display_label(self) -> &'static str {
133 self.field_name()
134 }
135
136 pub const fn as_str(self) -> &'static str {
137 self.field_name()
138 }
139
140 pub fn parse_field_name(value: &str) -> Option<Self> {
141 Self::ALL
142 .iter()
143 .copied()
144 .find(|delta| delta.field_name() == value.trim())
145 }
146}
147
148impl fmt::Display for PostureDelta {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 write!(f, "{}", self.display_label())
151 }
152}
153
154impl FromStr for PostureDelta {
155 type Err = CargoAllowError;
156
157 fn from_str(value: &str) -> Result<Self, Self::Err> {
158 Self::parse_field_name(value)
159 .ok_or_else(|| CargoAllowError::new(format!("unsupported posture delta `{value}`")))
160 }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
168pub enum NetPosture {
169 Worse,
170 ReviewRequired,
171 Improved,
172 Unchanged,
173}
174
175impl NetPosture {
176 pub const ALL: &[Self] = &[
177 Self::Worse,
178 Self::ReviewRequired,
179 Self::Improved,
180 Self::Unchanged,
181 ];
182
183 pub const fn net_posture_label(self) -> &'static str {
184 match self {
185 Self::Worse => "worse",
186 Self::ReviewRequired => "review-required",
187 Self::Improved => "improved",
188 Self::Unchanged => "unchanged",
189 }
190 }
191
192 pub const fn as_str(self) -> &'static str {
193 self.net_posture_label()
194 }
195
196 pub const fn reviewer_action(self) -> &'static str {
197 match self {
198 Self::Worse => {
199 "block until failing source exception changes are fixed, narrowed, or receipted."
200 }
201 Self::ReviewRequired => "review the source exception posture change before merging.",
202 Self::Improved => "verify the cleanup was intentional and keep the narrower posture.",
203 Self::Unchanged => "no source exception posture change detected.",
204 }
205 }
206
207 pub fn parse_net_posture_label(value: &str) -> Option<Self> {
208 Self::ALL
209 .iter()
210 .copied()
211 .find(|posture| posture.net_posture_label() == value.trim())
212 }
213
214 pub const fn posture_delta(self) -> Option<PostureDelta> {
215 match self {
216 Self::Worse => Some(PostureDelta::Worsened),
217 Self::ReviewRequired => Some(PostureDelta::ReviewRequired),
218 Self::Improved => Some(PostureDelta::Improved),
219 Self::Unchanged => Some(PostureDelta::Unchanged),
220 }
221 }
222}
223
224impl fmt::Display for NetPosture {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 write!(f, "{}", self.net_posture_label())
227 }
228}
229
230impl FromStr for NetPosture {
231 type Err = CargoAllowError;
232
233 fn from_str(value: &str) -> Result<Self, Self::Err> {
234 Self::parse_net_posture_label(value)
235 .ok_or_else(|| CargoAllowError::new(format!("unsupported net posture `{value}`")))
236 }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub struct LedgerPosture {
242 pub movement: PresenceMovement,
243 pub delta: PostureDelta,
244}
245
246impl LedgerPosture {
247 pub const fn new(movement: PresenceMovement, delta: PostureDelta) -> Self {
248 Self { movement, delta }
249 }
250
251 pub fn movement_projection(self, touched_in_diff: bool) -> &'static str {
253 match self.movement {
254 PresenceMovement::Introduced => "new",
255 PresenceMovement::Removed => "resolved",
256 PresenceMovement::Retained
257 if self.delta == PostureDelta::Unchanged && !touched_in_diff =>
258 {
259 "inherited"
260 }
261 PresenceMovement::Retained => "retained",
262 }
263 }
264}
265
266#[cfg(test)]
267#[path = "ledger_posture_tests.rs"]
268mod tests;