1use std::collections::HashMap;
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6use url::form_urlencoded;
7
8use crate::comments::Comments;
9use crate::labels::Label;
10use crate::users::User;
11use crate::utils::{percent_encode, PATH_SEGMENT};
12use crate::{Future, Github, SortDirection, Stream};
13use crate::milestone::Milestone;
14
15#[derive(Clone, Copy, Debug, PartialEq)]
17pub enum State {
18 Open,
20 Closed,
22 All,
24}
25
26impl fmt::Display for State {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match *self {
29 State::Open => "open",
30 State::Closed => "closed",
31 State::All => "all",
32 }
33 .fmt(f)
34 }
35}
36
37impl Default for State {
38 fn default() -> State {
39 State::Open
40 }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq)]
45pub enum Sort {
46 Created,
48 Updated,
50 Comments,
52}
53
54impl fmt::Display for Sort {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 match *self {
57 Sort::Created => "created",
58 Sort::Updated => "updated",
59 Sort::Comments => "comments",
60 }
61 .fmt(f)
62 }
63}
64
65impl Default for Sort {
66 fn default() -> Sort {
67 Sort::Created
68 }
69}
70
71pub struct IssueAssignees {
73 github: Github,
74 owner: String,
75 repo: String,
76 number: u64,
77}
78
79impl IssueAssignees {
80 #[doc(hidden)]
81 pub fn new<O, R>(github: Github, owner: O, repo: R, number: u64) -> Self
82 where
83 O: Into<String>,
84 R: Into<String>,
85 {
86 IssueAssignees {
87 github,
88 owner: owner.into(),
89 repo: repo.into(),
90 number,
91 }
92 }
93
94 fn path(&self, more: &str) -> String {
95 format!(
96 "/repos/{}/{}/issues/{}/assignees{}",
97 self.owner, self.repo, self.number, more
98 )
99 }
100
101 pub fn add(&self, assignees: Vec<&str>) -> Future<Issue> {
103 self.github
104 .post(&self.path(""), json_lit!({ "assignees": assignees }))
105 }
106}
107
108pub struct IssueLabels {
110 github: Github,
111 owner: String,
112 repo: String,
113 number: u64,
114}
115
116impl IssueLabels {
117 #[doc(hidden)]
118 pub fn new<O, R>(github: Github, owner: O, repo: R, number: u64) -> Self
119 where
120 O: Into<String>,
121 R: Into<String>,
122 {
123 IssueLabels {
124 github,
125 owner: owner.into(),
126 repo: repo.into(),
127 number,
128 }
129 }
130
131 fn path(&self, more: &str) -> String {
132 format!(
133 "/repos/{}/{}/issues/{}/labels{}",
134 self.owner, self.repo, self.number, more
135 )
136 }
137
138 #[allow(clippy::needless_pass_by_value)] pub fn add(&self, labels: Vec<&str>) -> Future<Vec<Label>> {
141 self.github.post(&self.path(""), json!(labels))
142 }
143
144 pub fn remove(&self, label: &str) -> Future<()> {
146 let label = percent_encode(label.as_ref(), PATH_SEGMENT);
147 self.github.delete(&self.path(&format!("/{}", label)))
148 }
149
150 #[allow(clippy::needless_pass_by_value)] pub fn set(&self, labels: Vec<&str>) -> Future<Vec<Label>> {
155 self.github.put(&self.path(""), json!(labels))
156 }
157
158 pub fn clear(&self) -> Future<()> {
160 self.github.delete(&self.path(""))
161 }
162}
163
164pub struct IssueRef {
167 github: Github,
168 owner: String,
169 repo: String,
170 number: u64,
171}
172
173impl IssueRef {
174 #[doc(hidden)]
175 pub fn new<O, R>(github: Github, owner: O, repo: R, number: u64) -> Self
176 where
177 O: Into<String>,
178 R: Into<String>,
179 {
180 IssueRef {
181 github,
182 owner: owner.into(),
183 repo: repo.into(),
184 number,
185 }
186 }
187
188 pub fn get(&self) -> Future<Issue> {
190 self.github.get(&self.path(""))
191 }
192
193 fn path(&self, more: &str) -> String {
194 format!(
195 "/repos/{}/{}/issues/{}{}",
196 self.owner, self.repo, self.number, more
197 )
198 }
199
200 pub fn labels(&self) -> IssueLabels {
202 IssueLabels::new(
203 self.github.clone(),
204 self.owner.as_str(),
205 self.repo.as_str(),
206 self.number,
207 )
208 }
209
210 pub fn assignees(&self) -> IssueAssignees {
212 IssueAssignees::new(
213 self.github.clone(),
214 self.owner.as_str(),
215 self.repo.as_str(),
216 self.number,
217 )
218 }
219
220 pub fn edit(&self, is: &IssueOptions) -> Future<Issue> {
222 self.github.patch(&self.path(""), json!(is))
223 }
224
225 pub fn comments(&self) -> Comments {
227 Comments::new(
228 self.github.clone(),
229 self.owner.clone(),
230 self.repo.clone(),
231 self.number,
232 )
233 }
234}
235
236pub struct Issues {
239 github: Github,
240 owner: String,
241 repo: String,
242}
243
244impl Issues {
245 pub fn new<O, R>(github: Github, owner: O, repo: R) -> Self
247 where
248 O: Into<String>,
249 R: Into<String>,
250 {
251 Issues {
252 github,
253 owner: owner.into(),
254 repo: repo.into(),
255 }
256 }
257
258 fn path(&self, more: &str) -> String {
259 format!("/repos/{}/{}/issues{}", self.owner, self.repo, more)
260 }
261
262 pub fn get(&self, number: u64) -> IssueRef {
263 IssueRef::new(
264 self.github.clone(),
265 self.owner.as_str(),
266 self.repo.as_str(),
267 number,
268 )
269 }
270
271 pub fn create(&self, is: &IssueOptions) -> Future<Issue> {
272 self.github.post(&self.path(""), json!(is))
273 }
274
275 pub fn update(&self, number: &u64, is: &IssueOptions) -> Future<Issue> {
276 self.github.patch(&self.path(&format!("/{}", number)), json!(is))
277 }
278
279 pub fn list(&self, options: &IssueListOptions) -> Future<Vec<Issue>> {
283 let mut uri = vec![self.path("")];
284 if let Some(query) = options.serialize() {
285 uri.push(query);
286 }
287 self.github.get(&uri.join("?"))
288 }
289
290 pub fn iter(&self, options: &IssueListOptions) -> Stream<Issue> {
298 let mut uri = vec![self.path("")];
299 if let Some(query) = options.serialize() {
300 uri.push(query);
301 }
302 self.github.get_stream(&uri.join("?"))
303 }
304}
305
306#[derive(Default)]
317pub struct IssueListOptions {
318 params: HashMap<&'static str, String>,
319}
320
321impl IssueListOptions {
322 pub fn builder() -> IssueListOptionsBuilder {
323 IssueListOptionsBuilder::default()
324 }
325
326 pub fn serialize(&self) -> Option<String> {
327 if self.params.is_empty() {
328 None
329 } else {
330 let encoded: String = form_urlencoded::Serializer::new(String::new())
331 .extend_pairs(&self.params)
332 .finish();
333 Some(encoded)
334 }
335 }
336}
337
338#[derive(Default)]
340pub struct IssueListOptionsBuilder(IssueListOptions);
341
342impl IssueListOptionsBuilder {
343 pub fn state(&mut self, state: State) -> &mut Self {
344 self.0.params.insert("state", state.to_string());
345 self
346 }
347
348 pub fn sort(&mut self, sort: Sort) -> &mut Self {
349 self.0.params.insert("sort", sort.to_string());
350 self
351 }
352
353 pub fn asc(&mut self) -> &mut Self {
354 self.direction(SortDirection::Asc)
355 }
356
357 pub fn desc(&mut self) -> &mut Self {
358 self.direction(SortDirection::Desc)
359 }
360
361 pub fn direction(&mut self, direction: SortDirection) -> &mut Self {
362 self.0.params.insert("direction", direction.to_string());
363 self
364 }
365
366 pub fn assignee<A>(&mut self, assignee: A) -> &mut Self
367 where
368 A: Into<String>,
369 {
370 self.0.params.insert("assignee", assignee.into());
371 self
372 }
373
374 pub fn creator<C>(&mut self, creator: C) -> &mut Self
375 where
376 C: Into<String>,
377 {
378 self.0.params.insert("creator", creator.into());
379 self
380 }
381
382 pub fn mentioned<M>(&mut self, mentioned: M) -> &mut Self
383 where
384 M: Into<String>,
385 {
386 self.0.params.insert("mentioned", mentioned.into());
387 self
388 }
389
390 pub fn labels<L>(&mut self, labels: Vec<L>) -> &mut Self
391 where
392 L: Into<String>,
393 {
394 self.0.params.insert(
395 "labels",
396 labels
397 .into_iter()
398 .map(|l| l.into())
399 .collect::<Vec<_>>()
400 .join(","),
401 );
402 self
403 }
404
405 pub fn since<S>(&mut self, since: S) -> &mut Self
406 where
407 S: Into<String>,
408 {
409 self.0.params.insert("since", since.into());
410 self
411 }
412
413 pub fn per_page(&mut self, n: u32) -> &mut Self {
414 self.0.params.insert("per_page", n.to_string());
415 self
416 }
417
418 pub fn build(&self) -> IssueListOptions {
419 IssueListOptions {
420 params: self.0.params.clone(),
421 }
422 }
423}
424
425#[derive(Debug, Serialize)]
426pub struct IssueOptions {
427 pub title: String,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub body: Option<String>,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub assignee: Option<String>,
432 #[serde(skip_serializing_if = "Option::is_none")]
433 pub assignees: Option<Vec<String>>,
434 #[serde(skip_serializing_if = "Option::is_none")]
435 pub milestone: Option<u64>,
436 pub labels: Vec<String>,
437 pub state: String,
438}
439
440impl IssueOptions {
441 pub fn new<T, B, A, L>(
442 title: T,
443 body: Option<B>,
444 assignee: Option<A>,
445 milestone: Option<u64>,
446 labels: Vec<L>,
447 ) -> IssueOptions
448 where
449 T: Into<String>,
450 B: Into<String>,
451 A: Into<String>,
452 L: Into<String>,
453 {
454 IssueOptions {
455 title: title.into(),
456 body: body.map(|b| b.into()),
457 assignee: assignee.map(|a| a.into()),
458 assignees: None,
459 milestone,
460 labels: labels
461 .into_iter()
462 .map(|l| l.into())
463 .collect::<Vec<String>>(),
464 state: "open".to_string()
465 }
466 }
467}
468
469#[derive(Debug, Deserialize, Serialize)]
470pub struct Issue {
471 pub id: u64,
472 pub url: String,
473 pub repository_url: String,
474 pub labels_url: String,
475 pub comments_url: String,
476 pub events_url: String,
477 pub html_url: String,
478 pub number: u64,
479 pub state: String,
480 pub title: String,
481 pub body: Option<String>,
482 pub user: User,
483 pub labels: Vec<Label>,
484 pub assignee: Option<User>,
485 pub locked: bool,
486 pub comments: u64,
487 pub pull_request: Option<PullRef>,
488 pub closed_at: Option<String>,
489 pub created_at: String,
490 pub updated_at: String,
491 pub assignees: Vec<User>,
492 pub milestone: Option<Milestone>,
493}
494
495#[derive(Debug, Deserialize, Serialize)]
497pub struct PullRef {
498 pub url: String,
499 pub html_url: String,
500 pub diff_url: String,
501 pub patch_url: String,
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
509 fn default_state() {
510 let default: State = Default::default();
511 assert_eq!(default, State::Open)
512 }
513
514 #[test]
515 fn issue_list_reqs() {
516 fn test_serialize(tests: Vec<(IssueListOptions, Option<String>)>) {
517 for test in tests {
518 let (k, v) = test;
519 assert_eq!(k.serialize(), v);
520 }
521 }
522 let tests = vec![
523 (IssueListOptions::builder().build(), None),
524 (
525 IssueListOptions::builder().state(State::Closed).build(),
526 Some("state=closed".to_owned()),
527 ),
528 (
529 IssueListOptions::builder()
530 .labels(vec!["foo", "bar"])
531 .build(),
532 Some("labels=foo%2Cbar".to_owned()),
533 ),
534 ];
535 test_serialize(tests)
536 }
537
538 #[test]
539 fn sort_default() {
540 let default: Sort = Default::default();
541 assert_eq!(default, Sort::Created)
542 }
543
544 #[test]
545 fn sort_display() {
546 for (k, v) in &[
547 (Sort::Created, "created"),
548 (Sort::Updated, "updated"),
549 (Sort::Comments, "comments"),
550 ] {
551 assert_eq!(k.to_string(), *v)
552 }
553 }
554}