1use crate::pagination::{ListOptions, QueryEncode};
8use crate::types::enums::{MergeStyle, ReviewStateType, StateType};
9use crate::{Deserialize, Serialize};
10
11#[derive(Debug, Clone)]
15pub struct ListPullRequestsOptions {
17 pub list_options: ListOptions,
18 pub state: StateType,
19 pub sort: String,
21 pub milestone: i64,
22}
23
24impl Default for ListPullRequestsOptions {
25 fn default() -> Self {
26 Self {
27 list_options: ListOptions::default(),
28 state: StateType::All,
29 sort: String::new(),
30 milestone: 0,
31 }
32 }
33}
34
35impl QueryEncode for ListPullRequestsOptions {
36 fn query_encode(&self) -> String {
37 let mut out = self.list_options.query_encode();
38 if !matches!(self.state, StateType::All) {
39 out.push_str(&format!("&state={}", self.state));
40 }
41 if !self.sort.is_empty() {
42 out.push_str(&format!("&sort={}", self.sort));
43 }
44 if self.milestone > 0 {
45 out.push_str(&format!("&milestone={}", self.milestone));
46 }
47 out
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct CreatePullRequestOption {
55 pub head: String,
56 pub base: String,
57 pub title: String,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub body: Option<String>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub assignee: Option<String>,
62 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub assignees: Vec<String>,
64 #[serde(default, skip_serializing_if = "Vec::is_empty")]
65 pub reviewers: Vec<String>,
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
67 pub team_reviewers: Vec<String>,
68 #[serde(default)]
69 pub milestone: i64,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
71 pub labels: Vec<i64>,
72 #[serde(
73 rename = "due_date",
74 default,
75 with = "nullable_rfc3339_option",
76 skip_serializing_if = "Option::is_none"
77 )]
78 pub deadline: Option<time::OffsetDateTime>,
79}
80
81mod nullable_rfc3339_option {
82 use serde::{Deserializer, Serializer};
83 use time::OffsetDateTime;
84
85 pub fn serialize<S>(opt: &Option<OffsetDateTime>, serializer: S) -> Result<S::Ok, S::Error>
87 where
88 S: Serializer,
89 {
90 match opt {
91 Some(dt) => {
92 let formatted = dt
93 .format(&time::format_description::well_known::Rfc3339)
94 .map_err(serde::ser::Error::custom)?;
95 serializer.serialize_str(&formatted)
96 }
97 None => serializer.serialize_none(),
98 }
99 }
100
101 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
103 where
104 D: Deserializer<'de>,
105 {
106 use serde::Deserialize;
107 let opt: Option<String> = Option::deserialize(deserializer)?;
108 match opt {
109 None => Ok(None),
110 Some(ref s) => {
111 let dt = OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
112 .map_err(serde::de::Error::custom)?;
113 Ok(Some(dt))
114 }
115 }
116 }
117}
118
119#[derive(Debug, Clone, Default, Serialize, Deserialize)]
121pub struct EditPullRequestOption {
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub title: Option<String>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub body: Option<String>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub base: Option<String>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub assignee: Option<String>,
131 #[serde(default, skip_serializing_if = "Vec::is_empty")]
132 pub assignees: Vec<String>,
133 #[serde(default)]
134 pub milestone: i64,
135 #[serde(default, skip_serializing_if = "Vec::is_empty")]
136 pub labels: Vec<i64>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub state: Option<StateType>,
139 #[serde(
140 rename = "due_date",
141 default,
142 with = "nullable_rfc3339_option",
143 skip_serializing_if = "Option::is_none"
144 )]
145 pub deadline: Option<time::OffsetDateTime>,
146 #[serde(rename = "unset_due_date", skip_serializing_if = "Option::is_none")]
147 pub remove_deadline: Option<bool>,
148 #[serde(
149 rename = "allow_maintainer_edit",
150 skip_serializing_if = "Option::is_none"
151 )]
152 pub allow_maintainer_edit: Option<bool>,
153}
154
155impl EditPullRequestOption {
156 pub fn validate(&self) -> crate::Result<()> {
158 if let Some(ref title) = self.title
159 && title.trim().is_empty()
160 {
161 return Err(crate::Error::Validation("title is empty".to_string()));
162 }
163 Ok(())
164 }
165}
166
167#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169pub struct MergePullRequestOption {
171 #[serde(rename = "Do", skip_serializing_if = "Option::is_none")]
172 pub style: Option<MergeStyle>,
173 #[serde(rename = "MergeCommitID", skip_serializing_if = "Option::is_none")]
174 pub merge_commit_id: Option<String>,
175 #[serde(rename = "MergeTitleField", skip_serializing_if = "Option::is_none")]
176 pub title: Option<String>,
177 #[serde(rename = "MergeMessageField", skip_serializing_if = "Option::is_none")]
178 pub message: Option<String>,
179 #[serde(rename = "delete_branch_after_merge")]
180 pub delete_branch_after_merge: bool,
181 #[serde(rename = "force_merge")]
182 pub force_merge: bool,
183 #[serde(rename = "head_commit_id", skip_serializing_if = "Option::is_none")]
184 pub head_commit_id: Option<String>,
185 #[serde(rename = "merge_when_checks_succeed")]
186 pub merge_when_checks_succeed: bool,
187}
188
189#[derive(Debug, Clone, Default)]
191pub struct PullRequestDiffOptions {
193 pub binary: bool,
195}
196
197impl QueryEncode for PullRequestDiffOptions {
198 fn query_encode(&self) -> String {
199 format!("binary={}", self.binary)
200 }
201}
202
203#[derive(Debug, Clone, Default)]
205pub struct ListPullRequestCommitsOptions {
207 pub list_options: ListOptions,
208}
209
210impl QueryEncode for ListPullRequestCommitsOptions {
211 fn query_encode(&self) -> String {
212 self.list_options.query_encode()
213 }
214}
215
216#[derive(Debug, Clone, Default)]
218pub struct ListPullRequestFilesOptions {
220 pub list_options: ListOptions,
221}
222
223impl QueryEncode for ListPullRequestFilesOptions {
224 fn query_encode(&self) -> String {
225 self.list_options.query_encode()
226 }
227}
228
229#[derive(Debug, Clone, Default)]
233pub struct ListPullReviewsOptions {
235 pub list_options: ListOptions,
236}
237
238impl QueryEncode for ListPullReviewsOptions {
239 fn query_encode(&self) -> String {
240 self.list_options.query_encode()
241 }
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct CreatePullReviewOptions {
248 #[serde(rename = "event", skip_serializing_if = "Option::is_none")]
249 pub state: Option<ReviewStateType>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub body: Option<String>,
252 #[serde(rename = "commit_id", skip_serializing_if = "Option::is_none")]
253 pub commit_id: Option<String>,
254 #[serde(default, skip_serializing_if = "Vec::is_empty")]
255 pub comments: Vec<CreatePullReviewComment>,
256}
257
258impl CreatePullReviewOptions {
259 pub fn validate(&self) -> crate::Result<()> {
261 if self.state != Some(ReviewStateType::Approved)
262 && self.comments.is_empty()
263 && self.body.as_ref().is_some_and(|b| b.trim().is_empty())
264 {
265 return Err(crate::Error::Validation("body is empty".to_string()));
266 }
267 for comment in &self.comments {
268 comment.validate()?;
269 }
270 Ok(())
271 }
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct CreatePullReviewComment {
278 pub path: String,
280 pub body: String,
281 #[serde(rename = "old_position")]
283 pub old_line_num: i64,
284 #[serde(rename = "new_position")]
286 pub new_line_num: i64,
287}
288
289impl CreatePullReviewComment {
290 pub fn validate(&self) -> crate::Result<()> {
292 if self.body.trim().is_empty() {
293 return Err(crate::Error::Validation("body is empty".to_string()));
294 }
295 if self.old_line_num != 0 && self.new_line_num != 0 {
296 return Err(crate::Error::Validation(
297 "old and new line num are set, cant identify the code comment position".to_string(),
298 ));
299 }
300 Ok(())
301 }
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct SubmitPullReviewOptions {
308 #[serde(rename = "event", skip_serializing_if = "Option::is_none")]
309 pub state: Option<ReviewStateType>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub body: Option<String>,
312}
313
314impl SubmitPullReviewOptions {
315 pub fn validate(&self) -> crate::Result<()> {
317 if self.state != Some(ReviewStateType::Approved)
318 && self.body.as_ref().is_some_and(|b| b.trim().is_empty())
319 {
320 return Err(crate::Error::Validation("body is empty".to_string()));
321 }
322 Ok(())
323 }
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct DismissPullReviewOptions {
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub message: Option<String>,
332}
333
334#[derive(Debug, Clone, Default, Serialize, Deserialize)]
336pub struct PullReviewRequestOptions {
338 #[serde(default, skip_serializing_if = "Vec::is_empty")]
339 pub reviewers: Vec<String>,
340 #[serde(default, skip_serializing_if = "Vec::is_empty")]
341 pub team_reviewers: Vec<String>,
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_edit_pull_request_option_validate_success() {
350 let opt = EditPullRequestOption {
351 title: Some("new title".to_string()),
352 ..Default::default()
353 };
354 assert!(opt.validate().is_ok());
355 }
356
357 #[test]
358 fn test_edit_pull_request_option_validate_empty_title() {
359 let opt = EditPullRequestOption {
360 title: Some(" ".to_string()),
361 ..Default::default()
362 };
363 assert!(opt.validate().is_err());
364 }
365
366 #[test]
367 fn test_create_pull_review_options_validate_approved() {
368 let opt = CreatePullReviewOptions {
369 state: Some(ReviewStateType::Approved),
370 body: None,
371 commit_id: None,
372 comments: Vec::new(),
373 };
374 assert!(opt.validate().is_ok());
375 }
376
377 #[test]
378 fn test_create_pull_review_options_validate_empty_body() {
379 let opt = CreatePullReviewOptions {
380 state: None,
381 body: Some(" ".to_string()),
382 commit_id: None,
383 comments: Vec::new(),
384 };
385 assert!(opt.validate().is_err());
386 }
387
388 #[test]
389 fn test_create_pull_review_options_validate_with_comments() {
390 let opt = CreatePullReviewOptions {
391 state: None,
392 body: Some(" ".to_string()),
393 commit_id: None,
394 comments: vec![CreatePullReviewComment {
395 path: "main.rs".to_string(),
396 body: "fix this".to_string(),
397 old_line_num: 0,
398 new_line_num: 10,
399 }],
400 };
401 assert!(opt.validate().is_ok());
402 }
403
404 #[test]
405 fn test_create_pull_review_comment_validate_success() {
406 let comment = CreatePullReviewComment {
407 path: "main.rs".to_string(),
408 body: "fix this".to_string(),
409 old_line_num: 0,
410 new_line_num: 10,
411 };
412 assert!(comment.validate().is_ok());
413 }
414
415 #[test]
416 fn test_create_pull_review_comment_validate_empty_body() {
417 let comment = CreatePullReviewComment {
418 path: "main.rs".to_string(),
419 body: String::new(),
420 old_line_num: 0,
421 new_line_num: 10,
422 };
423 assert!(comment.validate().is_err());
424 }
425
426 #[test]
427 fn test_create_pull_review_comment_validate_both_lines_set() {
428 let comment = CreatePullReviewComment {
429 path: "main.rs".to_string(),
430 body: "fix this".to_string(),
431 old_line_num: 5,
432 new_line_num: 10,
433 };
434 assert!(comment.validate().is_err());
435 }
436
437 #[test]
438 fn test_submit_pull_review_options_validate_approved() {
439 let opt = SubmitPullReviewOptions {
440 state: Some(ReviewStateType::Approved),
441 body: None,
442 };
443 assert!(opt.validate().is_ok());
444 }
445
446 #[test]
447 fn test_submit_pull_review_options_validate_empty_body() {
448 let opt = SubmitPullReviewOptions {
449 state: None,
450 body: Some(" ".to_string()),
451 };
452 assert!(opt.validate().is_err());
453 }
454}