1use std::collections::HashMap;
2
3use serde::{de::DeserializeOwned, Deserialize};
4use serde_json::Value;
5
6#[derive(Deserialize, Debug)]
7#[serde(untagged)]
8pub enum License {
9 Object {
10 key: String,
11 name: String,
12 spdx_id: String,
13 url: String,
14 },
15 Name(String),
16}
17
18#[derive(Deserialize, Debug)]
19pub struct Repository {
20 pub name: String,
21 pub full_name: String,
22 pub private: bool,
23 pub owner: User,
24 pub html_url: String,
25 pub description: Option<String>,
26 pub fork: bool,
27 pub created_at: Value,
28 pub updated_at: String,
29 pub pushed_at: Value,
30 pub git_url: String,
31 pub ssh_url: String,
32 pub clone_url: String,
33 pub svn_url: String,
34 pub homepage: Option<String>,
35 pub stargazers_count: u32,
36 pub watchers_count: u32,
37 pub language: Option<String>,
38 pub has_issues: bool,
39 pub has_projects: bool,
40 pub has_downloads: bool,
41 pub has_wiki: bool,
42 pub has_pages: bool,
43 pub forks_count: u32,
44 pub mirror_url: Option<String>,
45 pub archived: bool,
46 pub disabled: bool,
47 pub open_issues_count: u32,
48 pub license: Option<License>,
49 pub visibility: String,
50 pub forks: u32,
51 pub open_issues: u32,
52 pub watchers: u32,
53 pub default_branch: String,
54}
55
56#[derive(Deserialize, Debug)]
57pub struct Organization {
58 pub login: String,
59 pub avatar_url: String,
60 pub description: Option<String>,
61}
62
63#[derive(Deserialize, Debug)]
64pub struct User {
65 pub login: String,
66 pub email: Option<String>,
67 pub avatar_url: String,
68 pub html_url: String,
69 pub r#type: String,
70 pub site_admin: bool,
71}
72
73#[derive(Deserialize, Debug)]
74pub struct DiscussionCategory {
75 pub name: String,
76 pub description: String,
77 pub emoji: Option<String>,
78 pub created_at: String,
79 pub updated_at: String,
80 pub slug: Option<String>,
81 pub is_answerable: bool,
82}
83
84#[derive(Deserialize, Debug)]
85pub struct Comment {
86 pub html_url: String,
87 pub user: User,
88 pub position: Option<u32>,
89 pub path: Option<String>,
90 pub created_at: String,
91 pub updated_at: String,
92 pub author_association: String,
93 pub body: String,
94}
95
96#[derive(Deserialize, Debug)]
97pub struct Discussion {
98 pub category: DiscussionCategory,
99 pub answer_html_url: Option<String>,
100 pub answer_chosen_at: Option<String>,
101 pub answer_chosen_by: Option<String>,
102 pub html_url: String,
103 pub user: User,
104 pub number: u32,
105 pub title: String,
106 pub state: String,
107 pub locked: bool,
108 pub comments: u32,
109 pub created_at: String,
110 pub updated_at: String,
111 pub author_association: String,
112 pub active_lock_reason: Option<String>,
113 pub body: String,
114}
115
116type Fork = Repository;
117
118#[derive(Deserialize, Debug)]
119pub struct Label {
120 pub name: String,
121 pub color: String,
122 pub default: bool,
123}
124
125#[derive(Deserialize, Debug)]
126pub struct Issue {
127 pub active_lock_reason: Option<String>,
128 pub html_url: String,
129 pub number: u32,
130 pub title: String,
131 pub user: User,
132 pub labels: Vec<Label>,
133 pub state: String,
134 pub state_reason: Option<String>,
135 pub locked: bool,
136 pub assignees: Vec<User>,
137 pub comments: u32,
138 pub created_at: String,
139 pub updated_at: String,
140 pub closed_at: Option<String>,
141 pub author_association: String,
142 pub body: Option<String>,
143}
144
145#[derive(Deserialize, Debug)]
146pub struct Plan {
147 pub name: String,
148 pub description: String,
149 pub monthly_price_in_cents: u32,
150 pub yearly_price_in_cents: u32,
151 pub price_model: String,
152 pub has_free_trial: bool,
153 pub unit_name: String,
154 pub bullets: Vec<String>,
155}
156
157#[derive(Deserialize, Debug)]
158pub struct MarketplacePurchase {
159 pub billing_cycle: String,
160 pub unit_count: u32,
161 pub on_free_trial: bool,
162 pub free_trial_ends_on: Option<String>,
163 pub next_billing_date: String,
164 pub plan: Plan,
165}
166
167#[derive(Deserialize, Debug)]
168pub struct PrPoint {
169 pub label: String,
170 pub r#ref: String,
171 pub sha: String,
172 pub user: User,
173 pub repo: Repository,
174}
175
176#[derive(Deserialize, Debug)]
177pub struct PullRequest {
178 pub html_url: String,
179 pub diff_url: String,
180 pub patch_url: String,
181 pub number: u32,
182 pub state: String,
183 pub locked: bool,
184 pub title: String,
185 pub user: User,
186 pub body: Option<String>,
187 pub created_at: String,
188 pub updated_at: String,
189 pub closed_at: Option<String>,
190 pub merged_at: Option<String>,
191 pub merge_commit_sha: Option<String>,
192 pub assignees: Vec<User>,
193 pub requested_reviewers: Vec<User>,
194 pub labels: Vec<Label>,
195 pub head: PrPoint,
196 pub base: PrPoint,
197 pub author_association: String,
198 pub draft: Option<bool>,
199 pub merged: Option<bool>,
200 pub mergeable: Option<bool>,
201 pub mergeable_state: Option<String>,
202 pub merged_by: Option<User>,
203 pub comments: Option<u32>,
204 pub review_comments: Option<u32>,
205 pub maintainer_can_modify: Option<bool>,
206 pub commits: Option<u32>,
207 pub additions: Option<u32>,
208 pub deletions: Option<u32>,
209 pub changed_files: Option<u32>,
210}
211
212#[derive(Deserialize, Debug)]
213pub struct Review {
214 pub user: User,
215 pub body: Option<String>,
216 pub commit_id: String,
217 pub submitted_at: String,
218 pub state: String,
219 pub html_url: String,
220 pub author_association: String,
221}
222
223#[derive(Deserialize, Debug)]
224pub struct ReleaseAsset {
225 pub browser_download_url: String,
226 pub name: String,
227 pub label: Option<String>,
228 pub state: String,
229 pub content_type: String,
230 pub size: i64,
231 pub download_count: i64,
232 pub created_at: String,
233 pub updated_at: String,
234 pub uploader: User,
235}
236
237#[derive(Deserialize, Debug)]
238pub struct Release {
239 pub html_url: String,
240 pub tag_name: String,
241 pub target_commitish: String,
242 pub name: Option<String>,
243 pub draft: bool,
244 pub author: User,
245 pub assets: Vec<ReleaseAsset>,
246 pub prerelease: bool,
247 pub created_at: String,
248 pub published_at: String,
249 pub body: Option<String>,
250}
251
252#[derive(Deserialize, Debug)]
253pub struct WorkflowStep {
254 pub name: String,
255 pub status: String,
256 pub conclusion: Option<String>,
257 pub number: u32,
258 pub started_at: Option<String>,
259 pub completed_at: Option<String>,
260}
261
262#[derive(Deserialize, Debug)]
263pub struct WorkflowJob {
264 pub html_url: String,
265 pub status: String,
266 pub conclusion: Option<String>,
267 pub started_at: String,
268 pub completed_at: Option<String>,
269 pub name: String,
270 pub steps: Vec<WorkflowStep>,
271 pub labels: Vec<String>,
272 pub runner_id: Option<u32>,
273 pub runner_name: Option<String>,
274 pub runner_group_id: Option<u32>,
275 pub runner_group_name: Option<String>,
276}
277
278#[derive(Deserialize, Debug)]
279pub struct GitUser {
280 pub name: String,
281 pub email: String,
282 pub username: Option<String>,
283}
284
285#[derive(Deserialize, Debug)]
286pub struct Commit {
287 pub distinct: bool,
288 #[serde(default)]
289 pub message: String,
290 pub timestamp: String,
291 pub url: String,
292 pub author: GitUser,
293 pub committer: GitUser,
294 pub added: Vec<String>,
295 pub removed: Vec<String>,
296 pub modified: Vec<String>,
297}
298
299#[derive(Deserialize, Debug)]
300pub struct InboundData {
301 pub sender: User,
302
303 pub action: Option<String>,
304 pub repository: Option<Repository>,
305 pub organization: Option<Organization>,
306 pub comment: Option<Comment>,
307 pub discussion: Option<Discussion>,
308 pub forkee: Option<Fork>,
309 pub issue: Option<Issue>,
310 pub label: Option<Label>,
311 pub marketplace_purchase: Option<MarketplacePurchase>,
312 pub pull_request: Option<PullRequest>,
313 pub review: Option<Review>,
314 pub release: Option<Release>,
315 pub starred_at: Option<String>,
316 pub workflow_job: Option<WorkflowJob>,
317 pub head_commit: Option<Commit>,
318 pub commits: Option<Vec<Commit>>,
319
320 #[serde(flatten)]
321 pub extra: HashMap<String, Value>,
322}
323
324impl InboundData {
325 #[inline]
332 pub fn get<T: DeserializeOwned, I: ToString>(&self, index: &I) -> Result<T, String> {
333 serde_json::from_value(
334 self.extra
335 .get(&index.to_string())
336 .ok_or(format!("Missing {}", index.to_string()))?
337 .clone(),
338 )
339 .map_err(|e| e.to_string())
340 }
341
342 #[inline]
343 pub fn get_action(&self) -> Result<&String, String> {
344 self.action.as_ref().ok_or("Missing action".to_string())
345 }
346
347 #[inline]
348 pub fn get_repository(&self) -> Result<&Repository, String> {
349 self.repository
350 .as_ref()
351 .ok_or("Missing repository".to_string())
352 }
353
354 #[inline]
355 pub fn get_comment(&self) -> Result<&Comment, String> {
356 self.comment.as_ref().ok_or("Missing comment".to_string())
357 }
358
359 #[inline]
360 pub fn get_discussion(&self) -> Result<&Discussion, String> {
361 self.discussion
362 .as_ref()
363 .ok_or("Missing discussion".to_string())
364 }
365
366 #[inline]
367 pub fn get_fork(&self) -> Result<&Fork, String> {
368 self.forkee.as_ref().ok_or("Missing forkee".to_string())
369 }
370
371 #[inline]
372 pub fn get_issue(&self) -> Result<&Issue, String> {
373 self.issue.as_ref().ok_or("Missing issue".to_string())
374 }
375
376 #[inline]
377 pub fn get_label(&self) -> Result<&Label, String> {
378 self.label.as_ref().ok_or("Missing label".to_string())
379 }
380
381 #[inline]
382 pub fn get_marketplace_purchase(&self) -> Result<&MarketplacePurchase, String> {
383 self.marketplace_purchase
384 .as_ref()
385 .ok_or("Missing marketplace_purchase".to_string())
386 }
387
388 #[inline]
389 pub fn get_pull_request(&self) -> Result<&PullRequest, String> {
390 self.pull_request
391 .as_ref()
392 .ok_or("Missing pull_request".to_string())
393 }
394
395 #[inline]
396 pub fn get_review(&self) -> Result<&Review, String> {
397 self.review.as_ref().ok_or("Missing review".to_string())
398 }
399
400 #[inline]
401 pub fn get_release(&self) -> Result<&Release, String> {
402 self.release.as_ref().ok_or("Missing release".to_string())
403 }
404
405 #[inline]
406 pub fn get_starred_at(&self) -> Result<&String, String> {
407 self.starred_at
408 .as_ref()
409 .ok_or("Missing starred_at".to_string())
410 }
411
412 #[inline]
413 pub fn get_workflow_job(&self) -> Result<&WorkflowJob, String> {
414 self.workflow_job
415 .as_ref()
416 .ok_or("Missing workflow_job".to_string())
417 }
418
419 pub fn get_head_commit(&self) -> Result<&Commit, String> {
420 self.head_commit
421 .as_ref()
422 .ok_or("Missing head_commit".to_string())
423 }
424
425 pub fn get_commits(&self) -> Result<&Vec<Commit>, String> {
426 self.commits.as_ref().ok_or("Missing commits".to_string())
427 }
428}
429
430pub fn inbound(s: String) -> Result<InboundData, String> {
432 #[cfg(debug_assertions)]
433 return serde_json::from_str::<InboundData>(&s)
434 .map_err(|e| format!("Parsing GitHub Webhook payload failed: {}", e.to_string()));
435
436 #[cfg(not(debug_assertions))]
437 serde_json::from_str::<InboundData>(&s)
438 .map_err(|_| format!("Parsing GitHub Webhook payload failed: {}", s))
439}
440
441pub mod outbound {
442 use std::collections::HashMap;
443
444 use serde::Serialize;
445 use serde_json::{json, Value};
446
447 #[derive(Serialize)]
448 pub struct OutboundData<'a> {
449 #[serde(flatten)]
450 inner: HashMap<&'a str, Value>,
451 }
452
453 impl<'a> OutboundData<'a> {
454 pub fn body<S: ToString + Serialize>(mut self, body: S) -> OutboundData<'a> {
456 self.inner.insert("body", json!(body));
457 self
458 }
459
460 pub fn milestone(mut self, milestone: Value) -> OutboundData<'a> {
462 self.inner.insert("milestone", milestone);
463 self
464 }
465
466 pub fn labels<S: ToString + Serialize>(mut self, labels: Vec<S>) -> OutboundData<'a> {
468 self.inner.insert("labels", json!(labels));
469 self
470 }
471
472 pub fn assignees<S: ToString + Serialize>(mut self, assignees: Vec<S>) -> OutboundData<'a> {
474 self.inner.insert("assignees", json!(assignees));
475 self
476 }
477
478 pub fn build(self) -> Result<String, String> {
480 if self.inner.len() < 2 {
481 return Err("OutboundData build failed: Too few fields".to_string());
482 }
483
484 serde_json::to_string(&self)
485 .map_err(|e| format!("OutboundData build failed: {}", e.to_string()))
486 }
487 }
488
489 pub fn create_issue<'a, S: ToString + Serialize>(title: S) -> OutboundData<'a> {
491 OutboundData {
492 inner: [("title", json!(title))]
493 .into_iter()
494 .collect::<HashMap<&str, Value>>(),
495 }
496 }
497
498 pub fn modify_issue<'a>(issue_number: u32) -> OutboundData<'a> {
501 OutboundData {
502 inner: [("issue_number", json!(issue_number))]
503 .into_iter()
504 .collect::<HashMap<&str, Value>>(),
505 }
506 }
507
508 pub fn merge_pull<'a>(pull_number: u32) -> OutboundData<'a> {
510 OutboundData {
511 inner: [("pull_number", json!(pull_number))]
512 .into_iter()
513 .collect::<HashMap<&str, Value>>(),
514 }
515 }
516}