1use serde::Deserialize;
6
7#[derive(Debug, Deserialize)]
8pub struct SearchReposResponse {
9 pub items: Vec<RepoItem>,
10}
11
12#[derive(Debug, Deserialize)]
13pub struct RepoItem {
14 pub full_name: String,
15}
16
17#[derive(Debug, Deserialize)]
18pub struct SearchUsersResponse {
19 pub items: Vec<UserItem>,
20}
21
22#[derive(Debug, Deserialize)]
23pub struct UserItem {
24 pub login: String,
25}
26
27#[derive(Debug, Deserialize)]
28pub struct OrgRepoItem {
29 pub name: String,
30 #[serde(default)]
31 pub description: Option<String>,
32 #[serde(default)]
33 pub private: bool,
34 #[serde(default)]
35 pub archived: bool,
36 #[serde(default)]
37 pub pushed_at: Option<String>,
38}
39
40#[derive(Debug, Deserialize)]
41pub struct RepoMeta {
42 pub default_branch: String,
43}
44
45#[derive(Debug, Deserialize)]
47pub struct SearchCodeResponse {
48 #[serde(default)]
49 pub total_count: u64,
50 pub items: Vec<CodeItem>,
51}
52
53#[derive(Debug, Deserialize)]
54pub struct CodeItem {
55 pub path: String,
56 pub sha: String,
57 pub repository: CodeRepo,
58 #[serde(default)]
59 pub text_matches: Vec<TextMatch>,
60}
61
62#[derive(Debug, Deserialize)]
63pub struct CodeRepo {
64 pub full_name: String,
65 #[serde(default)]
66 pub default_branch: Option<String>,
67}
68
69#[derive(Debug, Deserialize)]
73pub struct TextMatch {
74 pub fragment: String,
75 #[serde(default)]
76 pub matches: Vec<MatchRange>,
77}
78
79#[derive(Debug, Deserialize)]
80pub struct MatchRange {
81 pub text: String,
82}
83
84#[derive(Debug, Deserialize)]
89pub struct CommitResponse {
90 pub sha: String,
91 pub commit: CommitMeta,
92 #[serde(default)]
93 pub parents: Vec<CommitParent>,
94 #[serde(default)]
95 pub files: Vec<CommitFileItem>,
96 #[serde(default)]
97 pub html_url: Option<String>,
98 #[serde(skip)]
99 pub truncated: bool,
100}
101
102#[derive(Debug, Deserialize)]
103pub struct CommitMeta {
104 pub message: String,
105 pub author: Option<CommitAuthor>,
106}
107
108#[derive(Debug, Deserialize)]
109pub struct CommitAuthor {
110 pub name: Option<String>,
111 pub date: Option<String>,
112}
113
114#[derive(Debug, Deserialize)]
115pub struct CommitParent {
116 pub sha: String,
117}
118
119#[derive(Debug, Deserialize)]
120pub struct CommitFileItem {
121 pub filename: String,
122 #[serde(default)]
123 pub status: Option<String>,
124 #[serde(default)]
126 pub additions: Option<u32>,
127 #[serde(default)]
128 pub deletions: Option<u32>,
129 pub patch: Option<String>,
130 #[serde(default)]
131 pub previous_filename: Option<String>,
132}
133
134#[derive(Debug, Clone, Deserialize, serde::Serialize)]
136pub struct TreeResponse {
137 pub sha: String,
138 #[serde(default)]
139 pub truncated: bool,
140 pub tree: Vec<TreeEntry>,
141}
142
143#[derive(Debug, Clone, Deserialize, serde::Serialize)]
144pub struct TreeEntry {
145 pub path: String,
146 #[serde(rename = "type")]
147 pub kind: String, pub sha: String,
149 #[serde(default)]
150 pub size: Option<u64>,
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn parses_repo_search_payload() {
159 let json = r#"{"total_count": 1, "items": [{"full_name": "ratatui/ratatui"}]}"#;
160 let parsed: SearchReposResponse = serde_json::from_str(json).unwrap();
161 assert_eq!(parsed.items[0].full_name, "ratatui/ratatui");
162 }
163
164 #[test]
165 fn parses_org_search_payload() {
166 let json = r#"{"items": [{"login": "tokio-rs", "type": "Organization"}]}"#;
167 let parsed: SearchUsersResponse = serde_json::from_str(json).unwrap();
168 assert_eq!(parsed.items[0].login, "tokio-rs");
169 }
170
171 #[test]
172 fn parses_tree_payload() {
173 let json = r#"{
174 "sha": "abc123",
175 "truncated": false,
176 "tree": [
177 {"path": "src", "mode": "040000", "type": "tree", "sha": "d1"},
178 {"path": "src/lib.rs", "mode": "100644", "type": "blob", "sha": "f1", "size": 42}
179 ]
180 }"#;
181 let parsed: TreeResponse = serde_json::from_str(json).unwrap();
182 assert_eq!(parsed.tree.len(), 2);
183 assert_eq!(parsed.tree[0].kind, "tree");
184 assert_eq!(parsed.tree[1].size, Some(42));
185 }
186
187 #[test]
188 fn parses_commit_payload() {
189 let json = r#"{
190 "sha": "6dcb09b5",
191 "commit": {
192 "message": "Fix the race\n\nBody line.",
193 "author": {"name": "octocat", "date": "2026-09-06T10:00:00Z"}
194 },
195 "parents": [{"sha": "aaa111"}, {"sha": "bbb222"}],
196 "files": [
197 {"filename": "src/lib.rs", "status": "modified", "additions": 2,
198 "deletions": 1, "patch": "@@ -1 +1,2 @@\n-old\n+new\n+line"},
199 {"filename": "src/old.rs", "status": "removed", "additions": 0, "deletions": 9},
200 {"filename": "src/moved.rs", "status": "renamed", "previous_filename": "src/orig.rs",
201 "additions": 1, "deletions": 1},
202 {"filename": "img/logo.png", "status": "changed", "additions": 3, "deletions": 0}
203 ]
204 }"#;
205 let parsed: CommitResponse = serde_json::from_str(json).unwrap();
206 assert_eq!(parsed.commit.message, "Fix the race\n\nBody line.");
207 assert_eq!(
208 (
209 parsed.commit.author.as_ref().unwrap().name.as_deref(),
210 parsed.commit.author.as_ref().unwrap().date.as_deref()
211 ),
212 (Some("octocat"), Some("2026-09-06T10:00:00Z"))
213 );
214 assert_eq!(parsed.parents.len(), 2);
215 assert_eq!(parsed.parents[1].sha, "bbb222");
216 assert_eq!(
217 parsed.files[0].patch.as_deref(),
218 Some("@@ -1 +1,2 @@\n-old\n+new\n+line")
219 );
220 assert_eq!(parsed.files[1].deletions, Some(9));
221 assert_eq!(
222 parsed.files[2].previous_filename.as_deref(),
223 Some("src/orig.rs")
224 );
225 assert_eq!(parsed.files[3].patch, None);
228 assert_eq!(parsed.files[3].status.as_deref(), Some("changed"));
229 }
230}