challonge/
lib.rs

1//! Client library for the [Challonge](https://challonge.com) REST API.
2//!
3//! Log in to Challonge with `Challonge::new`.
4//! Call API methods to interact with the service.
5//!
6//! For Challonge API documentation [look here](http://api.challonge.com/ru/v1/documents).
7//!
8//! For examples, see the `examples` directory in the source tree.
9#![warn(missing_docs)]
10#![deny(warnings)]
11
12use chrono::offset::Local;
13use chrono::Date;
14#[macro_use]
15mod macroses;
16pub mod attachments;
17pub mod error;
18pub mod matches;
19pub mod participants;
20pub mod tournament;
21mod util;
22pub use attachments::{Attachment, AttachmentCreate, AttachmentId, Index as AttachmentIndex};
23use error::Error;
24pub use matches::{
25    Index as MatchIndex, Match, MatchId, MatchScore, MatchScores, MatchState, MatchUpdate,
26};
27pub use participants::{Index as ParticipantIndex, Participant, ParticipantCreate, ParticipantId};
28pub use tournament::{
29    Index as TournamentIndex, Tournament, TournamentCreate, TournamentId, TournamentIncludes,
30    TournamentState, TournamentType,
31};
32
33const API_BASE: &str = "https://api.challonge.com/v1";
34
35fn make_headers(user_name: String, api_key: String) -> reqwest::header::HeaderMap {
36    // use headers::Authorization;
37    // use headers::Header;
38
39    let mut headers = reqwest::header::HeaderMap::new();
40    headers.insert(
41        reqwest::header::AUTHORIZATION,
42        format!(
43            "Basic {}",
44            base64::encode(format!("{}:{}", user_name, api_key))
45        )
46        .parse()
47        .unwrap(),
48    );
49    // let auth_header = Authorization::basic(&user_name, &api_key);
50    // let mut value = headers::HeaderValue::;
51    // headers.extend(.encode(values));
52    // headers.insert(hyper::header::AUTHORIZATION, Authorization::basic(&user_name, &api_key).encode().);
53    headers
54}
55
56type FieldPairs = Vec<(&'static str, String)>;
57
58fn pairs_to_string(params: FieldPairs) -> String {
59    let mut body = String::new();
60    let mut sep = "";
61    for p in params {
62        body.push_str(sep);
63        body.push_str(&format!("{}={}", p.0, p.1));
64        sep = "&";
65    }
66    body
67}
68
69fn pcs_to_pairs(participants: Vec<ParticipantCreate>) -> FieldPairs {
70    let mut params = Vec::new();
71    for p in participants {
72        params.push((ps!("email"), p.email.clone()));
73        params.push((ps!("seed"), p.seed.to_string()));
74        params.push((ps!("misc"), p.misc.clone()));
75
76        if let Some(n) = p.name.as_ref() {
77            params.push((ps!("name"), n.clone()));
78        }
79        if let Some(un) = p.challonge_username.as_ref() {
80            params.push((ps!("challonge_username"), un.clone()));
81        }
82    }
83    params
84}
85
86fn pc_to_pairs(participant: &ParticipantCreate) -> FieldPairs {
87    let mut params = vec![
88        (p!("email"), participant.email.clone()),
89        (p!("seed"), participant.seed.to_string()),
90        (p!("misc"), participant.misc.clone()),
91    ];
92
93    if let Some(n) = participant.name.as_ref() {
94        params.push((p!("name"), n.clone()));
95    }
96    if let Some(un) = participant.challonge_username.as_ref() {
97        params.push((p!("challonge_username"), un.clone()));
98    }
99    params
100}
101
102fn at_to_pairs(attachment: &AttachmentCreate) -> FieldPairs {
103    let mut params = FieldPairs::new();
104
105    if let Some(a) = attachment.asset.as_ref() {
106        params.push((a!("asset"), String::from_utf8(a.clone()).unwrap()));
107    }
108    if let Some(url) = attachment.url.as_ref() {
109        params.push((a!("url"), url.clone()));
110    }
111    if let Some(d) = attachment.description.as_ref() {
112        params.push((a!("description"), d.clone()));
113    }
114    params
115}
116
117fn tc_to_pairs(tournament: &TournamentCreate) -> FieldPairs {
118    let mut params = vec![
119        (t!("name"), tournament.name.clone()),
120        (
121            t!("tournament_type"),
122            tournament.tournament_type.to_string(),
123        ),
124        (t!("url"), tournament.url.clone()),
125        (t!("subdomain"), tournament.subdomain.clone()),
126        (t!("description"), tournament.description.clone()),
127        (t!("open_signup"), tournament.open_signup.to_string()),
128        (
129            t!("hold_third_place_match"),
130            tournament.hold_third_place_match.to_string(),
131        ),
132        (
133            t!("pts_for_match_win"),
134            tournament.swiss_points.match_win.to_string(),
135        ),
136        (
137            t!("pts_for_match_tie"),
138            tournament.swiss_points.match_tie.to_string(),
139        ),
140        (
141            t!("pts_for_game_win"),
142            tournament.swiss_points.game_win.to_string(),
143        ),
144        (
145            t!("pts_for_game_tie"),
146            tournament.swiss_points.game_tie.to_string(),
147        ),
148        (t!("swiss_rounds"), tournament.swiss_rounds.to_string()),
149        (t!("ranked_by"), tournament.ranked_by.to_string()),
150        (
151            t!("rr_pts_for_match_win"),
152            tournament.round_robin_points.match_win.to_string(),
153        ),
154        (
155            t!("rr_pts_for_match_tie"),
156            tournament.round_robin_points.match_tie.to_string(),
157        ),
158        (
159            t!("rr_pts_for_game_win"),
160            tournament.round_robin_points.game_win.to_string(),
161        ),
162        (
163            t!("rr_pts_for_game_tie"),
164            tournament.round_robin_points.game_tie.to_string(),
165        ),
166        (t!("show_rounds"), tournament.show_rounds.to_string()),
167        (t!("private"), tournament.private.to_string()),
168        (
169            t!("notify_users_when_matches_open"),
170            tournament.notify_users_when_matches_open.to_string(),
171        ),
172        (
173            t!("notify_users_when_the_tournament_ends"),
174            tournament.notify_users_when_the_tournament_ends.to_string(),
175        ),
176        (
177            t!("sequential_pairings"),
178            tournament.sequential_pairings.to_string(),
179        ),
180        (t!("signup_cap"), tournament.signup_cap.to_string()),
181        (
182            t!("check_in_duration"),
183            tournament.check_in_duration.to_string(),
184        ),
185    ];
186    if let Some(gfm) = tournament.grand_finals_modifier.as_ref() {
187        params.push((t!("grand_finals_modifier"), gfm.clone()));
188    }
189    if let Some(start_at) = tournament.start_at.as_ref() {
190        params.push((t!("start_at"), start_at.to_rfc3339()));
191    }
192    if let Some(s_bye_pts) = tournament.swiss_points.bye.as_ref() {
193        params.push((t!("pts_for_bye"), s_bye_pts.to_string()));
194    }
195    if let Some(game) = tournament.game_name.as_ref() {
196        params.push((t!("game_name"), game.clone()));
197    }
198    params
199}
200
201fn mu_to_pairs(mu: &MatchUpdate) -> FieldPairs {
202    let mut params = Vec::new();
203
204    if let Some(v) = mu.player1_votes {
205        params.push((m!("player1_votes"), v.to_string()));
206    }
207    if let Some(v) = mu.player2_votes {
208        params.push((m!("player2_votes"), v.to_string()));
209    }
210    params.push((m!("scores_csv"), mu.scores_csv.to_string()));
211    if let Some(w) = mu.winner_id.as_ref() {
212        params.push((m!("winner_id"), w.0.to_string()));
213    }
214    params
215}
216
217/// Client for the Challonge REST API.
218pub struct Challonge {
219    client: reqwest::blocking::Client,
220}
221impl Challonge {
222    /// Create new connection to Challonge.
223    /// # Example
224    /// ```ignore
225    /// extern crate challonge;
226    ///
227    /// use self::challonge::Challonge;
228    ///
229    /// let c = Challonge::new("myusername", "myapikey");
230    /// ```
231    pub fn new<S: Into<String>>(user_name: S, api_key: S) -> Challonge {
232        Challonge {
233            client: reqwest::blocking::Client::builder()
234                .default_headers(make_headers(user_name.into(), api_key.into()))
235                .build()
236                .expect("Couldn't build the HTTP client."),
237        }
238    }
239
240    /// Retrieve a set of tournaments created with your account.
241    /// # Example
242    /// ```ignore
243    /// extern crate challonge;
244    /// extern crate chrono;
245    ///
246    /// use self::challonge::Challonge;
247    /// use self::challonge::tournament::{ TournamentState, TournamentType };
248    /// use self::chrono::*;
249    ///
250    /// let c = Challonge::new("myusername", "myapikey");
251    /// let index = c.tournament_index (
252    ///        &TournamentState::All,
253    ///        &TournamentType::DoubleElimination,
254    ///        &Local::today(),
255    ///        &Local::today(),
256    ///        "subdomain"
257    /// );
258    /// ```
259    pub fn tournament_index(
260        &self,
261        state: &TournamentState,
262        tournament_type: &TournamentType,
263        created_after: &Date<Local>,
264        created_before: &Date<Local>,
265        subdomain: &str,
266    ) -> Result<TournamentIndex, Error> {
267        let url = format!(
268            "{}/tournaments.json?state={}&type={}&created_after={}&created_before={}&subdomain={}",
269            API_BASE,
270            state,
271            tournament_type.to_get_param(),
272            format_date!(created_after),
273            format_date!(created_before),
274            subdomain
275        );
276
277        let response = self.client.get(&url).send()?;
278        TournamentIndex::decode(serde_json::from_reader(response)?)
279    }
280
281    /// Retrieve a single tournament record created with your account.
282    /// # Example
283    /// ```ignore
284    /// extern crate challonge;
285    ///
286    /// use challonge::Challonge;
287    ///
288    /// let c = Challonge::new("myusername", "myapikey");
289    /// let i = TournamentIncludes::Matches;
290    /// let t = c.get_tournament(&TournamentId::Id(2669881), &i);
291    /// ```
292    pub fn get_tournament(
293        &self,
294        id: &TournamentId,
295        includes: &TournamentIncludes,
296    ) -> Result<Tournament, Error> {
297        let mut url =
298            reqwest::Url::parse(&format!("{}/tournaments/{}.json", API_BASE, id.to_string()))
299                .unwrap();
300
301        Challonge::add_tournament_includes(&mut url, includes);
302        let response = self.client.get(url).send()?;
303        Tournament::decode(serde_json::from_reader(response)?)
304    }
305
306    /// Create a new tournament.
307    /// # Example
308    /// ```ignore
309    /// extern crate challonge;
310    ///
311    /// use challonge::Challonge;
312    /// use challonge::tournament::TournamentCreate;
313    ///
314    /// let c = Challonge::new("myusername", "myapikey");
315    /// let tc = TournamentCreate { // explicitly define the whole structure
316    ///            name: "Tester".to_owned(),
317    ///            tournament_type: TournamentType::SingleElimination,
318    ///            url: "testerurl".to_owned(),
319    ///            subdomain: "subdomain".to_owned(),
320    ///            description: "Test tournament created from challonge-rs".to_owned(),
321    ///            open_signup: false,
322    ///            hold_third_place_match: false,
323    ///            pts_for_match_win: 0.0f64,
324    ///            pts_for_match_tie: 0.0f64,
325    ///            pts_for_game_win: 0.0f64,
326    ///            pts_for_game_tie: 0.0f64,
327    ///            pts_for_bye: 0.0f64,
328    ///            swiss_rounds: 0,
329    ///            ranked_by: RankedBy::PointsScored,
330    ///            rr_pts_for_match_win: 0.0f64,
331    ///            rr_pts_for_match_tie: 0.0f64,
332    ///            rr_pts_for_game_win: 0.0f64,
333    ///            rr_pts_for_game_tie: 0.0f64,
334    ///            show_rounds: false,
335    ///            private: false,
336    ///            notify_users_when_matches_open: true,
337    ///            notify_users_when_the_tournament_ends: true,
338    ///            sequential_pairings: false,
339    ///            signup_cap: 4,
340    ///            start_at: UTC::now().add(Duration::weeks(2)),
341    ///            check_in_duration: 60,
342    ///            grand_finals_modifier: None,
343    /// };
344    /// let t = c.create_tournament(&tc);
345    /// // or you may create `TournamentCreate` by using a builder:
346    /// let mut tcb = TournamentCreate::new();
347    /// tcb.name("Test tournament")
348    ///   .tournament_type(TournamentType::SingleElimination)
349    ///   .url("TestUrl")
350    ///   .subdomain("subdomain")
351    ///   .description("TEST TOURNAMENT created by challonge-rs");
352    /// let tb = c.create_tournament(&tcb);
353    /// ```
354    pub fn create_tournament(&self, tournament: &TournamentCreate) -> Result<Tournament, Error> {
355        let url = &format!("{}/tournaments.json", API_BASE);
356        let body = pairs_to_string(tc_to_pairs(tournament));
357        let response = self.client.post(url).body(body).send()?;
358        Tournament::decode(serde_json::from_reader(response)?)
359    }
360
361    /// Update a tournament's attributes.
362    pub fn update_tournament(
363        &self,
364        id: &TournamentId,
365        tournament: &TournamentCreate,
366    ) -> Result<Tournament, Error> {
367        let url = &format!("{}/tournaments/{}.json", API_BASE, id.to_string());
368        let body = pairs_to_string(tc_to_pairs(tournament));
369        let response = self.client.put(url).body(body).send()?;
370        Tournament::decode(serde_json::from_reader(response)?)
371    }
372
373    /// Deletes a tournament along with all its associated records. There is no undo, so use with care!
374    pub fn delete_tournament(&self, id: &TournamentId) -> Result<(), Error> {
375        let url = &format!("{}/tournaments/{}.json", API_BASE, id.to_string());
376        let _ = self.client.delete(url).send()?;
377        Ok(())
378    }
379
380    /// This should be invoked after a tournament's check-in window closes before the tournament is started.
381    ///
382    /// 1. Marks participants who have not checked in as inactive.
383    /// 2. Moves inactive participants to bottom seeds (ordered by original seed).
384    /// 3. Transitions the tournament state from 'checking_in' to 'checked_in'
385    ///
386    /// NOTE: Checked in participants on the waiting list will be promoted if slots become available.
387    pub fn tournament_process_checkins(
388        &self,
389        id: &TournamentId,
390        includes: &TournamentIncludes,
391    ) -> Result<(), Error> {
392        self.tournament_action("process_check_ins", id, includes)
393    }
394
395    /// When your tournament is in a 'checking_in' or 'checked_in' state, there's no way to edit the tournament's start time (start_at) or check-in duration (check_in_duration). You must first abort check-in, then you may edit those attributes.
396    ///
397    /// 1. Makes all participants active and clears their checked_in_at times.
398    /// 2. Transitions the tournament state from 'checking_in' or 'checked_in' to 'pending'
399    pub fn tournament_abort_checkins(
400        &self,
401        id: &TournamentId,
402        includes: &TournamentIncludes,
403    ) -> Result<(), Error> {
404        self.tournament_action("abort_check_in", id, includes)
405    }
406
407    /// Start a tournament, opening up first round matches for score reporting. The tournament must have at least 2 participants.
408    pub fn tournament_start(
409        &self,
410        id: &TournamentId,
411        includes: &TournamentIncludes,
412    ) -> Result<(), Error> {
413        self.tournament_action("start", id, includes)
414    }
415
416    /// Finalize a tournament that has had all match scores submitted, rendering its results permanent.
417    pub fn tournament_finalize(
418        &self,
419        id: &TournamentId,
420        includes: &TournamentIncludes,
421    ) -> Result<(), Error> {
422        self.tournament_action("finalize", id, includes)
423    }
424
425    /// Reset a tournament, clearing all of its scores and attachments. You can then add/remove/edit participants before starting the tournament again.
426    pub fn tournament_reset(
427        &self,
428        id: &TournamentId,
429        includes: &TournamentIncludes,
430    ) -> Result<(), Error> {
431        self.tournament_action("reset", id, includes)
432    }
433
434    /// Retrieve a tournament's participant list.
435    pub fn participant_index(&self, id: &TournamentId) -> Result<ParticipantIndex, Error> {
436        let url = &format!(
437            "{}/tournaments/{}/participants.json",
438            API_BASE,
439            id.to_string()
440        );
441        let response = self.client.get(url).send()?;
442        ParticipantIndex::decode(serde_json::from_reader(response)?)
443    }
444
445    /// Add a participant to a tournament (up until it is started).
446    pub fn create_participant(
447        &self,
448        id: &TournamentId,
449        participant: &ParticipantCreate,
450    ) -> Result<Participant, Error> {
451        let url = &format!(
452            "{}/tournaments/{}/participants.json",
453            API_BASE,
454            id.to_string()
455        );
456        let body = pairs_to_string(pc_to_pairs(participant));
457        let response = self.client.post(url).body(body).send()?;
458        Participant::decode(serde_json::from_reader(response)?)
459    }
460
461    /// Bulk add participants to a tournament (up until it is started).
462    /// If an invalid participant is detected, bulk participant creation will halt and any previously added participants (from this API request) will be rolled back.
463    pub fn create_participant_bulk(
464        &self,
465        id: &TournamentId,
466        participants: Vec<ParticipantCreate>,
467    ) -> Result<(), Error> {
468        let url = &format!(
469            "{}/tournaments/{}/participants/bulk_add.json",
470            API_BASE,
471            id.to_string()
472        );
473        let body = pairs_to_string(pcs_to_pairs(participants));
474        let response = self.client.post(url).body(body).send()?;
475        let _: () = serde_json::from_reader(response)?;
476        Ok(())
477    }
478
479    /// Retrieve a single participant record for a tournament.
480    pub fn get_participant(
481        &self,
482        id: &TournamentId,
483        participant_id: &ParticipantId,
484        include_matches: bool,
485    ) -> Result<Participant, Error> {
486        let mut url = reqwest::Url::parse(&format!(
487            "{}/tournaments/{}/participants/{}.json",
488            API_BASE,
489            id.to_string(),
490            participant_id.0
491        ))
492        .unwrap();
493
494        url.query_pairs_mut()
495            .append_pair("include_matches", &(include_matches as i64).to_string());
496
497        let response = self.client.get(url).send()?;
498        Participant::decode(serde_json::from_reader(response)?)
499    }
500
501    /// Update the attributes of a tournament participant.
502    pub fn update_participant(
503        &self,
504        id: &TournamentId,
505        participant_id: &ParticipantId,
506        participant: &ParticipantCreate,
507    ) -> Result<(), Error> {
508        let url = &format!(
509            "{}/tournaments/{}/participants/{}.json",
510            API_BASE,
511            id.to_string(),
512            participant_id.0
513        );
514        let body = pairs_to_string(pc_to_pairs(participant));
515        let _ = self.client.put(url).body(body).send()?;
516        Ok(())
517    }
518
519    /// Checks a participant in, setting checked_in_at to the current time.
520    pub fn check_in_participant(
521        &self,
522        id: &TournamentId,
523        participant_id: &ParticipantId,
524    ) -> Result<(), Error> {
525        let url = &format!(
526            "{}/tournaments/{}/participants/{}/check_in.json",
527            API_BASE,
528            id.to_string(),
529            participant_id.0
530        );
531        let _ = self.client.post(url).send()?;
532        Ok(())
533    }
534
535    /// Marks a participant as having not checked in, setting checked_in_at to nil.
536    pub fn undo_check_in_participant(
537        &self,
538        id: &TournamentId,
539        participant_id: &ParticipantId,
540    ) -> Result<(), Error> {
541        let url = &format!(
542            "{}/tournaments/{}/participants/{}/undo_check_in.json",
543            API_BASE,
544            id.to_string(),
545            participant_id.0
546        );
547        let _ = self.client.post(url).send()?;
548        Ok(())
549    }
550
551    /// If the tournament has not started, delete a participant, automatically filling in the abandoned seed number.
552    /// If tournament is underway, mark a participant inactive, automatically forfeiting his/her remaining matches.
553    pub fn delete_participant(
554        &self,
555        id: &TournamentId,
556        participant_id: &ParticipantId,
557    ) -> Result<(), Error> {
558        let url = &format!(
559            "{}/tournaments/{}/participants/{}.json",
560            API_BASE,
561            id.to_string(),
562            participant_id.0
563        );
564        let _ = self.client.delete(url).send()?;
565        Ok(())
566    }
567
568    /// Randomize seeds among participants. Only applicable before a tournament has started.
569    pub fn randomize_participants(&self, id: &TournamentId) -> Result<(), Error> {
570        let url = &format!(
571            "{}/tournaments/{}/participants/randomize.json",
572            API_BASE,
573            id.to_string()
574        );
575        let _ = self.client.post(url).send()?;
576        Ok(())
577    }
578
579    /// Retrieve a tournament's match list.
580    pub fn match_index(
581        &self,
582        id: &TournamentId,
583        state: Option<MatchState>,
584        participant_id: Option<ParticipantId>,
585    ) -> Result<MatchIndex, Error> {
586        let mut url = reqwest::Url::parse(&format!(
587            "{}/tournaments/{}/matches.json",
588            API_BASE,
589            id.to_string()
590        ))
591        .unwrap();
592        {
593            let mut pairs = url.query_pairs_mut();
594            if let Some(s) = state {
595                pairs.append_pair("state", &s.to_string());
596            }
597            if let Some(pid) = participant_id {
598                pairs.append_pair("participant_id", &pid.0.to_string());
599            }
600        }
601        let response = self.client.get(url.as_str()).send()?;
602        MatchIndex::decode(serde_json::from_reader(response)?)
603    }
604
605    /// Retrieve a single match record for a tournament.
606    pub fn get_match(
607        &self,
608        id: &TournamentId,
609        match_id: &MatchId,
610        include_attachments: bool,
611    ) -> Result<Match, Error> {
612        let mut url = reqwest::Url::parse(&format!(
613            "{}/tournaments/{}/matches/{}.json",
614            API_BASE,
615            id.to_string(),
616            match_id.0
617        ))
618        .unwrap();
619
620        url.query_pairs_mut().append_pair(
621            "include_attachments",
622            &(include_attachments as i64).to_string(),
623        );
624
625        let response = self.client.get(url.as_str()).send()?;
626
627        Match::decode(serde_json::from_reader(response)?)
628    }
629
630    /// Update/submit the score(s) for a match.
631    pub fn update_match(
632        &self,
633        id: &TournamentId,
634        match_id: &MatchId,
635        match_update: &MatchUpdate,
636    ) -> Result<Match, Error> {
637        let url = &format!(
638            "{}/tournaments/{}/matches/{}.json",
639            API_BASE,
640            id.to_string(),
641            match_id.0
642        );
643        let body = pairs_to_string(mu_to_pairs(match_update));
644        let response = self.client.put(url).body(body).send()?;
645        Match::decode(serde_json::from_reader(response)?)
646    }
647
648    /// Retrieve a match's attachments.
649    pub fn attachments_index(
650        &self,
651        id: &TournamentId,
652        match_id: &MatchId,
653    ) -> Result<AttachmentIndex, Error> {
654        let url = &format!(
655            "{}/tournaments/{}/matches/{}/attachments.json",
656            API_BASE,
657            id.to_string(),
658            match_id.0
659        );
660        let response = self.client.get(url).send()?;
661        AttachmentIndex::decode(serde_json::from_reader(response)?)
662    }
663
664    /// Retrieve a single match attachment record.
665    pub fn get_attachment(
666        &self,
667        id: &TournamentId,
668        match_id: &MatchId,
669        attachment_id: &AttachmentId,
670    ) -> Result<Attachment, Error> {
671        let url = &format!(
672            "{}/tournaments/{}/matches/{}/attachments/{}.json",
673            API_BASE,
674            id.to_string(),
675            match_id.0,
676            attachment_id.0
677        );
678        let response = self.client.get(url).send()?;
679        Attachment::decode(serde_json::from_reader(response)?)
680    }
681
682    /// Add a file, link, or text attachment to a match. NOTE: The associated tournament's "accept_attachments" attribute must be true for this action to succeed.
683    pub fn create_attachment(
684        &self,
685        id: &TournamentId,
686        match_id: &MatchId,
687        attachment: &AttachmentCreate,
688    ) -> Result<Attachment, Error> {
689        let url = &format!(
690            "{}/tournaments/{}/matches/{}/attachments.json",
691            API_BASE,
692            id.to_string(),
693            match_id.0
694        );
695        let body = pairs_to_string(at_to_pairs(attachment));
696        let response = self.client.post(url).body(body).send()?;
697        Attachment::decode(serde_json::from_reader(response)?)
698    }
699
700    /// Update the attributes of a match attachment.
701    pub fn update_attachment(
702        &self,
703        id: &TournamentId,
704        match_id: &MatchId,
705        attachment_id: &AttachmentId,
706        attachment: &AttachmentCreate,
707    ) -> Result<Attachment, Error> {
708        let url = &format!(
709            "{}/tournaments/{}/matches/{}/attachments/{}.json",
710            API_BASE,
711            id.to_string(),
712            match_id.0,
713            attachment_id.0
714        );
715        let body = pairs_to_string(at_to_pairs(attachment));
716        let response = self.client.put(url).body(body).send()?;
717        Attachment::decode(serde_json::from_reader(response)?)
718    }
719
720    /// Delete a match attachment.
721    pub fn delete_attachment(
722        &self,
723        id: &TournamentId,
724        match_id: &MatchId,
725        attachment_id: &AttachmentId,
726    ) -> Result<(), Error> {
727        let url = &format!(
728            "{}/tournaments/{}/matches/{}/attachments/{}.json",
729            API_BASE,
730            id.to_string(),
731            match_id.0,
732            attachment_id.0
733        );
734        let _ = self.client.delete(url).send()?;
735        Ok(())
736    }
737
738    fn tournament_action(
739        &self,
740        endpoint: &str,
741        id: &TournamentId,
742        includes: &TournamentIncludes,
743    ) -> Result<(), Error> {
744        let mut url = reqwest::Url::parse(&format!(
745            "{}/tournaments/{}/{}.json",
746            API_BASE,
747            id.to_string(),
748            endpoint
749        ))
750        .unwrap();
751        Challonge::add_tournament_includes(&mut url, includes);
752        let _ = self.client.post(url.as_str()).send()?;
753        Ok(())
754    }
755
756    // TODO refactor to be better
757    fn add_tournament_includes(url: &mut reqwest::Url, includes: &TournamentIncludes) {
758        let mut pairs = url.query_pairs_mut();
759        match *includes {
760            TournamentIncludes::All => {
761                pairs
762                    .append_pair("include_participants", "1")
763                    .append_pair("include_matches", "1");
764            }
765            TournamentIncludes::Matches => {
766                pairs
767                    .append_pair("include_participants", "0")
768                    .append_pair("include_matches", "1");
769            }
770            TournamentIncludes::Participants => {
771                pairs
772                    .append_pair("include_participants", "1")
773                    .append_pair("include_matches", "0");
774            }
775        }
776    }
777}