gitlab.rs 20 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};

Ben Boeckel's avatar
Ben Boeckel committed
21
use super::error::*;
22
use super::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
93
94
    fn _new(protocol: &str, host: &str, token: String) -> Result<Self> {
        let base_url = try!(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 = try!(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> {
Makoto Nakashima's avatar
Makoto Nakashima committed
124
        let mut users = try!(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.
Ben Boeckel's avatar
Ben Boeckel committed
188
    pub fn add_hook(&self, project: ProjectId, url: &str, events: WebhookEvents)
Ben Boeckel's avatar
Ben Boeckel committed
189
                    -> Result<Hook> {
Makoto Nakashima's avatar
Makoto Nakashima committed
190
191
        let mut flags = Self::event_flags(events);
        flags.push(("url", url));
Ben Boeckel's avatar
Ben Boeckel committed
192

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

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

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

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

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

216
217
    /// 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
218
                               -> Result<Member> {
219
220
221
        let user_str = format!("{}", user);
        let access_str = format!("{}", access);

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

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

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

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

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

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

    /// 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
262
                                      path: &str, line: u64)
Ben Boeckel's avatar
Ben Boeckel committed
263
                                      -> Result<CommitNote> {
264
        let line_str = format!("{}", line);
Ben Boeckel's avatar
Ben Boeckel committed
265
        let line_type = LineType::New;
266

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

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

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

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

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

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

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

Makoto Nakashima's avatar
Makoto Nakashima committed
315
316
317
318
        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)));
319

Makoto Nakashima's avatar
Makoto Nakashima committed
320
        self._post_with_param(path, &params)
321
322
    }

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

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

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

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

Makoto Nakashima's avatar
Makoto Nakashima committed
343
        self._post_with_param(path, &[("body", content)])
Ben Boeckel's avatar
Ben Boeckel committed
344
345
    }

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

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

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

364
365
    /// 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
366
                                       -> Result<Vec<IssueReference>> {
367
368
369
370
371
        self._get_paged(&format!("projects/{}/merge_requests/{}/closes_issues",
                                 project,
                                 merge_request))
    }

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

380
381
    /// 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
382
                                    note: NoteId, award: &str)
Ben Boeckel's avatar
Ben Boeckel committed
383
                                    -> Result<AwardEmoji> {
384
        let path = &format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
Ben Boeckel's avatar
Ben Boeckel committed
385
386
387
                            project,
                            merge_request,
                            note);
Makoto Nakashima's avatar
Makoto Nakashima committed
388
        self._post_with_param(path, &[("name", award)])
389
390
    }

391
392
    /// 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
393
                                -> Result<Vec<AwardEmoji>> {
394
395
396
397
398
        self._get_paged(&format!("projects/{}/merge_requests/{}/award_emoji",
                                 project,
                                 merge_request))
    }

399
400
    /// 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
401
                                     note: NoteId)
Ben Boeckel's avatar
Ben Boeckel committed
402
                                     -> Result<Vec<AwardEmoji>> {
403
404
405
406
407
408
        self._get_paged(&format!("projects/{}/merge_requests/{}/notes/{}/award_emoji",
                                 project,
                                 merge_request,
                                 note))
    }

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

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

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

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

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

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

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

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

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

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

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

Makoto Nakashima's avatar
Makoto Nakashima committed
508
509
        let full_url = try!(self._mk_url_with_param(url, param));

510
        let mut results: Vec<T> = vec![];
511
512

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

Makoto Nakashima's avatar
Makoto Nakashima committed
519
520
521
            let page: Vec<T> = try!(self._comm(req));
            let page_len = page.len();
            results.extend(page);
522

523
524
525
526
            // 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`.
527
            if page_len != per_page {
528
                break;
529
            }
530
            page_num += 1;
531
532
        }

533
        Ok(results)
534
535
    }
}