1use std::collections::HashMap;
3use std::fmt;
4
5use serde::de::DeserializeOwned;
6use serde::{Deserialize, Serialize};
7use url::{self, form_urlencoded};
8
9use crate::labels::Label;
10use crate::users::User;
11use crate::{unfold, Future, Github, SortDirection, Stream};
12
13mod repos;
14
15pub use self::repos::*;
16use crate::milestone::Milestone;
17
18#[derive(Clone, Copy, Debug, PartialEq)]
20pub enum IssuesSort {
21 Created,
23 Updated,
25 Comments,
27}
28
29impl fmt::Display for IssuesSort {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match *self {
32 IssuesSort::Comments => "comments",
33 IssuesSort::Created => "created",
34 IssuesSort::Updated => "updated",
35 }
36 .fmt(f)
37 }
38}
39
40#[derive(Clone)]
43pub struct Search {
44 github: Github,
45}
46
47fn items<D>(result: SearchResult<D>) -> Vec<D>
48where
49 D: DeserializeOwned + 'static + Send,
50{
51 result.items
52}
53
54impl Search {
55 #[doc(hidden)]
56 pub fn new(github: Github) -> Self {
57 Self { github }
58 }
59
60 pub fn issues(&self) -> SearchIssues {
62 SearchIssues::new(self.clone())
63 }
64
65 pub fn repos(&self) -> SearchRepos {
67 SearchRepos::new(self.clone())
68 }
69
70 fn iter<D>(&self, url: &str) -> Stream<D>
71 where
72 D: DeserializeOwned + 'static + Send,
73 {
74 unfold(self.github.clone(), self.github.get_pages(url), items)
75 }
76
77 fn search<D>(&self, url: &str) -> Future<SearchResult<D>>
78 where
79 D: DeserializeOwned + 'static + Send,
80 {
81 self.github.get(url)
82 }
83}
84
85pub struct SearchIssues {
88 search: Search,
89}
90
91impl SearchIssues {
92 #[doc(hidden)]
93 pub fn new(search: Search) -> Self {
94 Self { search }
95 }
96
97 fn search_uri<Q>(&self, q: Q, options: &SearchIssuesOptions) -> String
98 where
99 Q: Into<String>,
100 {
101 let mut uri = vec!["/search/issues".to_string()];
102 let query_options = options.serialize().unwrap_or_default();
103 let query = form_urlencoded::Serializer::new(query_options)
104 .append_pair("q", &q.into())
105 .finish();
106 uri.push(query);
107 uri.join("?")
108 }
109
110 pub fn iter<Q>(&self, q: Q, options: &SearchIssuesOptions) -> Stream<IssuesItem>
114 where
115 Q: Into<String>,
116 {
117 self.search.iter::<IssuesItem>(&self.search_uri(q, options))
118 }
119
120 pub fn list<Q>(&self, q: Q, options: &SearchIssuesOptions) -> Future<SearchResult<IssuesItem>>
124 where
125 Q: Into<String>,
126 {
127 self.search
128 .search::<IssuesItem>(&self.search_uri(q, options))
129 }
130}
131
132#[derive(Default)]
135pub struct SearchIssuesOptions {
136 params: HashMap<&'static str, String>,
137}
138
139impl SearchIssuesOptions {
140 pub fn builder() -> SearchIssuesOptionsBuilder {
141 SearchIssuesOptionsBuilder::default()
142 }
143
144 pub fn serialize(&self) -> Option<String> {
146 if self.params.is_empty() {
147 None
148 } else {
149 let encoded: String = form_urlencoded::Serializer::new(String::new())
150 .extend_pairs(&self.params)
151 .finish();
152 Some(encoded)
153 }
154 }
155}
156
157#[derive(Default)]
159pub struct SearchIssuesOptionsBuilder(SearchIssuesOptions);
160
161impl SearchIssuesOptionsBuilder {
162 pub fn per_page(&mut self, n: usize) -> &mut Self {
163 self.0.params.insert("per_page", n.to_string());
164 self
165 }
166
167 pub fn sort(&mut self, sort: IssuesSort) -> &mut Self {
168 self.0.params.insert("sort", sort.to_string());
169 self
170 }
171
172 pub fn order(&mut self, direction: SortDirection) -> &mut Self {
173 self.0.params.insert("order", direction.to_string());
174 self
175 }
176
177 pub fn page(&mut self, n: usize) -> &mut Self {
178 self.0.params.insert("page", n.to_string());
179 self
180 }
181
182 pub fn build(&self) -> SearchIssuesOptions {
183 SearchIssuesOptions {
184 params: self.0.params.clone(),
185 }
186 }
187}
188
189#[derive(Debug, Deserialize)]
190pub struct SearchResult<D> {
191 pub total_count: u64,
192 pub incomplete_results: bool,
193 pub items: Vec<D>,
194}
195
196#[derive(Debug, Deserialize, Serialize)]
199pub struct IssuesItem {
200 pub url: String,
201 pub repository_url: String,
202 pub labels_url: String,
203 pub comments_url: String,
204 pub events_url: String,
205 pub html_url: String,
206 pub id: u64,
207 pub number: u64,
208 pub title: String,
209 pub user: User,
210 pub labels: Vec<Label>,
211 pub state: String,
212 pub locked: bool,
213 pub assignee: Option<User>,
214 pub assignees: Vec<User>,
215 pub comments: u64,
216 pub created_at: String,
217 pub updated_at: String,
218 pub closed_at: Option<String>,
219 pub pull_request: Option<PullRequestInfo>,
220 pub body: Option<String>,
221 pub milestone: Option<Milestone>,
222}
223
224impl IssuesItem {
225 pub fn repo_tuple(&self) -> (String, String) {
227 let parsed = url::Url::parse(&self.repository_url).unwrap();
229 let mut path = parsed.path().split('/').collect::<Vec<_>>();
230 path.reverse();
231 (path[1].to_owned(), path[0].to_owned())
232 }
233}
234
235#[derive(Debug, Deserialize, Serialize)]
236pub struct PullRequestInfo {
237 pub url: String,
238 pub html_url: String,
239 pub diff_url: String,
240 pub patch_url: String,
241}