gitlab.rs 20.5 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
extern crate reqwest;
10
use self::reqwest::{Client, Method, RequestBuilder, Url};
11

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

18
19
20
21
22
extern crate serde_json;

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

23
24
use error::*;
use types::*;
25

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

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

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

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

Ben Boeckel's avatar
Ben Boeckel committed
48
#[derive(Debug)]
49
50
51
52
53
54
55
56
57
58
59
60
/// 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>,
}

61
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62
63
64
65
66
67
68
69
70
/// 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,
}
71
72
73
74
75
76

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

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

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

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

98
99
        let api = Gitlab {
            base_url: base_url,
100
            token: token,
101
        };
102

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

106
        Ok(api)
107
108
    }

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

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

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

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

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

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

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

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

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

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

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

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

Ben Boeckel's avatar
Ben Boeckel committed
178
    /// HTTP parameters required to register to a project.
Makoto Nakashima's avatar
Makoto Nakashima committed
179
180
181
182
183
184
185
186
    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
187
188
189
    }

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

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

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

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

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

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

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

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

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

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

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

    /// Get comments on a commit.
245
    pub fn commit_comments(&self, project: ProjectId, commit: &str) -> 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.
294
    pub fn commit_latest_builds(&self, project: ProjectId, commit: &str) -> Result<Vec<Build>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
295
296
297
        self._get_paged(&format!("projects/{}/repository/commits/{}/builds", project, commit))
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ben Boeckel's avatar
Ben Boeckel committed
460
    /// Create a `GET` request to an API endpoint with query parameters.
Ben Boeckel's avatar
Ben Boeckel committed
461
    fn _get_with_param<T, I, K, V>(&self, url: &str, param: I) -> Result<T>
Makoto Nakashima's avatar
Makoto Nakashima committed
462
463
464
465
466
467
        where T: Deserialize,
              I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
468
469
        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
470
        self._comm(req)
471
472
    }

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

Ben Boeckel's avatar
Ben Boeckel committed
479
    /// Create a `POST` request to an API endpoint with query parameters.
Ben Boeckel's avatar
Ben Boeckel committed
480
    fn _post_with_param<T, U>(&self, url: &str, param: U) -> Result<T>
Makoto Nakashima's avatar
Makoto Nakashima committed
481
482
483
        where T: Deserialize,
              U: Serialize,
    {
484
485
        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
486
        self._comm(req)
487
488
    }

489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
    /// Create a `PUT` request to an API endpoint.
    fn _put<T: Deserialize>(&self, url: &str) -> Result<T> {
        let param: &[(&str, &str)] = &[];
        self._put_with_param(url, param)
    }

    /// Create a `PUT` request to an API endpoint with query parameters.
    fn _put_with_param<T, U>(&self, url: &str, param: U) -> Result<T>
        where T: Deserialize,
              U: Serialize,
    {
        let full_url = self._mk_url(url)?;
        let req = Client::new().chain_err(|| ErrorKind::Communication)?.request(Method::Put, full_url).form(&param);
        self._comm(req)
    }

Ben Boeckel's avatar
Ben Boeckel committed
505
    /// Handle paginated queries. Returns all results.
Ben Boeckel's avatar
Ben Boeckel committed
506
    fn _get_paged<T: Deserialize>(&self, url: &str) -> Result<Vec<T>> {
Makoto Nakashima's avatar
Makoto Nakashima committed
507
508
        let param: &[(&str, &str)] = &[];
        self._get_paged_with_param(url, param)
509
510
    }

Ben Boeckel's avatar
Ben Boeckel committed
511
    /// Handle paginated queries with query parameters. Returns all results.
Ben Boeckel's avatar
Ben Boeckel committed
512
    fn _get_paged_with_param<T, I, K, V>(&self, url: &str, param: I) -> Result<Vec<T>>
Makoto Nakashima's avatar
Makoto Nakashima committed
513
514
515
516
517
518
        where T: Deserialize,
              I: IntoIterator,
              I::Item: Borrow<(K, V)>,
              K: AsRef<str>,
              V: AsRef<str>,
    {
519
        let mut page_num = 1;
520
521
522
        let per_page = 100;
        let per_page_str = &format!("{}", per_page);

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

525
        let mut results: Vec<T> = vec![];
526
527

        loop {
528
            let page_str = &format!("{}", page_num);
Makoto Nakashima's avatar
Makoto Nakashima committed
529
530
531
            let mut page_url = full_url.clone();
            page_url.query_pairs_mut()
                .extend_pairs(&[("page", page_str), ("per_page", per_page_str)]);
532
            let req = Client::new().chain_err(|| ErrorKind::Communication)?.get(page_url);
533

534
            let page: Vec<T> = self._comm(req)?;
Makoto Nakashima's avatar
Makoto Nakashima committed
535
536
            let page_len = page.len();
            results.extend(page);
537

538
539
540
541
            // 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`.
542
            if page_len != per_page {
543
                break;
544
            }
545
            page_num += 1;
546
547
        }

548
        Ok(results)
549
550
    }
}