1use processkit::{Error, Result};
5use serde::Deserialize;
6use serde::de::DeserializeOwned;
7
8use crate::BINARY;
9
10#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
12#[non_exhaustive]
13pub struct PullRequest {
14 pub number: u64,
16 pub title: String,
18 pub state: String,
20 #[serde(rename = "headRefName", default)]
22 pub head_ref_name: String,
23 #[serde(rename = "baseRefName", default)]
25 pub base_ref_name: String,
26 #[serde(default)]
28 pub url: String,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
33#[non_exhaustive]
34pub struct Issue {
35 pub number: u64,
37 pub title: String,
39 pub state: String,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45#[non_exhaustive]
46pub struct Repo {
47 pub name: String,
49 pub owner: String,
51 pub description: Option<String>,
53 pub url: String,
55 pub is_private: bool,
57 pub default_branch: String,
59}
60
61#[derive(Deserialize)]
64struct RepoJson {
65 name: String,
66 owner: OwnerJson,
67 #[serde(default)]
68 description: Option<String>,
69 url: String,
70 #[serde(rename = "isPrivate")]
71 is_private: bool,
72 #[serde(rename = "defaultBranchRef", default)]
73 default_branch_ref: Option<BranchRefJson>,
74}
75
76#[derive(Deserialize)]
77struct OwnerJson {
78 login: String,
79}
80
81#[derive(Deserialize)]
82struct BranchRefJson {
83 name: String,
84}
85
86pub(crate) fn from_json<T: DeserializeOwned>(json: &str) -> Result<T> {
89 serde_json::from_str(json).map_err(|e| Error::Parse {
90 program: BINARY.to_string(),
91 message: e.to_string(),
92 })
93}
94
95pub(crate) fn parse_repo(json: &str) -> Result<Repo> {
97 let raw: RepoJson = from_json(json)?;
98 Ok(Repo {
99 name: raw.name,
100 owner: raw.owner.login,
101 description: raw.description,
102 url: raw.url,
103 is_private: raw.is_private,
104 default_branch: raw.default_branch_ref.map(|b| b.name).unwrap_or_default(),
105 })
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn parses_pr_list() {
114 let json = r#"[
115 {"number": 12, "title": "Add feature", "state": "OPEN",
116 "headRefName": "feat/x", "baseRefName": "main", "url": "https://gh/pr/12"}
117 ]"#;
118 let prs: Vec<PullRequest> = from_json(json).expect("parse prs");
119 assert_eq!(prs.len(), 1);
120 assert_eq!(
121 prs[0],
122 PullRequest {
123 number: 12,
124 title: "Add feature".into(),
125 state: "OPEN".into(),
126 head_ref_name: "feat/x".into(),
127 base_ref_name: "main".into(),
128 url: "https://gh/pr/12".into(),
129 }
130 );
131 }
132
133 #[test]
134 fn parses_issue_list() {
135 let json = r#"[{"number": 3, "title": "Docs", "state": "OPEN"}]"#;
136 let issues: Vec<Issue> = from_json(json).expect("parse issues");
137 assert_eq!(issues[0].number, 3);
138 }
139
140 #[test]
141 fn parses_repo_flattening_nested_objects() {
142 let json = r#"{
143 "name": "vcs-toolkit-rs",
144 "owner": {"login": "ZelAnton"},
145 "description": null,
146 "url": "https://gh/repo",
147 "isPrivate": false,
148 "defaultBranchRef": {"name": "main"}
149 }"#;
150 let repo = parse_repo(json).expect("parse repo");
151 assert_eq!(repo.name, "vcs-toolkit-rs");
152 assert_eq!(repo.owner, "ZelAnton");
153 assert_eq!(repo.description, None);
154 assert_eq!(repo.default_branch, "main");
155 assert!(!repo.is_private);
156 }
157
158 #[test]
159 fn empty_repo_has_blank_default_branch() {
160 let json = r#"{"name":"e","owner":{"login":"o"},"url":"u","isPrivate":true,"defaultBranchRef":null}"#;
161 let repo = parse_repo(json).expect("parse repo");
162 assert_eq!(repo.default_branch, "");
163 assert!(repo.is_private);
164 }
165
166 #[test]
167 fn malformed_json_is_a_parse_error() {
168 match from_json::<Vec<Issue>>("not json").unwrap_err() {
169 Error::Parse { .. } => {}
170 other => panic!("expected Parse, got {other:?}"),
171 }
172 }
173}