use serde::Deserialize;
use crate::error::GitHubError;
#[derive(Debug, Clone, Deserialize)]
pub struct GitHubIssue {
pub number: u32,
pub title: String,
#[serde(default)]
pub body: String,
#[serde(default)]
pub labels: Vec<GitHubLabel>,
#[serde(default)]
pub state: IssueState,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GitHubLabel {
pub name: String,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "UPPERCASE")]
pub enum IssueState {
#[default]
Open,
Closed,
}
impl GitHubIssue {
#[must_use]
pub fn label_names(&self) -> Vec<&str> {
self.labels.iter().map(|l| l.name.as_str()).collect()
}
#[must_use]
pub fn labels_display(&self) -> String {
if self.labels.is_empty() {
String::new()
} else {
let names: Vec<_> = self.labels.iter().map(|l| l.name.as_str()).collect();
format!("[{}]", names.join(", "))
}
}
}
pub async fn fetch_issue_list() -> Result<Vec<GitHubIssue>, GitHubError> {
super::run_command(
"gh",
&[
"issue",
"list",
"--state",
"open",
"--json",
"number,title,labels,state",
"--limit",
"50",
],
"gh issue list",
)
.await
}
pub async fn fetch_issue(number: u32) -> Result<GitHubIssue, GitHubError> {
let num_str = number.to_string();
super::run_command(
"gh",
&[
"issue",
"view",
&num_str,
"--json",
"number,title,body,labels,state",
],
&format!("gh issue view {number}"),
)
.await
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rstest::rstest;
#[test]
fn github_issue_deserialize() {
let json = r#"{
"number": 22,
"title": "GitHub Issue Integration",
"body": "Add issue integration",
"labels": [{"name": "enhancement"}],
"state": "OPEN"
}"#;
let issue: GitHubIssue = serde_json::from_str(json).unwrap();
assert_eq!(issue.number, 22);
assert_eq!(issue.title, "GitHub Issue Integration");
assert_eq!(issue.labels.len(), 1);
assert_eq!(issue.labels[0].name, "enhancement");
assert_eq!(issue.state, IssueState::Open);
}
#[test]
fn github_issue_label_methods() {
let issue = GitHubIssue {
number: 1,
title: "Test".to_string(),
body: String::new(),
labels: vec![
GitHubLabel {
name: "bug".to_string(),
},
GitHubLabel {
name: "enhancement".to_string(),
},
],
state: IssueState::Open,
};
assert_eq!(issue.labels_display(), "[bug, enhancement]");
assert_eq!(issue.label_names(), vec!["bug", "enhancement"]);
}
#[test]
fn github_issue_empty_labels_display() {
let json = r#"{"number": 1, "title": "Test", "labels": []}"#;
let issue: GitHubIssue = serde_json::from_str(json).unwrap();
assert!(issue.labels.is_empty());
assert_eq!(issue.labels_display(), "");
}
#[rstest]
#[case(IssueState::Open, "OPEN")]
#[case(IssueState::Closed, "CLOSED")]
fn issue_state_deserialize(#[case] expected: IssueState, #[case] json_val: &str) {
let json = format!(r#"{{"number": 1, "title": "T", "state": "{json_val}"}}"#);
let issue: GitHubIssue = serde_json::from_str(&json).unwrap();
assert_eq!(issue.state, expected);
}
#[test]
fn issue_state_default_is_open() {
assert_eq!(IssueState::default(), IssueState::Open);
}
#[test]
fn github_issue_optional_fields_default() {
let json = r#"{"number": 1, "title": "Test"}"#;
let issue: GitHubIssue = serde_json::from_str(json).unwrap();
assert_eq!(issue.body, "");
assert!(issue.labels.is_empty());
assert_eq!(issue.state, IssueState::Open);
}
}