1use crate::internal::request::urlencoding;
8use crate::pagination::{ListOptions, QueryEncode};
9use crate::types::enums::StateType;
10use crate::types::serde_helpers::nullable_rfc3339;
11use crate::{Deserialize, Serialize};
12use time::OffsetDateTime;
13
14#[derive(Debug, Clone, Default)]
18pub struct ListIssueOption {
20 pub list_options: ListOptions,
21 pub state: Option<StateType>,
22 pub r#type: Option<crate::types::enums::IssueType>,
23 pub labels: Vec<String>,
24 pub milestones: Vec<String>,
25 pub key_word: String,
26 pub since: Option<OffsetDateTime>,
27 pub before: Option<OffsetDateTime>,
28 pub created_by: String,
30 pub assigned_by: String,
32 pub mentioned_by: String,
34 pub owner: String,
36 pub team: String,
38}
39
40impl QueryEncode for ListIssueOption {
41 fn query_encode(&self) -> String {
42 let mut out = self.list_options.query_encode();
43
44 if let Some(ref state) = self.state {
45 out.push_str(&format!("&state={}", state.as_ref()));
46 }
47
48 if !self.labels.is_empty() {
49 out.push_str(&format!("&labels={}", self.labels.join(",")));
50 }
51
52 if !self.key_word.is_empty() {
53 out.push_str(&format!("&q={}", urlencoding(&self.key_word)));
54 }
55
56 if let Some(ref t) = self.r#type {
57 out.push_str(&format!("&type={}", t.as_ref()));
58 }
59
60 if !self.milestones.is_empty() {
61 out.push_str(&format!("&milestones={}", self.milestones.join(",")));
62 }
63
64 if let Some(since) = self.since {
65 let formatted = since
66 .format(&time::format_description::well_known::Rfc3339)
67 .unwrap_or_default();
68 out.push_str(&format!("&since={}", urlencoding(&formatted)));
69 }
70 if let Some(before) = self.before {
71 let formatted = before
72 .format(&time::format_description::well_known::Rfc3339)
73 .unwrap_or_default();
74 out.push_str(&format!("&before={}", urlencoding(&formatted)));
75 }
76
77 if !self.created_by.is_empty() {
78 out.push_str(&format!("&created_by={}", urlencoding(&self.created_by)));
79 }
80 if !self.assigned_by.is_empty() {
81 out.push_str(&format!("&assigned_by={}", urlencoding(&self.assigned_by)));
82 }
83 if !self.mentioned_by.is_empty() {
84 out.push_str(&format!(
85 "&mentioned_by={}",
86 urlencoding(&self.mentioned_by)
87 ));
88 }
89 if !self.owner.is_empty() {
90 out.push_str(&format!("&owner={}", urlencoding(&self.owner)));
91 }
92 if !self.team.is_empty() {
93 out.push_str(&format!("&team={}", urlencoding(&self.team)));
94 }
95
96 out
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct CreateIssueOption {
104 pub title: String,
105 pub body: String,
106 #[serde(default)]
107 pub r#ref: String,
108 #[serde(default)]
109 pub assignees: Vec<String>,
110 #[serde(
111 rename = "due_date",
112 default,
113 with = "nullable_rfc3339",
114 skip_serializing_if = "Option::is_none"
115 )]
116 pub deadline: Option<OffsetDateTime>,
117 #[serde(default)]
119 pub milestone: i64,
120 #[serde(default)]
122 pub labels: Vec<i64>,
123 #[serde(default)]
124 pub closed: bool,
125}
126
127impl CreateIssueOption {
128 pub fn validate(&self) -> crate::Result<()> {
130 if self.title.trim().is_empty() {
131 return Err(crate::Error::Validation("title is empty".to_string()));
132 }
133 Ok(())
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct EditIssueOption {
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub title: Option<String>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub body: Option<String>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub r#ref: Option<String>,
147 #[serde(default)]
148 pub assignees: Vec<String>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub milestone: Option<i64>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub state: Option<StateType>,
153 #[serde(
154 rename = "due_date",
155 default,
156 with = "nullable_rfc3339",
157 skip_serializing_if = "Option::is_none"
158 )]
159 pub deadline: Option<OffsetDateTime>,
160 #[serde(
161 rename = "unset_due_date",
162 default,
163 skip_serializing_if = "Option::is_none"
164 )]
165 pub remove_deadline: Option<bool>,
166}
167
168impl EditIssueOption {
169 pub fn validate(&self) -> crate::Result<()> {
171 if let Some(ref title) = self.title
172 && title.trim().is_empty()
173 {
174 return Err(crate::Error::Validation("title is empty".to_string()));
175 }
176 Ok(())
177 }
178}
179
180#[derive(Debug, Clone, Default)]
184pub struct ListIssueCommentOptions {
186 pub list_options: ListOptions,
187 pub since: Option<OffsetDateTime>,
188 pub before: Option<OffsetDateTime>,
189}
190
191impl QueryEncode for ListIssueCommentOptions {
192 fn query_encode(&self) -> String {
193 let mut out = self.list_options.query_encode();
194 if let Some(since) = self.since {
195 let formatted = since
196 .format(&time::format_description::well_known::Rfc3339)
197 .unwrap_or_default();
198 out.push_str(&format!("&since={}", urlencoding(&formatted)));
199 }
200 if let Some(before) = self.before {
201 let formatted = before
202 .format(&time::format_description::well_known::Rfc3339)
203 .unwrap_or_default();
204 out.push_str(&format!("&before={}", urlencoding(&formatted)));
205 }
206 out
207 }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct CreateIssueCommentOption {
214 pub body: String,
215}
216
217impl CreateIssueCommentOption {
218 pub fn validate(&self) -> crate::Result<()> {
220 if self.body.is_empty() {
221 return Err(crate::Error::Validation("body is empty".to_string()));
222 }
223 Ok(())
224 }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct EditIssueCommentOption {
231 pub body: String,
232}
233
234impl EditIssueCommentOption {
235 pub fn validate(&self) -> crate::Result<()> {
237 if self.body.is_empty() {
238 return Err(crate::Error::Validation("body is empty".to_string()));
239 }
240 Ok(())
241 }
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct IssueLabelsOption {
250 #[serde(default)]
252 pub labels: Vec<i64>,
253}
254
255#[derive(Debug, Clone, Default)]
259pub struct ListMilestoneOption {
261 pub list_options: ListOptions,
262 pub state: Option<StateType>,
264 pub name: String,
265}
266
267impl QueryEncode for ListMilestoneOption {
268 fn query_encode(&self) -> String {
269 let mut out = self.list_options.query_encode();
270 if let Some(ref state) = self.state {
271 out.push_str(&format!("&state={}", state.as_ref()));
272 }
273 if !self.name.is_empty() {
274 out.push_str(&format!("&name={}", urlencoding(&self.name)));
275 }
276 out
277 }
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct CreateMilestoneOption {
284 pub title: String,
285 #[serde(default)]
286 pub description: String,
287 pub state: StateType,
288 #[serde(
289 rename = "due_on",
290 default,
291 with = "nullable_rfc3339",
292 skip_serializing_if = "Option::is_none"
293 )]
294 pub deadline: Option<OffsetDateTime>,
295}
296
297impl CreateMilestoneOption {
298 pub fn validate(&self) -> crate::Result<()> {
300 if self.title.trim().is_empty() {
301 return Err(crate::Error::Validation("title is empty".to_string()));
302 }
303 Ok(())
304 }
305}
306
307#[derive(Debug, Clone, Default, Serialize, Deserialize)]
309pub struct EditMilestoneOption {
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub title: Option<String>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub description: Option<String>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub state: Option<StateType>,
317 #[serde(
318 rename = "due_on",
319 default,
320 with = "nullable_rfc3339",
321 skip_serializing_if = "Option::is_none"
322 )]
323 pub deadline: Option<OffsetDateTime>,
324}
325
326impl EditMilestoneOption {
327 pub fn validate(&self) -> crate::Result<()> {
329 if let Some(ref title) = self.title
330 && title.trim().is_empty()
331 {
332 return Err(crate::Error::Validation("title is empty".to_string()));
333 }
334 Ok(())
335 }
336}
337
338#[derive(Debug, Clone, Default)]
342pub struct ListIssueReactionsOptions {
344 pub list_options: ListOptions,
345}
346
347impl QueryEncode for ListIssueReactionsOptions {
348 fn query_encode(&self) -> String {
349 self.list_options.query_encode()
350 }
351}
352
353#[derive(Debug, Clone, Default)]
357pub struct ListIssueSubscribersOptions {
359 pub list_options: ListOptions,
360}
361
362impl QueryEncode for ListIssueSubscribersOptions {
363 fn query_encode(&self) -> String {
364 self.list_options.query_encode()
365 }
366}
367
368#[derive(Debug, Clone, Default)]
372pub struct ListStopwatchesOptions {
374 pub list_options: ListOptions,
375}
376
377impl QueryEncode for ListStopwatchesOptions {
378 fn query_encode(&self) -> String {
379 self.list_options.query_encode()
380 }
381}
382
383#[derive(Debug, Clone, Default)]
387pub struct ListTrackedTimesOptions {
389 pub list_options: ListOptions,
390 pub since: Option<OffsetDateTime>,
391 pub before: Option<OffsetDateTime>,
392 pub user: String,
394}
395
396impl QueryEncode for ListTrackedTimesOptions {
397 fn query_encode(&self) -> String {
398 let mut out = self.list_options.query_encode();
399 if let Some(since) = self.since {
400 let formatted = since
401 .format(&time::format_description::well_known::Rfc3339)
402 .unwrap_or_default();
403 out.push_str(&format!("&since={}", urlencoding(&formatted)));
404 }
405 if let Some(before) = self.before {
406 let formatted = before
407 .format(&time::format_description::well_known::Rfc3339)
408 .unwrap_or_default();
409 out.push_str(&format!("&before={}", urlencoding(&formatted)));
410 }
411 if !self.user.is_empty() {
412 out.push_str(&format!("&user={}", urlencoding(&self.user)));
413 }
414 out
415 }
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct AddTimeOption {
422 pub time: i64,
424 #[serde(
426 default,
427 with = "nullable_rfc3339",
428 skip_serializing_if = "Option::is_none"
429 )]
430 pub created: Option<OffsetDateTime>,
431 #[serde(default, rename = "user_name")]
433 pub user: String,
434}
435
436impl AddTimeOption {
437 pub fn validate(&self) -> crate::Result<()> {
439 if self.time == 0 {
440 return Err(crate::Error::Validation("no time to add".to_string()));
441 }
442 Ok(())
443 }
444}
445
446#[derive(Debug, Clone, Default)]
450pub struct ListIssueBlocksOptions {
452 pub list_options: ListOptions,
453}
454
455impl QueryEncode for ListIssueBlocksOptions {
456 fn query_encode(&self) -> String {
457 self.list_options.query_encode()
458 }
459}
460
461#[derive(Debug, Clone, Default)]
463pub struct ListIssueDependenciesOptions {
465 pub list_options: ListOptions,
466}
467
468impl QueryEncode for ListIssueDependenciesOptions {
469 fn query_encode(&self) -> String {
470 self.list_options.query_encode()
471 }
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct LockIssueOption {
478 #[serde(default, rename = "lock_reason")]
479 pub lock_reason: String,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484pub struct EditDeadlineOption {
486 #[serde(
487 rename = "due_date",
488 default,
489 with = "nullable_rfc3339",
490 skip_serializing_if = "Option::is_none"
491 )]
492 pub deadline: Option<OffsetDateTime>,
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn test_create_issue_option_validate_success() {
501 let opt = CreateIssueOption {
502 title: "bug report".to_string(),
503 body: String::new(),
504 r#ref: String::new(),
505 assignees: Vec::new(),
506 deadline: None,
507 milestone: 0,
508 labels: Vec::new(),
509 closed: false,
510 };
511 assert!(opt.validate().is_ok());
512 }
513
514 #[test]
515 fn test_create_issue_option_validate_empty_title() {
516 let opt = CreateIssueOption {
517 title: String::new(),
518 body: String::new(),
519 r#ref: String::new(),
520 assignees: Vec::new(),
521 deadline: None,
522 milestone: 0,
523 labels: Vec::new(),
524 closed: false,
525 };
526 assert!(opt.validate().is_err());
527 }
528
529 #[test]
530 fn test_create_issue_option_validate_whitespace_title() {
531 let opt = CreateIssueOption {
532 title: " ".to_string(),
533 body: String::new(),
534 r#ref: String::new(),
535 assignees: Vec::new(),
536 deadline: None,
537 milestone: 0,
538 labels: Vec::new(),
539 closed: false,
540 };
541 assert!(opt.validate().is_err());
542 }
543
544 #[test]
545 fn test_edit_issue_option_validate_success() {
546 let opt = EditIssueOption {
547 title: Some("new title".to_string()),
548 body: None,
549 r#ref: None,
550 assignees: Vec::new(),
551 milestone: None,
552 state: None,
553 deadline: None,
554 remove_deadline: None,
555 };
556 assert!(opt.validate().is_ok());
557 }
558
559 #[test]
560 fn test_edit_issue_option_validate_empty_title() {
561 let opt = EditIssueOption {
562 title: Some(" ".to_string()),
563 body: None,
564 r#ref: None,
565 assignees: Vec::new(),
566 milestone: None,
567 state: None,
568 deadline: None,
569 remove_deadline: None,
570 };
571 assert!(opt.validate().is_err());
572 }
573
574 #[test]
575 fn test_create_issue_comment_option_validate_success() {
576 let opt = CreateIssueCommentOption {
577 body: "comment".to_string(),
578 };
579 assert!(opt.validate().is_ok());
580 }
581
582 #[test]
583 fn test_create_issue_comment_option_validate_empty_body() {
584 let opt = CreateIssueCommentOption {
585 body: String::new(),
586 };
587 assert!(opt.validate().is_err());
588 }
589
590 #[test]
591 fn test_edit_issue_comment_option_validate_success() {
592 let opt = EditIssueCommentOption {
593 body: "updated comment".to_string(),
594 };
595 assert!(opt.validate().is_ok());
596 }
597
598 #[test]
599 fn test_edit_issue_comment_option_validate_empty_body() {
600 let opt = EditIssueCommentOption {
601 body: String::new(),
602 };
603 assert!(opt.validate().is_err());
604 }
605
606 #[test]
607 fn test_create_milestone_option_validate_success() {
608 let opt = CreateMilestoneOption {
609 title: "v1.0".to_string(),
610 description: String::new(),
611 state: StateType::Open,
612 deadline: None,
613 };
614 assert!(opt.validate().is_ok());
615 }
616
617 #[test]
618 fn test_create_milestone_option_validate_empty_title() {
619 let opt = CreateMilestoneOption {
620 title: String::new(),
621 description: String::new(),
622 state: StateType::Open,
623 deadline: None,
624 };
625 assert!(opt.validate().is_err());
626 }
627
628 #[test]
629 fn test_edit_milestone_option_validate_success() {
630 let opt = EditMilestoneOption {
631 title: Some("v2.0".to_string()),
632 ..Default::default()
633 };
634 assert!(opt.validate().is_ok());
635 }
636
637 #[test]
638 fn test_edit_milestone_option_validate_empty_title() {
639 let opt = EditMilestoneOption {
640 title: Some(" ".to_string()),
641 ..Default::default()
642 };
643 assert!(opt.validate().is_err());
644 }
645
646 #[test]
647 fn test_add_time_option_validate_success() {
648 let opt = AddTimeOption {
649 time: 3600,
650 created: None,
651 user: String::new(),
652 };
653 assert!(opt.validate().is_ok());
654 }
655
656 #[test]
657 fn test_add_time_option_validate_zero_time() {
658 let opt = AddTimeOption {
659 time: 0,
660 created: None,
661 user: String::new(),
662 };
663 assert!(opt.validate().is_err());
664 }
665}