1use std::fmt;
4use std::str::FromStr;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum FocusedReviewStatus {
9 Pending,
11 Ready,
13 Failed,
15}
16
17impl fmt::Display for FocusedReviewStatus {
18 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
19 formatter.write_str(match self {
20 Self::Pending => "Pending",
21 Self::Ready => "Ready",
22 Self::Failed => "Failed",
23 })
24 }
25}
26
27impl FromStr for FocusedReviewStatus {
28 type Err = String;
29
30 fn from_str(value: &str) -> Result<Self, Self::Err> {
31 match value {
32 "Pending" => Ok(Self::Pending),
33 "Ready" => Ok(Self::Ready),
34 "Failed" => Ok(Self::Failed),
35 _ => Err(format!("Unknown focused review status: {value}")),
36 }
37 }
38}
39
40#[must_use]
45pub fn review_suggestions(review_text: &str) -> Option<String> {
46 let suggestions_header = "### Suggestions";
47 let header_start = review_text.find(suggestions_header)?;
48 let content_start = header_start + suggestions_header.len();
49 let content = &review_text[content_start..];
50 let section_end = content.find("\n### ").unwrap_or(content.len());
51 let suggestions = content[..section_end].trim();
52
53 if suggestions.is_empty() || is_no_suggestions_sentinel(suggestions) {
54 return None;
55 }
56
57 Some(suggestions.to_string())
58}
59
60fn is_no_suggestions_sentinel(suggestions: &str) -> bool {
63 suggestions.strip_prefix("- None").is_some_and(|suffix| {
64 suffix
65 .chars()
66 .all(|character| character.is_ascii_punctuation())
67 })
68}
69
70#[must_use]
73pub fn has_actionable_review_suggestions(review_text: Option<&str>) -> bool {
74 review_text.and_then(review_suggestions).is_some()
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn focused_review_status_round_trips_persisted_values() {
83 let statuses = [
85 FocusedReviewStatus::Pending,
86 FocusedReviewStatus::Ready,
87 FocusedReviewStatus::Failed,
88 ];
89
90 for status in statuses {
92 assert_eq!(status.to_string().parse(), Ok(status));
93 }
94 assert!("Unknown".parse::<FocusedReviewStatus>().is_err());
95 }
96
97 #[test]
98 fn test_review_suggestions_returns_suggestions_content() {
99 let review_text = "\
101### Summary
102
103- Good shape.
104
105### Suggestions
106
107- Fix the typo in `README.md:10`.";
108
109 let suggestions = review_suggestions(review_text);
111
112 assert_eq!(
114 suggestions,
115 Some("- Fix the typo in `README.md:10`.".to_string())
116 );
117 }
118
119 #[test]
120 fn test_review_suggestions_returns_none_for_no_suggestions() {
121 let review_text = "\
123### Summary
124
125- Good shape.
126
127### Suggestions
128
129- None";
130
131 let suggestions = review_suggestions(review_text);
133
134 assert_eq!(suggestions, None);
136 }
137
138 #[test]
139 fn test_review_suggestions_returns_none_for_punctuated_no_suggestions() {
140 let review_text = "## Review\n\n### Suggestions\n\n- None.";
142
143 let suggestions = review_suggestions(review_text);
145
146 assert_eq!(suggestions, None);
148 }
149
150 #[test]
151 fn test_review_suggestions_returns_none_when_section_missing() {
152 let review_text = "\
154### Summary
155
156- Good shape overall.";
157
158 let suggestions = review_suggestions(review_text);
160
161 assert_eq!(suggestions, None);
163 }
164
165 #[test]
166 fn test_review_suggestions_stops_at_next_heading() {
167 let review_text = "\
169### Summary
170
171- Good shape.
172
173### Suggestions
174
175- Fix the typo in `README.md:10`.
176
177### Project Impact
178
179- Great work overall.";
180
181 let suggestions = review_suggestions(review_text);
183
184 assert_eq!(
186 suggestions,
187 Some("- Fix the typo in `README.md:10`.".to_string())
188 );
189 }
190
191 #[test]
192 fn test_review_suggestions_returns_none_for_empty_section() {
193 let review_text = "\
195### Suggestions
196
197### Project Impact
198
199- None";
200
201 let suggestions = review_suggestions(review_text);
203
204 assert_eq!(suggestions, None);
206 }
207
208 #[test]
209 fn test_has_actionable_review_suggestions_detects_suggestions_section() {
210 let review_with_suggestions = "## Review\n### Suggestions\n- Fix typo\n### Notes";
212 let review_without_suggestions = "## Review\n### Suggestions\n- None\n### Notes";
213
214 let with_suggestions = has_actionable_review_suggestions(Some(review_with_suggestions));
216 let without_suggestions =
217 has_actionable_review_suggestions(Some(review_without_suggestions));
218 let missing_header = has_actionable_review_suggestions(Some("## Review"));
219
220 assert!(with_suggestions);
222 assert!(!without_suggestions);
223 assert!(!missing_header);
224 }
225}