use std::fmt;
use std::str::FromStr;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FocusedReviewStatus {
Pending,
Ready,
Failed,
}
impl fmt::Display for FocusedReviewStatus {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Pending => "Pending",
Self::Ready => "Ready",
Self::Failed => "Failed",
})
}
}
impl FromStr for FocusedReviewStatus {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"Pending" => Ok(Self::Pending),
"Ready" => Ok(Self::Ready),
"Failed" => Ok(Self::Failed),
_ => Err(format!("Unknown focused review status: {value}")),
}
}
}
#[must_use]
pub fn review_suggestions(review_text: &str) -> Option<String> {
let suggestions_header = "### Suggestions";
let header_start = review_text.find(suggestions_header)?;
let content_start = header_start + suggestions_header.len();
let content = &review_text[content_start..];
let section_end = content.find("\n### ").unwrap_or(content.len());
let suggestions = content[..section_end].trim();
if suggestions.is_empty() || is_no_suggestions_sentinel(suggestions) {
return None;
}
Some(suggestions.to_string())
}
fn is_no_suggestions_sentinel(suggestions: &str) -> bool {
suggestions.strip_prefix("- None").is_some_and(|suffix| {
suffix
.chars()
.all(|character| character.is_ascii_punctuation())
})
}
#[must_use]
pub fn has_actionable_review_suggestions(review_text: Option<&str>) -> bool {
review_text.and_then(review_suggestions).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn focused_review_status_round_trips_persisted_values() {
let statuses = [
FocusedReviewStatus::Pending,
FocusedReviewStatus::Ready,
FocusedReviewStatus::Failed,
];
for status in statuses {
assert_eq!(status.to_string().parse(), Ok(status));
}
assert!("Unknown".parse::<FocusedReviewStatus>().is_err());
}
#[test]
fn test_review_suggestions_returns_suggestions_content() {
let review_text = "\
### Summary
- Good shape.
### Suggestions
- Fix the typo in `README.md:10`.";
let suggestions = review_suggestions(review_text);
assert_eq!(
suggestions,
Some("- Fix the typo in `README.md:10`.".to_string())
);
}
#[test]
fn test_review_suggestions_returns_none_for_no_suggestions() {
let review_text = "\
### Summary
- Good shape.
### Suggestions
- None";
let suggestions = review_suggestions(review_text);
assert_eq!(suggestions, None);
}
#[test]
fn test_review_suggestions_returns_none_for_punctuated_no_suggestions() {
let review_text = "## Review\n\n### Suggestions\n\n- None.";
let suggestions = review_suggestions(review_text);
assert_eq!(suggestions, None);
}
#[test]
fn test_review_suggestions_returns_none_when_section_missing() {
let review_text = "\
### Summary
- Good shape overall.";
let suggestions = review_suggestions(review_text);
assert_eq!(suggestions, None);
}
#[test]
fn test_review_suggestions_stops_at_next_heading() {
let review_text = "\
### Summary
- Good shape.
### Suggestions
- Fix the typo in `README.md:10`.
### Project Impact
- Great work overall.";
let suggestions = review_suggestions(review_text);
assert_eq!(
suggestions,
Some("- Fix the typo in `README.md:10`.".to_string())
);
}
#[test]
fn test_review_suggestions_returns_none_for_empty_section() {
let review_text = "\
### Suggestions
### Project Impact
- None";
let suggestions = review_suggestions(review_text);
assert_eq!(suggestions, None);
}
#[test]
fn test_has_actionable_review_suggestions_detects_suggestions_section() {
let review_with_suggestions = "## Review\n### Suggestions\n- Fix typo\n### Notes";
let review_without_suggestions = "## Review\n### Suggestions\n- None\n### Notes";
let with_suggestions = has_actionable_review_suggestions(Some(review_with_suggestions));
let without_suggestions =
has_actionable_review_suggestions(Some(review_without_suggestions));
let missing_header = has_actionable_review_suggestions(Some("## Review"));
assert!(with_suggestions);
assert!(!without_suggestions);
assert!(!missing_header);
}
}