use serde::{Deserialize, Serialize};
use crate::model::PrState;
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct IssueLabel {
#[serde(default)]
pub name: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct IssueType {
#[serde(default)]
pub name: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct IssueMilestone {
#[serde(default)]
pub title: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct IssueSummary {
pub number: u64,
pub title: String,
pub state: String,
#[serde(default)]
pub labels: Vec<IssueLabel>,
#[serde(rename = "issueType", default)]
pub issue_type: Option<IssueType>,
#[serde(default)]
pub milestone: Option<IssueMilestone>,
#[serde(rename = "createdAt", default)]
pub created_at: String,
#[serde(default)]
pub url: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct IssueView {
pub number: u64,
pub title: String,
#[serde(default)]
pub body: String,
pub state: String,
#[serde(default)]
pub labels: Vec<IssueLabel>,
#[serde(rename = "issueType", default)]
pub issue_type: Option<IssueType>,
#[serde(default)]
pub milestone: Option<IssueMilestone>,
#[serde(rename = "createdAt", default)]
pub created_at: String,
#[serde(rename = "updatedAt", default)]
pub updated_at: String,
#[serde(default)]
pub url: String,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Author {
#[serde(default)]
pub login: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PrSummary {
pub number: u64,
pub title: String,
#[serde(default)]
pub author: Author,
pub state: String,
#[serde(rename = "isDraft", default)]
pub is_draft: bool,
#[serde(rename = "headRefName", default)]
pub head_ref_name: String,
#[serde(rename = "createdAt", default)]
pub created_at: String,
}
impl PrSummary {
pub fn pr_state(&self) -> PrState {
pr_state(&self.state, self.is_draft)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct PrView {
pub number: u64,
pub title: String,
pub state: String,
#[serde(rename = "isDraft", default)]
pub is_draft: bool,
#[serde(rename = "headRefName")]
pub head_ref_name: String,
#[serde(rename = "baseRefName")]
pub base_ref_name: String,
#[serde(default)]
pub url: String,
}
impl PrView {
pub fn pr_state(&self) -> PrState {
pr_state(&self.state, self.is_draft)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct OpenPr {
pub number: u64,
#[serde(default)]
pub url: String,
pub state: String,
#[serde(rename = "isDraft", default)]
pub is_draft: bool,
}
pub(crate) fn parse_default_branch(json: &str) -> Option<String> {
#[derive(Deserialize)]
struct Ref {
name: String,
}
#[derive(Deserialize)]
struct View {
#[serde(rename = "defaultBranchRef")]
default_branch_ref: Option<Ref>,
}
let view: View = serde_json::from_str(json).ok()?;
view.default_branch_ref.map(|r| r.name)
}
pub fn pr_state(state: &str, is_draft: bool) -> PrState {
if is_draft && state.eq_ignore_ascii_case("open") {
return PrState::Draft;
}
match state.to_ascii_lowercase().as_str() {
"closed" => PrState::Closed,
"merged" => PrState::Merged,
_ => PrState::Open,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_issue_list_json() {
let json = r#"[
{"number": 12, "title": "Broken login", "state": "OPEN",
"labels": [{"name": "bug"}], "issueType": {"name": "Bug"},
"milestone": {"title": "v2"}, "createdAt": "2024-01-15T10:30:00Z",
"url": "https://github.com/o/r/issues/12"},
{"number": 13, "title": "Bare", "state": "OPEN"}
]"#;
let issues: Vec<IssueSummary> = serde_json::from_str(json).unwrap();
assert_eq!(issues.len(), 2);
assert_eq!(issues[0].number, 12);
assert_eq!(issues[0].labels[0].name, "bug");
assert_eq!(issues[0].issue_type.as_ref().unwrap().name, "Bug");
assert_eq!(issues[0].milestone.as_ref().unwrap().title, "v2");
assert!(issues[1].labels.is_empty());
assert_eq!(issues[1].issue_type, None);
assert_eq!(issues[1].milestone, None);
assert_eq!(issues[1].url, "");
}
#[test]
fn parses_issue_view_json() {
let json = r#"{"number": 7, "title": "Add login", "body": "Please add it.",
"state": "OPEN", "labels": [{"name": "enhancement"}],
"issueType": null, "milestone": null,
"createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-02-01T09:00:00Z",
"url": "https://github.com/o/r/issues/7"}"#;
let issue: IssueView = serde_json::from_str(json).unwrap();
assert_eq!(issue.number, 7);
assert_eq!(issue.body, "Please add it.");
assert_eq!(issue.labels[0].name, "enhancement");
assert_eq!(issue.issue_type, None);
assert_eq!(issue.url, "https://github.com/o/r/issues/7");
}
#[test]
fn issue_view_tolerates_a_body_only_response() {
let issue: IssueView =
serde_json::from_str(r#"{"number": 1, "title": "T", "state": "OPEN"}"#).unwrap();
assert_eq!(issue.body, "");
assert_eq!(issue.updated_at, "");
}
#[test]
fn parses_pr_list_json() {
let json = r#"[
{"number": 42, "title": "Add login", "author": {"login": "alice"},
"state": "OPEN", "isDraft": false, "headRefName": "feature/login",
"createdAt": "2024-01-15T10:30:00Z"},
{"number": 7, "title": "WIP", "author": {"login": "bob"},
"state": "OPEN", "isDraft": true, "headRefName": "wip"}
]"#;
let prs: Vec<PrSummary> = serde_json::from_str(json).unwrap();
assert_eq!(prs.len(), 2);
assert_eq!(prs[0].number, 42);
assert_eq!(prs[0].author.login, "alice");
assert_eq!(prs[0].pr_state(), PrState::Open);
assert_eq!(prs[1].pr_state(), PrState::Draft); }
#[test]
fn parses_pr_view_json() {
let json = r#"{"number": 5, "title": "Fix", "state": "MERGED", "isDraft": false,
"headRefName": "fork-branch", "baseRefName": "main"}"#;
let view: PrView = serde_json::from_str(json).unwrap();
assert_eq!(view.number, 5);
assert_eq!(view.head_ref_name, "fork-branch");
assert_eq!(view.base_ref_name, "main");
assert_eq!(view.pr_state(), PrState::Merged);
}
#[test]
fn state_mapping() {
assert_eq!(pr_state("OPEN", false), PrState::Open);
assert_eq!(pr_state("OPEN", true), PrState::Draft);
assert_eq!(pr_state("CLOSED", false), PrState::Closed);
assert_eq!(pr_state("MERGED", false), PrState::Merged);
assert_eq!(pr_state("CLOSED", true), PrState::Closed); }
#[test]
fn parses_open_pr_list() {
let json = r#"[{"number": 77, "url": "https://github.com/o/r/pull/77",
"state": "OPEN", "isDraft": true}]"#;
let prs: Vec<OpenPr> = serde_json::from_str(json).unwrap();
assert_eq!(prs.len(), 1);
assert_eq!(prs[0].number, 77);
assert_eq!(prs[0].url, "https://github.com/o/r/pull/77");
assert!(prs[0].is_draft);
}
#[test]
fn parses_default_branch() {
assert_eq!(
parse_default_branch(r#"{"defaultBranchRef": {"name": "main"}}"#),
Some("main".to_string())
);
assert_eq!(parse_default_branch(r#"{"defaultBranchRef": null}"#), None);
assert_eq!(parse_default_branch("not json"), None);
}
}