use serde::Deserialize;
use crate::model::PrState;
#[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_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);
}
}