1#![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 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 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
217pub struct Challonge {
219 client: reqwest::blocking::Client,
220}
221impl Challonge {
222 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}