1use failure::{Error, ResultExt};
2use hyperx::header::{Link, LinkValue, RelationType};
3use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, LINK, USER_AGENT};
4use reqwest::Client;
5use reqwest::StatusCode;
6use serde::Deserialize;
7use serde_json::{self, Value};
8use std::marker::PhantomData;
9use std::vec::IntoIter;
10
11#[macro_export]
34macro_rules! cmd {
35 (in $cwd:expr; $format:expr, $arg:expr) => {
36 cmd!(in $cwd; format!($format, $arg))
37 };
38 (in $cwd:expr; $command:expr) => {{
39 use ::failure::ResultExt;
40
41 let command = String::from($command);
42 trace!("Executing `{}`", command);
43 let arguments: Vec<_> = command.split_whitespace().collect();
44
45 let mut cmd_builder = ::std::process::Command::new(&arguments[0]);
46 cmd_builder.current_dir($cwd);
47
48 for arg in &arguments[1..] {
49 cmd_builder.arg(arg);
50 }
51
52 cmd_builder.output()
53 .with_context(|_| format!("Unable to execute `{}`. Is {} installed?", command, &arguments[0]))
54 .map_err(::failure::Error::from)
55 .and_then(|output| {
56 if output.status.success() {
60 Ok(())
61 } else {
62 match output.status.code() {
63 Some(code) => warn!("`{}` failed with return code {}", command, code),
64 None => warn!("`{}` failed", command),
65 }
66
67 if !output.stderr.is_empty() {
68 debug!("Stderr:");
69 for line in String::from_utf8_lossy(&output.stderr).lines() {
70 debug!("\t{}", line);
71 }
72 }
73 if !output.stdout.is_empty() {
74 debug!("Stdout:");
75 for line in String::from_utf8_lossy(&output.stdout).lines() {
76 debug!("\t{}", line);
77 }
78 }
79
80 Err(::failure::err_msg(format!("`{}` failed", command)))
81 }
82 })
83 }};
84 ($format:expr, $($arg:expr),*) => {
85 cmd!(format!($format, $($arg),*))
86 };
87 ($command:expr) => {
88 cmd!(in "."; $command)
89 };
90}
91
92pub struct Paginated<I>
93where
94 I: for<'de> Deserialize<'de>,
95{
96 client: Client,
97 token: String,
98 _phantom: PhantomData<I>,
99 next_endpoint: Option<String>,
100 items: IntoIter<I>,
101}
102
103impl<I> Paginated<I>
104where
105 for<'de> I: Deserialize<'de>,
106{
107 pub fn new(token: &str, endpoint: &str) -> Self {
108 Paginated {
109 client: Client::new(),
110 token: token.to_string(),
111 _phantom: PhantomData,
112 next_endpoint: Some(String::from(endpoint)),
113 items: Vec::new().into_iter(),
114 }
115 }
116
117 fn send_request(&mut self, endpoint: &str) -> Result<Vec<I>, Error> {
118 debug!("Sending request to {:?}", endpoint);
119
120 let request = self
121 .client
122 .get(endpoint)
123 .header(CONTENT_TYPE, "application/json")
124 .header(USER_AGENT, "repo-backup")
125 .header(ACCEPT, "application/vnd.github.v3+json")
126 .header(AUTHORIZATION, format!("token {}", self.token))
127 .build()
128 .context("Generated invalid request. This is a bug.")?;
129
130 if log_enabled!(::log::Level::Trace) {
131 let redacted_header =
132 format!("Request Headers {:#?}", request.headers())
133 .replace(&self.token, "...");
134
135 for line in redacted_header.lines() {
136 trace!("{}", line);
137 }
138 }
139
140 let mut response = self
141 .client
142 .execute(request)
143 .context("Unable to send request")?;
144
145 let raw: Value = response.json()?;
146 let status = response.status();
147 let headers = response.headers();
148 debug!("Received response ({})", status);
149
150 if log_enabled!(::log::Level::Trace) {
151 for line in format!("Response Headers {:#?}", headers).lines() {
152 trace!("{}", line);
153 }
154
155 }
160
161 let got = serde_json::from_value(raw)
162 .context("Unable to deserialize response")?;
163
164 if let Some(link) = headers
165 .get(LINK)
166 .and_then(|l| l.to_str().ok())
167 .and_then(|l| l.parse().ok())
168 {
169 self.next_endpoint = next_link(&link).map(|s| s.to_string());
170 }
171
172 if !status.is_success() {
173 warn!("Request failed with {}", status);
174
175 let err = FailedRequest {
176 status: status,
177 url: endpoint.to_string(),
178 };
179
180 return Err(err.into());
181 }
182
183 Ok(got)
184 }
185}
186
187impl<I> Iterator for Paginated<I>
188where
189 for<'de> I: Deserialize<'de>,
190{
191 type Item = Result<I, Error>;
192
193 fn next(&mut self) -> Option<Self::Item> {
194 if let Some(next_item) = self.items.next() {
195 return Some(Ok(next_item));
196 }
197
198 if let Some(next_endpoint) = self.next_endpoint.take() {
199 match self.send_request(&next_endpoint) {
200 Ok(values) => {
201 self.items = values.into_iter();
202 return self.items.next().map(|it| Ok(it));
203 }
204 Err(e) => {
205 return Some(Err(e));
206 }
207 }
208 }
209
210 None
211 }
212}
213fn next_link(link: &Link) -> Option<&str> {
214 link.values()
215 .iter()
216 .filter_map(|v| if is_next(v) { Some(v) } else { None })
217 .map(|v| v.link())
218 .next()
219}
220
221fn is_next(link_value: &LinkValue) -> bool {
222 link_value
223 .rel()
224 .map(|relations| relations.iter().any(|rel| *rel == RelationType::Next))
225 .unwrap_or(false)
226}
227
228#[derive(Debug, Clone, PartialEq, Fail)]
229#[fail(display = "Request failed with {}", status)]
230pub struct FailedRequest {
231 status: StatusCode,
232 url: String,
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn get_next_link() {
241 let src = r#"<https://api.github.com/user/repos?page=2>; rel="next", <https://api.github.com/user/repos?page=3>; rel="last""#;
242 let link: Link = src.parse().unwrap();
243
244 let should_be = "https://api.github.com/user/repos?page=2";
245 let got = next_link(&link).unwrap();
246 assert_eq!(got, should_be);
247 }
248}