gitlab.rs 19.9 KB
Newer Older
1
2
3
4
5
6
7
8
// Copyright 2016 Kitware, Inc.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

Makoto Nakashima's avatar
Makoto Nakashima committed
9
10
extern crate reqwest;
use self::reqwest::{Client, RequestBuilder, Url};
11

12
extern crate serde;
13
14
use self::serde::{Deserialize, Deserializer, Serialize, Serializer};
use self::serde::de::Error as SerdeError;
15

16
17
18
19
20
extern crate serde_json;

extern crate url;
use self::url::percent_encoding::{PATH_SEGMENT_ENCODE_SET, percent_encode};

21
22
use error::*;
use types::*;
23

Makoto Nakashima's avatar
Makoto Nakashima committed
24
use std::borrow::Borrow;
Ben Boeckel's avatar
Ben Boeckel committed
25
26
use std::fmt::{self, Debug};

Ben Boeckel's avatar
Ben Boeckel committed
27
28
29
/// A representation of the Gitlab API for a single user.
///
/// Separate users should use separate instances of this.
30
pub struct Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
31
    /// The base URL to use for API calls.
32
    base_url: Url,
Ben Boeckel's avatar
Ben Boeckel committed
33
    /// The secret token to use when communicating with Gitlab.
34
35
36
    token: String,
}

Ben Boeckel's avatar
Ben Boeckel committed
37
impl Debug for Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
38
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Ben Boeckel's avatar
Ben Boeckel committed
39
40
41
42
        write!(f, "Gitlab {{ {} }}", self.base_url)
    }
}

Ben Boeckel's avatar
Ben Boeckel committed
43
// The header Gitlab uses to authenticate the user.
44
45
header!{ (GitlabPrivateToken, "PRIVATE-TOKEN") => [String] }

Ben Boeckel's avatar
Ben Boeckel committed
46
#[derive(Debug)]
47
48
49
50
51
52
53
54
55
56
57
58
/// Optional information for commit statuses.
pub struct CommitStatusInfo<'a> {
    /// The refname of the commit being tested.
    pub refname: Option<&'a str>,
    /// The name of the status (defaults to `"default"` on the Gitlab side).
    pub name: Option<&'a str>,
    /// A URL to associate with the status.
    pub target_url: Option<&'a str>,
    /// A description of the status check.
    pub description: Option<&'a str>,
}

59
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60
61
62
63
64
65
66
67
68
/// Optional information for merge requests.
pub enum MergeRequestStateFilter {
    /// Get the opened/reopened merge requests.
    Opened,
    /// Get the closes merge requests.
    Closed,
    /// Get the merged merge requests.
    Merged,
}
69
70
71
72
73
74

enum_serialize!(MergeRequestStateFilter -> "state",
    Opened => "opened",
    Closed => "closed",
    Merged => "merged",
);
75

76
impl Gitlab {
Ben Boeckel's avatar
Ben Boeckel committed
77
78
79
    /// Create a new Gitlab API representation.
    ///
    /// Errors out if `token` is invalid.
Ben Boeckel's avatar
Ben Boeckel committed
80
    pub fn new<T: ToString>(host: &str, token: T) -> Result<Self> {
Ben Boeckel's avatar
Ben Boeckel committed
81
        Self::_new("https", host, token.to_string())
82
83
84
85
86
    }

    /// Create a new non-SSL Gitlab API representation.
    ///
    /// Errors out if `token` is invalid.
Ben Boeckel's avatar
Ben Boeckel committed
87
    pub fn new_insecure<T: ToString>(host: &str, token: T) -> Result<Self> {
Ben Boeckel's avatar
Ben Boeckel committed
88
        Self::_new("http", host, token.to_string())
89
90
    }

Ben Boeckel's avatar
Ben Boeckel committed
91
    /// Internal method to create a new Gitlab client.
Ben Boeckel's avatar
Ben Boeckel committed
92
    fn _new(protocol: &str, host: &str, token: String) -> Result<Self> {
93
94
        let base_url = Url::parse(&format!("{}://{}/api/v3/", protocol, host))
            .chain_err(|| ErrorKind::UrlParse)?;
Ben Boeckel's avatar
Ben Boeckel committed
95

96
97
        let api = Gitlab {
            base_url: base_url,
98
            token: token,
99
        };
100

101
        // Ensure the API is working.
102
        let _: UserPublic = api._get("user")?;
103

104
        Ok(api)
105
106
    }

Ben Boeckel's avatar
Ben Boeckel committed
107
    /// The user the API is acting as.
108
    pub fn current_user(&self) -> Result<UserPublic> {
109
110
111
        self._get("user")
    }

112
    /// Get all user accounts
Ben Boeckel's avatar
Ben Boeckel committed
113
    pub fn users<T: UserResult>(&self) -> Result<Vec<T>> {
114
115
116
117
        self._get_paged("users")
    }

    /// Find a user by id.
Ben Boeckel's avatar
Ben Boeckel committed
118
    pub fn user<T: UserResult>(&self, user: UserId) -> Result<T> {
119
120
121
        self._get(&format!("users/{}", user))
    }

Ben Boeckel's avatar
Ben Boeckel committed
122
    /// Find a user by username.
Ben Boeckel's avatar
Ben Boeckel committed
123
    pub fn user_by_name<T: UserResult>(&self, name: &str) -> Result<T> {
124
        let mut users = self._get_paged_with_param("users", &[("username", name)])?;
125
        users.pop()
Ben Boeckel's avatar
Ben Boeckel committed
126
            .ok_or_else(|| Error::from_kind(ErrorKind::Gitlab("no such user".to_string())))
127
128
    }

129
    /// Get all accessible projects.
Ben Boeckel's avatar
Ben Boeckel committed
130
    pub fn projects(&self) -> Result<Vec<Project>> {
131
132
133
134
        self._get_paged("projects")
    }

    /// Get all owned projects.
Ben Boeckel's avatar
Ben Boeckel committed
135
    pub fn owned_projects(&self) -> Result<Vec<Project>> {
136
137
138
139
140
141
        self._get_paged("projects/owned")
    }

    /// Get all projects.
    ///
    /// Requires administrator privileges.
Ben Boeckel's avatar
Ben Boeckel committed
142
    pub fn all_projects(&self) -> Result<Vec<Project>> {
143
144
145
146
        self._get_paged("projects/all")
    }

    /// Find a project by id.
Ben Boeckel's avatar
Ben Boeckel committed
147
    pub fn project(&self, project: ProjectId) -> Result<Project> {
148
        self._get(&format!("projects/{}", project))
149
150
    }

Ben Boeckel's avatar
Ben Boeckel committed
151
    /// Find a project by name.
Ben Boeckel's avatar
Ben Boeckel committed
152
    pub fn project_by_name(&self, name: &str) -> Result<Project> {
153
154
        self._get(&format!("projects/{}",
                           percent_encode(name.as_bytes(), PATH_SEGMENT_ENCODE_SET)))
155
156
    }

157
    /// Get a project's hooks.
Ben Boeckel's avatar
Ben Boeckel committed
158
    pub fn hooks(&self, project: ProjectId) -> Result<Vec<Hook>> {
159
160
        self._get_paged(&format!("projects/{}/hooks", project))
    }
161

162
    /// Get a project hook.
Ben Boeckel's avatar
Ben Boeckel committed
163
    pub fn hook(&self, project: ProjectId, hook: HookId) -> Result<Hook> {
164
165
166
        self._get(&format!("projects/{}/hooks/{}", project, hook))
    }

Ben Boeckel's avatar
Ben Boeckel committed
167
    /// Convert a boolean parameter into an HTTP request value.
Ben Boeckel's avatar
Ben Boeckel committed
168
169
170
171
172
173
174
175
    fn bool_param_value(value: bool) -> &'static str {
        if value {
            "true"
        } else {
            "false"
        }
    }

Ben Boeckel's avatar
Ben Boeckel committed
176
    /// HTTP parameters required to register to a project.
Makoto Nakashima's avatar
Makoto Nakashima committed
177
178
179
180
181
182
183
184
    fn event_flags(events: WebhookEvents) -> Vec<(&'static str, &'static str)> {
        vec![("build_events", Self::bool_param_value(events.build())),
             ("issues_events", Self::bool_param_value(events.issues())),
             ("merge_requests_events", Self::bool_param_value(events.merge_requests())),
             ("note_events", Self::bool_param_value(events.note())),
             ("pipeline_events", Self::bool_param_value(events.pipeline())),
             ("push_events", Self::bool_param_value(events.push())),
             ("wiki_page_events", Self::bool_param_value(events.wiki_page()))]
Ben Boeckel's avatar
Ben Boeckel committed
185
186
187
    }

    /// Add a project hook.
188
    pub fn add_hook(&self, project: ProjectId, url: &str, events: WebhookEvents) -> Result<Hook> {
Makoto Nakashima's avatar
Makoto Nakashima committed
189
190
        let mut flags = Self::event_flags(events);
        flags.push(("url", url));
Ben Boeckel's avatar
Ben Boeckel committed
191

Makoto Nakashima's avatar
Makoto Nakashima committed
192
        self._post_with_param(&format!("projects/{}/hooks", project), &flags)
Ben Boeckel's avatar
Ben Boeckel committed
193
194
    }

195
    /// Get the team members of a group.
Ben Boeckel's avatar
Ben Boeckel committed
196
    pub fn group_members(&self, group: GroupId) -> Result<Vec<Member>> {
197
198
199
200
        self._get_paged(&format!("groups/{}/members", group))
    }

    /// Get a team member of a group.
Ben Boeckel's avatar
Ben Boeckel committed
201
    pub fn group_member(&self, group: GroupId, user: UserId) -> Result<Member> {
202
203
204
        self._get(&format!("groups/{}/members/{}", group, user))
    }

205
    /// Get the team members of a project.
Ben Boeckel's avatar
Ben Boeckel committed
206
    pub fn project_members(&self, project: ProjectId) -> Result<Vec<Member>> {
207
208
209
210
        self._get_paged(&format!("projects/{}/members", project))
    }

    /// Get a team member of a project.
Ben Boeckel's avatar
Ben Boeckel committed
211
    pub fn project_member(&self, project: ProjectId, user: UserId) -> Result<Member> {
212
213
214
        self._get(&format!("projects/{}/members/{}", project, user))
    }

215
216
    /// Add a user to a project.
    pub fn add_user_to_project(&self, project: ProjectId, user: UserId, access: AccessLevel)
Ben Boeckel's avatar
Ben Boeckel committed
217
                               -> Result<Member> {
218
219
220
        let user_str = format!("{}", user);
        let access_str = format!("{}", access);

Makoto Nakashima's avatar
Makoto Nakashima committed
221
222
        self._post_with_param(&format!("projects/{}/members", project),
                              &[("user", &user_str), ("access", &access_str)])
223
224
225
    }

    /// Get branches for a project.
Ben Boeckel's avatar
Ben Boeckel committed
226
    pub fn branches(&self, project: ProjectId) -> Result<Vec<RepoBranch>> {
227
228
229
230
        self._get_paged(&format!("projects/{}/branches", project))
    }

    /// Get a branch.
Ben Boeckel's avatar
Ben Boeckel committed
231
    pub fn branch(&self, project: ProjectId, branch: &str) -> Result<RepoBranch> {
232
233
234
        self._get(&format!("projects/{}/repository/branches/{}",
                           project,
                           percent_encode(branch.as_bytes(), PATH_SEGMENT_ENCODE_SET)))
235
236
237
    }

    /// Get a commit.
Ben Boeckel's avatar
Ben Boeckel committed
238
    pub fn commit(&self, project: ProjectId, commit: &str) -> Result<RepoCommitDetail> {
239
        self._get(&format!("projects/{}/repository/commits/{}", project, commit))
240
241
242
    }

    /// Get comments on a commit.
243
    pub fn commit_comments(&self, project: ProjectId, commit: &str) -> Result<Vec<CommitNote>> {
Ben Boeckel's avatar
Ben Boeckel committed
244
245
246
        self._get_paged(&format!("projects/{}/repository/commits/{}/comments",
                                 project,
                                 commit))
247
248
249
    }

    /// Get comments on a commit.
Ben Boeckel's avatar
Ben Boeckel committed
250
    pub fn create_commit_comment(&self, project: ProjectId, commit: &str, body: &str)
Ben Boeckel's avatar
Ben Boeckel committed
251
                                 -> Result<CommitNote> {
Makoto Nakashima's avatar
Makoto Nakashima committed
252
253
254
255
        self._post_with_param(&format!("projects/{}/repository/commits/{}/comment",
                                       project,
                                       commit),
                              &[("note", body)])
256
257
258
259
    }

    /// Get comments on a commit.
    pub fn create_commit_line_comment(&self, project: ProjectId, commit: &str, body: &str,
Ben Boeckel's avatar
Ben Boeckel committed
260
                                      path: &str, line: u64)
Ben Boeckel's avatar
Ben Boeckel committed
261
                                      -> Result<CommitNote> {
262
        let line_str = format!("{}", line);
Ben Boeckel's avatar
Ben Boeckel committed
263
        let line_type = LineType::New;
264

Makoto Nakashima's avatar
Makoto Nakashima committed
265
266
267
268
269
270
271
        self._post_with_param(&format!("projects/{}/repository/commits/{}/comment",
                                       project,
                                       commit),
                              &[("note", body),
                                ("path", path),
                                ("line", &line_str),
                                ("line_type", line_type.as_str())])
272
273
    }

274
275
    /// Get the latest statuses of a commit.
    pub fn commit_latest_statuses(&self, project: ProjectId, commit: &str)
Ben Boeckel's avatar
Ben Boeckel committed
276
                                  -> Result<Vec<CommitStatus>> {
Ben Boeckel's avatar
Ben Boeckel committed
277
278
279
        self._get_paged(&format!("projects/{}/repository/commits/{}/statuses",
                                 project,
                                 commit))
280
281
    }

282
    /// Get the all statuses of a commit.
Ben Boeckel's avatar
Ben Boeckel committed
283
    pub fn commit_all_statuses(&self, project: ProjectId, commit: &str)
Ben Boeckel's avatar
Ben Boeckel committed
284
                               -> Result<Vec<CommitStatus>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
285
286
287
288
        self._get_paged_with_param(&format!("projects/{}/repository/commits/{}/statuses",
                                            project,
                                            commit),
                                   &[("all", "true")])
289
290
    }

291
    /// Get the latest builds of a commit.
292
    pub fn commit_latest_builds(&self, project: ProjectId, commit: &str) -> Result<Vec<Build>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
293
294
295
        self._get_paged(&format!("projects/{}/repository/commits/{}/builds", project, commit))
    }

296
    /// Get the all builds of a commit.
Ben Boeckel's avatar
Ben Boeckel committed
297
    pub fn commit_all_builds(&self, project: ProjectId, commit: &str) -> Result<Vec<Build>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
298
299
300
301
        self._get_paged_with_param(&format!("projects/{}/repository/commits/{}/builds",
                                            project,
                                            commit),
                                   &[("all", "true")])
Makoto Nakashima's avatar
Makoto Nakashima committed
302
303
    }

Ben Boeckel's avatar
Ben Boeckel committed
304
    /// Create a status message for a commit.
Ben Boeckel's avatar
Ben Boeckel committed
305
    pub fn create_commit_status(&self, project: ProjectId, sha: &str, state: StatusState,
Ben Boeckel's avatar
Ben Boeckel committed
306
                                info: &CommitStatusInfo)
Ben Boeckel's avatar
Ben Boeckel committed
307
                                -> Result<CommitStatus> {
308
        let path = &format!("projects/{}/statuses/{}", project, sha);
309

Makoto Nakashima's avatar
Makoto Nakashima committed
310
        let mut params = vec![("state", state.as_str())];
311

Makoto Nakashima's avatar
Makoto Nakashima committed
312
313
314
315
        info.refname.map(|v| params.push(("ref", v)));
        info.name.map(|v| params.push(("name", v)));
        info.target_url.map(|v| params.push(("target_url", v)));
        info.description.map(|v| params.push(("description", v)));
316

Makoto Nakashima's avatar
Makoto Nakashima committed
317
        self._post_with_param(path, &params)
318
319
    }

Ben Boeckel's avatar
Ben Boeckel committed
320
    /// Get the issues for a project.
Ben Boeckel's avatar
Ben Boeckel committed
321
    pub fn issues(&self, project: ProjectId) -> Result<Vec<Issue>> {
Ben Boeckel's avatar
Ben Boeckel committed
322
323
324
325
        self._get_paged(&format!("projects/{}/issues", project))
    }

    /// Get issues.
Ben Boeckel's avatar
Ben Boeckel committed
326
    pub fn issue(&self, project: ProjectId, issue: IssueId) -> Result<Issue> {
Ben Boeckel's avatar
Ben Boeckel committed
327
328
329
330
        self._get(&format!("projects/{}/issues/{}", project, issue))
    }

    /// Get the notes from a issue.
Ben Boeckel's avatar
Ben Boeckel committed
331
    pub fn issue_notes(&self, project: ProjectId, issue: IssueId) -> Result<Vec<Note>> {
Ben Boeckel's avatar
Ben Boeckel committed
332
        self._get_paged(&format!("projects/{}/issues/{}/notes", project, issue))
Ben Boeckel's avatar
Ben Boeckel committed
333
334
335
    }

    /// Create a note on a issue.
Ben Boeckel's avatar
Ben Boeckel committed
336
    pub fn create_issue_note(&self, project: ProjectId, issue: IssueId, content: &str)
Ben Boeckel's avatar
Ben Boeckel committed
337
                             -> Result<Note> {
Ben Boeckel's avatar
Ben Boeckel committed
338
        let path = &format!("projects/{}/issues/{}/notes", project, issue);
Ben Boeckel's avatar
Ben Boeckel committed
339

Makoto Nakashima's avatar
Makoto Nakashima committed
340
        self._post_with_param(path, &[("body", content)])
Ben Boeckel's avatar
Ben Boeckel committed
341
342
    }

343
    /// Get the merge requests for a project.
Ben Boeckel's avatar
Ben Boeckel committed
344
    pub fn merge_requests(&self, project: ProjectId) -> Result<Vec<MergeRequest>> {
345
346
347
        self._get_paged(&format!("projects/{}/merge_requests", project))
    }

348
    /// Get the merge requests with a given state.
Ben Boeckel's avatar
Ben Boeckel committed
349
    pub fn merge_requests_with_state(&self, project: ProjectId, state: MergeRequestStateFilter)
Ben Boeckel's avatar
Ben Boeckel committed
350
                                     -> Result<Vec<MergeRequest>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
351
352
        self._get_paged_with_param(&format!("projects/{}/merge_requests", project),
                                   &[("state", state.as_str())])
353
354
    }

355
    /// Get merge requests.
Ben Boeckel's avatar
Ben Boeckel committed
356
    pub fn merge_request(&self, project: ProjectId, merge_request: MergeRequestId)
Ben Boeckel's avatar
Ben Boeckel committed
357
                         -> Result<MergeRequest> {
358
359
360
        self._get(&format!("projects/{}/merge_requests/{}", project, merge_request))
    }

361
362
    /// Get the issues that will be closed when a merge request is merged.
    pub fn merge_request_closes_issues(&self, project: ProjectId, merge_request: MergeRequestId)
Ben Boeckel's avatar
Ben Boeckel committed
363
                                       -> Result<Vec<IssueReference>> {
364
365
366
367
368
        self._get_paged(&format!("projects/{}/merge_requests/{}/closes_issues",
                                 project,
                                 merge_request))
    }

369
    /// Get the notes from a merge request.
Ben Boeckel's avatar
Ben Boeckel committed
370
    pub fn merge_request_notes(&self, project: ProjectId, merge_request: MergeRequestId)
Ben Boeckel's avatar
Ben Boeckel committed
371
                               -> Result<Vec<Note>> {
Ben Boeckel's avatar
Ben Boeckel committed
372
373
374
        self._get_paged(&format!("projects/{}/merge_requests/{}/notes",
                                 project,
                                 merge_request))
375
376
    }

377
378
    /// Award a merge request note with an award.
    pub fn award_merge_request_note(&self, project: ProjectId, merge_request: MergeRequestId,
Ben Boeckel's avatar
Ben Boeckel committed
379
                                    note: NoteId, award: &str)
Ben Boeckel's avatar
Ben Boeckel committed
380
                                    -> Result<AwardEmoji> {
381
        let path = &format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
Ben Boeckel's avatar
Ben Boeckel committed
382
383
384
                            project,
                            merge_request,
                            note);
Makoto Nakashima's avatar
Makoto Nakashima committed
385
        self._post_with_param(path, &[("name", award)])
386
387
    }

388
389
    /// Get the awards for a merge request.
    pub fn merge_request_awards(&self, project: ProjectId, merge_request: MergeRequestId)
Ben Boeckel's avatar
Ben Boeckel committed
390
                                -> Result<Vec<AwardEmoji>> {
391
392
393
394
395
        self._get_paged(&format!("projects/{}/merge_requests/{}/award_emoji",
                                 project,
                                 merge_request))
    }

396
397
    /// Get the awards for a merge request note.
    pub fn merge_request_note_awards(&self, project: ProjectId, merge_request: MergeRequestId,
Ben Boeckel's avatar
Ben Boeckel committed
398
                                     note: NoteId)
Ben Boeckel's avatar
Ben Boeckel committed
399
                                     -> Result<Vec<AwardEmoji>> {
400
401
402
403
404
405
        self._get_paged(&format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
                                 project,
                                 merge_request,
                                 note))
    }

406
    /// Create a note on a merge request.
Ben Boeckel's avatar
Ben Boeckel committed
407
408
    pub fn create_merge_request_note(&self, project: ProjectId, merge_request: MergeRequestId,
                                     content: &str)
Ben Boeckel's avatar
Ben Boeckel committed
409
                                     -> Result<Note> {
Ben Boeckel's avatar
Ben Boeckel committed
410
411
412
        let path = &format!("projects/{}/merge_requests/{}/notes",
                            project,
                            merge_request);
Makoto Nakashima's avatar
Makoto Nakashima committed
413
        self._post_with_param(path, &[("body", content)])
414
415
    }

Ben Boeckel's avatar
Ben Boeckel committed
416
    /// Create a URL to an API endpoint.
Ben Boeckel's avatar
Ben Boeckel committed
417
    fn _mk_url(&self, url: &str) -> Result<Url> {
418
        debug!(target: "gitlab", "api call {}", url);
419
        Ok(self.base_url.join(url).chain_err(|| ErrorKind::UrlParse)?)
420
421
    }

Ben Boeckel's avatar
Ben Boeckel committed
422
    /// Create a URL to an API endpoint with query parameters.
Ben Boeckel's avatar
Ben Boeckel committed
423
    fn _mk_url_with_param<I, K, V>(&self, url: &str, param: I) -> Result<Url>
Makoto Nakashima's avatar
Makoto Nakashima committed
424
425
426
427
428
        where I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
429
        let mut full_url = self._mk_url(url)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
430
431
        full_url.query_pairs_mut().extend_pairs(param);
        Ok(full_url)
432
433
    }

Ben Boeckel's avatar
Ben Boeckel committed
434
    /// Refactored code which talks to Gitlab and transforms error messages properly.
Ben Boeckel's avatar
Ben Boeckel committed
435
    fn _comm<T>(&self, req: RequestBuilder) -> Result<T>
Makoto Nakashima's avatar
Makoto Nakashima committed
436
        where T: Deserialize,
437
    {
Makoto Nakashima's avatar
Makoto Nakashima committed
438
        let req = req.header(GitlabPrivateToken(self.token.to_string()));
439
        let rsp = req.send().chain_err(|| ErrorKind::Communication)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
440
        if !rsp.status().is_success() {
441
            let v = serde_json::from_reader(rsp).chain_err(|| ErrorKind::Deserialize)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
442
            return Err(Error::from_gitlab(v));
443
444
        }

445
        let v = serde_json::from_reader(rsp).chain_err(|| ErrorKind::Deserialize)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
446
447
448
        debug!(target: "gitlab",
               "received data: {:?}",
               v);
449
        Ok(serde_json::from_value::<T>(v).chain_err(|| ErrorKind::Deserialize)?)
450
451
    }

Ben Boeckel's avatar
Ben Boeckel committed
452
    /// Create a `GET` request to an API endpoint.
Ben Boeckel's avatar
Ben Boeckel committed
453
    fn _get<T: Deserialize>(&self, url: &str) -> Result<T> {
Makoto Nakashima's avatar
Makoto Nakashima committed
454
455
        let param: &[(&str, &str)] = &[];
        self._get_with_param(url, param)
456
457
    }

Ben Boeckel's avatar
Ben Boeckel committed
458
    /// Create a `GET` request to an API endpoint with query parameters.
Ben Boeckel's avatar
Ben Boeckel committed
459
    fn _get_with_param<T, I, K, V>(&self, url: &str, param: I) -> Result<T>
Makoto Nakashima's avatar
Makoto Nakashima committed
460
461
462
463
464
465
        where T: Deserialize,
              I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
466
467
        let full_url = self._mk_url_with_param(url, param)?;
        let req = Client::new().chain_err(|| ErrorKind::Communication)?.get(full_url);
Makoto Nakashima's avatar
Makoto Nakashima committed
468
        self._comm(req)
469
470
    }

Ben Boeckel's avatar
Ben Boeckel committed
471
    /// Create a `POST` request to an API endpoint.
Ben Boeckel's avatar
Ben Boeckel committed
472
    fn _post<T: Deserialize>(&self, url: &str) -> Result<T> {
Makoto Nakashima's avatar
Makoto Nakashima committed
473
474
        let param: &[(&str, &str)] = &[];
        self._post_with_param(url, param)
475
476
    }

Ben Boeckel's avatar
Ben Boeckel committed
477
    /// Create a `POST` request to an API endpoint with query parameters.
Ben Boeckel's avatar
Ben Boeckel committed
478
    fn _post_with_param<T, U>(&self, url: &str, param: U) -> Result<T>
Makoto Nakashima's avatar
Makoto Nakashima committed
479
480
481
        where T: Deserialize,
              U: Serialize,
    {
482
483
        let full_url = self._mk_url(url)?;
        let req = Client::new().chain_err(|| ErrorKind::Communication)?.post(full_url).form(&param);
Makoto Nakashima's avatar
Makoto Nakashima committed
484
        self._comm(req)
485
486
    }

Ben Boeckel's avatar
Ben Boeckel committed
487
    /// Handle paginated queries. Returns all results.
Ben Boeckel's avatar
Ben Boeckel committed
488
    fn _get_paged<T: Deserialize>(&self, url: &str) -> Result<Vec<T>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
489
490
        let param: &[(&str, &str)] = &[];
        self._get_paged_with_param(url, param)
491
492
    }

Ben Boeckel's avatar
Ben Boeckel committed
493
    /// Handle paginated queries with query parameters. Returns all results.
Ben Boeckel's avatar
Ben Boeckel committed
494
    fn _get_paged_with_param<T, I, K, V>(&self, url: &str, param: I) -> Result<Vec<T>>
Makoto Nakashima's avatar
Makoto Nakashima committed
495
496
497
498
499
500
        where T: Deserialize,
              I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
501
        let mut page_num = 1;
502
503
504
        let per_page = 100;
        let per_page_str = &format!("{}", per_page);

505
        let full_url = self._mk_url_with_param(url, param)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
506

507
        let mut results: Vec<T> = vec![];
508
509

        loop {
510
            let page_str = &format!("{}", page_num);
Makoto Nakashima's avatar
Makoto Nakashima committed
511
512
513
            let mut page_url = full_url.clone();
            page_url.query_pairs_mut()
                .extend_pairs(&[("page", page_str), ("per_page", per_page_str)]);
514
            let req = Client::new().chain_err(|| ErrorKind::Communication)?.get(page_url);
515

516
            let page: Vec<T> = self._comm(req)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
517
518
            let page_len = page.len();
            results.extend(page);
519

520
521
522
523
            // Gitlab used to have issues returning paginated results; these have been fixed since,
            // but if it is needed, the bug manifests as Gitlab returning *all* results instead of
            // just the requested results. This can cause an infinite loop here if the number of
            // total results is exactly equal to `per_page`.
524
            if page_len != per_page {
525
                break;
526
            }
527
            page_num += 1;
528
529
        }

530
        Ok(results)
531
532
    }
}