faceit/http/client.rs
1use crate::error::Error;
2use crate::types::*;
3use std::time::Duration;
4
5const DEFAULT_BASE_URL: &str = "https://open.faceit.com";
6const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
7
8/// Builder for creating a customized `Client`
9pub struct ClientBuilder {
10 base_url: Option<String>,
11 api_key: Option<String>,
12 timeout: Option<Duration>,
13 client_builder: reqwest::ClientBuilder,
14}
15
16impl ClientBuilder {
17 /// Create a new builder with default settings
18 pub fn new() -> Self {
19 Self {
20 base_url: None,
21 api_key: None,
22 timeout: Some(DEFAULT_TIMEOUT),
23 client_builder: reqwest::Client::builder(),
24 }
25 }
26
27 /// Set a custom base URL for the API
28 ///
29 /// # Examples
30 ///
31 /// ```no_run
32 /// use faceit::HttpClient;
33 ///
34 /// let client = HttpClient::builder()
35 /// .base_url("https://custom-api.example.com")
36 /// .build()
37 /// .unwrap();
38 /// ```
39 pub fn base_url(mut self, url: impl Into<String>) -> Self {
40 self.base_url = Some(url.into());
41 self
42 }
43
44 /// Set the API key or access token
45 ///
46 /// For the Data API, you can use either:
47 /// - An API key (obtained from the FACEIT Developer Portal)
48 /// - An access token (obtained via OAuth2)
49 ///
50 /// Both are passed in the `Authorization: Bearer {token}` header.
51 ///
52 /// # Examples
53 ///
54 /// ```no_run
55 /// use faceit::HttpClient;
56 ///
57 /// // Using an API key
58 /// let client = HttpClient::builder()
59 /// .api_key("your-api-key")
60 /// .build()
61 /// .unwrap();
62 ///
63 /// // Using an access token
64 /// let client = HttpClient::builder()
65 /// .api_key("your-access-token")
66 /// .build()
67 /// .unwrap();
68 /// ```
69 pub fn api_key(mut self, key: impl Into<String>) -> Self {
70 self.api_key = Some(key.into());
71 self
72 }
73
74 /// Set the request timeout
75 ///
76 /// # Examples
77 ///
78 /// ```no_run
79 /// use faceit::HttpClient;
80 /// use std::time::Duration;
81 ///
82 /// let client = HttpClient::builder()
83 /// .timeout(Duration::from_secs(60))
84 /// .build()
85 /// .unwrap();
86 /// ```
87 pub fn timeout(mut self, timeout: Duration) -> Self {
88 self.timeout = Some(timeout);
89 self.client_builder = self.client_builder.timeout(timeout);
90 self
91 }
92
93 /// Configure the underlying reqwest client builder
94 ///
95 /// This allows advanced configuration of the HTTP client.
96 pub fn client_builder(mut self, builder: reqwest::ClientBuilder) -> Self {
97 self.client_builder = builder;
98 self
99 }
100
101 /// Build the client
102 ///
103 /// # Examples
104 ///
105 /// ```no_run
106 /// use faceit::HttpClient;
107 ///
108 /// let client = HttpClient::builder()
109 /// .api_key("your-api-key-or-access-token")
110 /// .build()?;
111 /// # Ok::<(), faceit::error::Error>(())
112 /// ```
113 pub fn build(self) -> Result<Client, Error> {
114 let client = self
115 .client_builder
116 .timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT))
117 .build()
118 .map_err(Error::Http)?;
119
120 let base_url = self
121 .base_url
122 .unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
123
124 Ok(Client {
125 reqwest_client: client,
126 base_url,
127 api_key: self.api_key,
128 })
129 }
130}
131
132impl Default for ClientBuilder {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138/// Client for interacting with the FACEIT Public API
139pub struct Client {
140 reqwest_client: reqwest::Client,
141 base_url: String,
142 api_key: Option<String>,
143}
144
145impl Client {
146 /// Create a new client without authentication
147 ///
148 /// Requests without authentication are subject to standard rate limits.
149 ///
150 /// # Examples
151 ///
152 /// ```no_run
153 /// use faceit::HttpClient;
154 ///
155 /// let client = HttpClient::new();
156 /// ```
157 pub fn new() -> Self {
158 ClientBuilder::new()
159 .build()
160 .expect("Failed to create default client")
161 }
162
163 /// Create a builder for customizing the client configuration
164 ///
165 /// # Examples
166 ///
167 /// ```no_run
168 /// use faceit::HttpClient;
169 /// use std::time::Duration;
170 ///
171 /// let client = HttpClient::builder()
172 /// .api_key("your-api-key-or-access-token")
173 /// .timeout(Duration::from_secs(60))
174 /// .base_url("https://custom-api.example.com")
175 /// .build()
176 /// .unwrap();
177 /// ```
178 pub fn builder() -> ClientBuilder {
179 ClientBuilder::new()
180 }
181
182 // ============================================================================
183 // Player Methods
184 // ============================================================================
185
186 /// Get player details by player ID
187 ///
188 /// # Arguments
189 /// * `player_id` - The FACEIT player ID (UUID format)
190 ///
191 /// # Examples
192 ///
193 /// ```no_run
194 /// # use faceit::HttpClient;
195 /// # async fn example() -> Result<(), faceit::error::Error> {
196 /// let client = HttpClient::new();
197 /// let player = client.get_player("player-id-here").await?;
198 /// println!("Player: {}", player.nickname);
199 /// # Ok(())
200 /// # }
201 /// ```
202 pub async fn get_player(&self, player_id: &str) -> Result<Player, Error> {
203 let url = format!("{}/data/v4/players/{}", self.base_url, player_id);
204 let request = self.reqwest_client.get(&url);
205 let request = self.add_api_key_header(request);
206
207 let response = request.send().await?;
208 self.handle_response(response).await
209 }
210
211 /// Get player details from lookup (by nickname, game, or game_player_id)
212 ///
213 /// # Arguments
214 /// * `nickname` - Optional player nickname
215 /// * `game` - Optional game ID
216 /// * `game_player_id` - Optional game-specific player ID
217 ///
218 /// # Examples
219 ///
220 /// ```no_run
221 /// # use faceit::HttpClient;
222 /// # async fn example() -> Result<(), faceit::error::Error> {
223 /// let client = HttpClient::new();
224 /// let player = client.get_player_from_lookup(Some("player_nickname"), None, None).await?;
225 /// # Ok(())
226 /// # }
227 /// ```
228 pub async fn get_player_from_lookup(
229 &self,
230 nickname: Option<&str>,
231 game: Option<&str>,
232 game_player_id: Option<&str>,
233 ) -> Result<Player, Error> {
234 let url = format!("{}/data/v4/players", self.base_url);
235 let mut request = self.reqwest_client.get(&url);
236
237 if let Some(nickname) = nickname {
238 request = request.query(&[("nickname", nickname)]);
239 }
240 if let Some(game) = game {
241 request = request.query(&[("game", game)]);
242 }
243 if let Some(game_player_id) = game_player_id {
244 request = request.query(&[("game_player_id", game_player_id)]);
245 }
246
247 let request = self.add_api_key_header(request);
248 let response = request.send().await?;
249 self.handle_response(response).await
250 }
251
252 /// Get player statistics for a specific game
253 ///
254 /// # Arguments
255 /// * `player_id` - The FACEIT player ID
256 /// * `game_id` - The game ID (e.g., "cs2", "csgo")
257 ///
258 /// # Examples
259 ///
260 /// ```no_run
261 /// # use faceit::HttpClient;
262 /// # async fn example() -> Result<(), faceit::error::Error> {
263 /// let client = HttpClient::new();
264 /// let stats = client.get_player_stats("player-id", "cs2").await?;
265 /// # Ok(())
266 /// # }
267 /// ```
268 pub async fn get_player_stats(
269 &self,
270 player_id: &str,
271 game_id: &str,
272 ) -> Result<PlayerStats, Error> {
273 let url = format!(
274 "{}/data/v4/players/{}/stats/{}",
275 self.base_url, player_id, game_id
276 );
277 let request = self.reqwest_client.get(&url);
278 let request = self.add_api_key_header(request);
279
280 let response = request.send().await?;
281 self.handle_response(response).await
282 }
283
284 /// Get player match history
285 ///
286 /// # Arguments
287 /// * `player_id` - The FACEIT player ID
288 /// * `game` - The game ID (required)
289 /// * `from` - Optional start timestamp (Unix time)
290 /// * `to` - Optional end timestamp (Unix time)
291 /// * `offset` - Optional offset for pagination (default: 0)
292 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
293 ///
294 /// # Examples
295 ///
296 /// ```no_run
297 /// # use faceit::HttpClient;
298 /// # async fn example() -> Result<(), faceit::error::Error> {
299 /// let client = HttpClient::new();
300 /// let history = client.get_player_history("player-id", "cs2", None, None, Some(0), Some(20)).await?;
301 /// # Ok(())
302 /// # }
303 /// ```
304 pub async fn get_player_history(
305 &self,
306 player_id: &str,
307 game: &str,
308 from: Option<i64>,
309 to: Option<i64>,
310 offset: Option<i64>,
311 limit: Option<i64>,
312 ) -> Result<MatchHistoryList, Error> {
313 let url = format!("{}/data/v4/players/{}/history", self.base_url, player_id);
314 let mut request = self.reqwest_client.get(&url);
315
316 request = request.query(&[("game", game)]);
317 if let Some(from) = from {
318 request = request.query(&[("from", &from.to_string())]);
319 }
320 if let Some(to) = to {
321 request = request.query(&[("to", &to.to_string())]);
322 }
323 if let Some(offset) = offset {
324 request = request.query(&[("offset", &offset.to_string())]);
325 }
326 if let Some(limit) = limit {
327 request = request.query(&[("limit", &limit.to_string())]);
328 }
329
330 let request = self.add_api_key_header(request);
331 let response = request.send().await?;
332 self.handle_response(response).await
333 }
334
335 /// Get player bans
336 ///
337 /// # Arguments
338 /// * `player_id` - The FACEIT player ID
339 /// * `offset` - Optional offset for pagination (default: 0)
340 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
341 ///
342 /// # Examples
343 ///
344 /// ```no_run
345 /// # use faceit::HttpClient;
346 /// # async fn example() -> Result<(), faceit::error::Error> {
347 /// let client = HttpClient::new();
348 /// let bans = client.get_player_bans("player-id", Some(0), Some(20)).await?;
349 /// # Ok(())
350 /// # }
351 /// ```
352 pub async fn get_player_bans(
353 &self,
354 player_id: &str,
355 offset: Option<i64>,
356 limit: Option<i64>,
357 ) -> Result<PlayerBansList, Error> {
358 let url = format!("{}/data/v4/players/{}/bans", self.base_url, player_id);
359 let mut request = self.reqwest_client.get(&url);
360
361 if let Some(offset) = offset {
362 request = request.query(&[("offset", &offset.to_string())]);
363 }
364 if let Some(limit) = limit {
365 request = request.query(&[("limit", &limit.to_string())]);
366 }
367
368 let request = self.add_api_key_header(request);
369 let response = request.send().await?;
370 self.handle_response(response).await
371 }
372
373 /// Get player hubs
374 ///
375 /// # Arguments
376 /// * `player_id` - The FACEIT player ID
377 /// * `offset` - Optional offset for pagination (default: 0, max: 1000)
378 /// * `limit` - Optional limit for pagination (default: 50, max: 50)
379 ///
380 /// # Examples
381 ///
382 /// ```no_run
383 /// # use faceit::HttpClient;
384 /// # async fn example() -> Result<(), faceit::error::Error> {
385 /// let client = HttpClient::new();
386 /// let hubs = client.get_player_hubs("player-id", Some(0), Some(50)).await?;
387 /// # Ok(())
388 /// # }
389 /// ```
390 pub async fn get_player_hubs(
391 &self,
392 player_id: &str,
393 offset: Option<i64>,
394 limit: Option<i64>,
395 ) -> Result<HubsList, Error> {
396 let url = format!("{}/data/v4/players/{}/hubs", self.base_url, player_id);
397 let mut request = self.reqwest_client.get(&url);
398
399 if let Some(offset) = offset {
400 request = request.query(&[("offset", &offset.to_string())]);
401 }
402 if let Some(limit) = limit {
403 request = request.query(&[("limit", &limit.to_string())]);
404 }
405
406 let request = self.add_api_key_header(request);
407 let response = request.send().await?;
408 self.handle_response(response).await
409 }
410
411 /// Get player teams
412 ///
413 /// # Arguments
414 /// * `player_id` - The FACEIT player ID
415 /// * `offset` - Optional offset for pagination (default: 0)
416 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
417 ///
418 /// # Examples
419 ///
420 /// ```no_run
421 /// # use faceit::HttpClient;
422 /// # async fn example() -> Result<(), faceit::error::Error> {
423 /// let client = HttpClient::new();
424 /// let teams = client.get_player_teams("player-id", Some(0), Some(20)).await?;
425 /// # Ok(())
426 /// # }
427 /// ```
428 pub async fn get_player_teams(
429 &self,
430 player_id: &str,
431 offset: Option<i64>,
432 limit: Option<i64>,
433 ) -> Result<TeamList, Error> {
434 let url = format!("{}/data/v4/players/{}/teams", self.base_url, player_id);
435 let mut request = self.reqwest_client.get(&url);
436
437 if let Some(offset) = offset {
438 request = request.query(&[("offset", &offset.to_string())]);
439 }
440 if let Some(limit) = limit {
441 request = request.query(&[("limit", &limit.to_string())]);
442 }
443
444 let request = self.add_api_key_header(request);
445 let response = request.send().await?;
446 self.handle_response(response).await
447 }
448
449 /// Get player tournaments
450 ///
451 /// # Arguments
452 /// * `player_id` - The FACEIT player ID
453 /// * `offset` - Optional offset for pagination (default: 0)
454 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
455 ///
456 /// # Examples
457 ///
458 /// ```no_run
459 /// # use faceit::HttpClient;
460 /// # async fn example() -> Result<(), faceit::error::Error> {
461 /// let client = HttpClient::new();
462 /// let tournaments = client.get_player_tournaments("player-id", Some(0), Some(20)).await?;
463 /// # Ok(())
464 /// # }
465 /// ```
466 pub async fn get_player_tournaments(
467 &self,
468 player_id: &str,
469 offset: Option<i64>,
470 limit: Option<i64>,
471 ) -> Result<TournamentsList, Error> {
472 let url = format!(
473 "{}/data/v4/players/{}/tournaments",
474 self.base_url, player_id
475 );
476 let mut request = self.reqwest_client.get(&url);
477
478 if let Some(offset) = offset {
479 request = request.query(&[("offset", &offset.to_string())]);
480 }
481 if let Some(limit) = limit {
482 request = request.query(&[("limit", &limit.to_string())]);
483 }
484
485 let request = self.add_api_key_header(request);
486 let response = request.send().await?;
487 self.handle_response(response).await
488 }
489
490 // ============================================================================
491 // Match Methods
492 // ============================================================================
493
494 /// Get match details
495 ///
496 /// # Arguments
497 /// * `match_id` - The FACEIT match ID
498 ///
499 /// # Examples
500 ///
501 /// ```no_run
502 /// # use faceit::HttpClient;
503 /// # async fn example() -> Result<(), faceit::error::Error> {
504 /// let client = HttpClient::new();
505 /// let match_details = client.get_match("match-id-here").await?;
506 /// # Ok(())
507 /// # }
508 /// ```
509 pub async fn get_match(&self, match_id: &str) -> Result<Match, Error> {
510 let url = format!("{}/data/v4/matches/{}", self.base_url, match_id);
511 let request = self.reqwest_client.get(&url);
512 let request = self.add_api_key_header(request);
513
514 let response = request.send().await?;
515 self.handle_response(response).await
516 }
517
518 /// Get match statistics
519 ///
520 /// # Arguments
521 /// * `match_id` - The FACEIT match ID
522 ///
523 /// # Examples
524 ///
525 /// ```no_run
526 /// # use faceit::HttpClient;
527 /// # async fn example() -> Result<(), faceit::error::Error> {
528 /// let client = HttpClient::new();
529 /// let stats = client.get_match_stats("match-id-here").await?;
530 /// # Ok(())
531 /// # }
532 /// ```
533 pub async fn get_match_stats(&self, match_id: &str) -> Result<MatchStats, Error> {
534 let url = format!("{}/data/v4/matches/{}/stats", self.base_url, match_id);
535 let request = self.reqwest_client.get(&url);
536 let request = self.add_api_key_header(request);
537
538 let response = request.send().await?;
539 self.handle_response(response).await
540 }
541
542 // ============================================================================
543 // Game Methods
544 // ============================================================================
545
546 /// Get all games
547 ///
548 /// # Arguments
549 /// * `offset` - Optional offset for pagination (default: 0)
550 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
551 ///
552 /// # Examples
553 ///
554 /// ```no_run
555 /// # use faceit::HttpClient;
556 /// # async fn example() -> Result<(), faceit::error::Error> {
557 /// let client = HttpClient::new();
558 /// let games = client.get_all_games(Some(0), Some(20)).await?;
559 /// # Ok(())
560 /// # }
561 /// ```
562 pub async fn get_all_games(
563 &self,
564 offset: Option<i64>,
565 limit: Option<i64>,
566 ) -> Result<GamesList, Error> {
567 let url = format!("{}/data/v4/games", self.base_url);
568 let mut request = self.reqwest_client.get(&url);
569
570 if let Some(offset) = offset {
571 request = request.query(&[("offset", &offset.to_string())]);
572 }
573 if let Some(limit) = limit {
574 request = request.query(&[("limit", &limit.to_string())]);
575 }
576
577 let request = self.add_api_key_header(request);
578 let response = request.send().await?;
579 self.handle_response(response).await
580 }
581
582 /// Get game details
583 ///
584 /// # Arguments
585 /// * `game_id` - The game ID (e.g., "cs2", "csgo")
586 ///
587 /// # Examples
588 ///
589 /// ```no_run
590 /// # use faceit::HttpClient;
591 /// # async fn example() -> Result<(), faceit::error::Error> {
592 /// let client = HttpClient::new();
593 /// let game = client.get_game("cs2").await?;
594 /// # Ok(())
595 /// # }
596 /// ```
597 pub async fn get_game(&self, game_id: &str) -> Result<Game, Error> {
598 let url = format!("{}/data/v4/games/{}", self.base_url, game_id);
599 let request = self.reqwest_client.get(&url);
600 let request = self.add_api_key_header(request);
601
602 let response = request.send().await?;
603 self.handle_response(response).await
604 }
605
606 /// Get parent game details (for region-specific games)
607 ///
608 /// # Arguments
609 /// * `game_id` - The game ID
610 ///
611 /// # Examples
612 ///
613 /// ```no_run
614 /// # use faceit::HttpClient;
615 /// # async fn example() -> Result<(), faceit::error::Error> {
616 /// let client = HttpClient::new();
617 /// let parent_game = client.get_parent_game("game-id").await?;
618 /// # Ok(())
619 /// # }
620 /// ```
621 pub async fn get_parent_game(&self, game_id: &str) -> Result<Game, Error> {
622 let url = format!("{}/data/v4/games/{}/parent", self.base_url, game_id);
623 let request = self.reqwest_client.get(&url);
624 let request = self.add_api_key_header(request);
625
626 let response = request.send().await?;
627 self.handle_response(response).await
628 }
629
630 /// Get game matchmakings
631 ///
632 /// # Arguments
633 /// * `game_id` - The game ID
634 /// * `region` - Optional region filter
635 /// * `offset` - Optional offset for pagination (default: 0)
636 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
637 ///
638 /// # Examples
639 ///
640 /// ```no_run
641 /// # use faceit::HttpClient;
642 /// # async fn example() -> Result<(), faceit::error::Error> {
643 /// let client = HttpClient::new();
644 /// let matchmakings = client.get_game_matchmakings("cs2", Some("EU"), Some(0), Some(20)).await?;
645 /// # Ok(())
646 /// # }
647 /// ```
648 pub async fn get_game_matchmakings(
649 &self,
650 game_id: &str,
651 region: Option<&str>,
652 offset: Option<i64>,
653 limit: Option<i64>,
654 ) -> Result<MatchmakingList, Error> {
655 let url = format!("{}/data/v4/games/{}/matchmakings", self.base_url, game_id);
656 let mut request = self.reqwest_client.get(&url);
657
658 if let Some(region) = region {
659 request = request.query(&[("region", region)]);
660 }
661 if let Some(offset) = offset {
662 request = request.query(&[("offset", &offset.to_string())]);
663 }
664 if let Some(limit) = limit {
665 request = request.query(&[("limit", &limit.to_string())]);
666 }
667
668 let request = self.add_api_key_header(request);
669 let response = request.send().await?;
670 self.handle_response(response).await
671 }
672
673 // ============================================================================
674 // Hub Methods
675 // ============================================================================
676
677 /// Get hub details
678 ///
679 /// # Arguments
680 /// * `hub_id` - The hub ID
681 /// * `expanded` - Optional list of entities to expand (e.g., ["organizer", "game"])
682 ///
683 /// # Examples
684 ///
685 /// ```no_run
686 /// # use faceit::HttpClient;
687 /// # async fn example() -> Result<(), faceit::error::Error> {
688 /// let client = HttpClient::new();
689 /// let hub = client.get_hub("hub-id", None).await?;
690 /// # Ok(())
691 /// # }
692 /// ```
693 pub async fn get_hub(&self, hub_id: &str, expanded: Option<&[&str]>) -> Result<Hub, Error> {
694 let url = format!("{}/data/v4/hubs/{}", self.base_url, hub_id);
695 let mut request = self.reqwest_client.get(&url);
696
697 if let Some(expanded) = expanded {
698 let expanded_str = expanded.join(",");
699 request = request.query(&[("expanded", expanded_str.as_str())]);
700 }
701
702 let request = self.add_api_key_header(request);
703 let response = request.send().await?;
704 self.handle_response(response).await
705 }
706
707 /// Get hub matches
708 ///
709 /// # Arguments
710 /// * `hub_id` - The hub ID
711 /// * `match_type` - Optional match type filter ("all", "upcoming", "ongoing", "past")
712 /// * `offset` - Optional offset for pagination (default: 0)
713 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
714 ///
715 /// # Examples
716 ///
717 /// ```no_run
718 /// # use faceit::HttpClient;
719 /// # async fn example() -> Result<(), faceit::error::Error> {
720 /// let client = HttpClient::new();
721 /// let matches = client.get_hub_matches("hub-id", Some("all"), Some(0), Some(20)).await?;
722 /// # Ok(())
723 /// # }
724 /// ```
725 pub async fn get_hub_matches(
726 &self,
727 hub_id: &str,
728 match_type: Option<&str>,
729 offset: Option<i64>,
730 limit: Option<i64>,
731 ) -> Result<MatchesList, Error> {
732 let url = format!("{}/data/v4/hubs/{}/matches", self.base_url, hub_id);
733 let mut request = self.reqwest_client.get(&url);
734
735 if let Some(match_type) = match_type {
736 request = request.query(&[("type", match_type)]);
737 }
738 if let Some(offset) = offset {
739 request = request.query(&[("offset", &offset.to_string())]);
740 }
741 if let Some(limit) = limit {
742 request = request.query(&[("limit", &limit.to_string())]);
743 }
744
745 let request = self.add_api_key_header(request);
746 let response = request.send().await?;
747 self.handle_response(response).await
748 }
749
750 /// Get hub members
751 ///
752 /// # Arguments
753 /// * `hub_id` - The hub ID
754 /// * `offset` - Optional offset for pagination (default: 0, max: 1000)
755 /// * `limit` - Optional limit for pagination (default: 50, max: 50)
756 ///
757 /// # Examples
758 ///
759 /// ```no_run
760 /// # use faceit::HttpClient;
761 /// # async fn example() -> Result<(), faceit::error::Error> {
762 /// let client = HttpClient::new();
763 /// let members = client.get_hub_members("hub-id", Some(0), Some(50)).await?;
764 /// # Ok(())
765 /// # }
766 /// ```
767 pub async fn get_hub_members(
768 &self,
769 hub_id: &str,
770 offset: Option<i64>,
771 limit: Option<i64>,
772 ) -> Result<HubMembers, Error> {
773 let url = format!("{}/data/v4/hubs/{}/members", self.base_url, hub_id);
774 let mut request = self.reqwest_client.get(&url);
775
776 if let Some(offset) = offset {
777 request = request.query(&[("offset", &offset.to_string())]);
778 }
779 if let Some(limit) = limit {
780 request = request.query(&[("limit", &limit.to_string())]);
781 }
782
783 let request = self.add_api_key_header(request);
784 let response = request.send().await?;
785 self.handle_response(response).await
786 }
787
788 /// Get hub statistics
789 ///
790 /// # Arguments
791 /// * `hub_id` - The hub ID
792 /// * `offset` - Optional offset for pagination (default: 0)
793 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
794 ///
795 /// # Examples
796 ///
797 /// ```no_run
798 /// # use faceit::HttpClient;
799 /// # async fn example() -> Result<(), faceit::error::Error> {
800 /// let client = HttpClient::new();
801 /// let stats = client.get_hub_stats("hub-id", Some(0), Some(20)).await?;
802 /// # Ok(())
803 /// # }
804 /// ```
805 pub async fn get_hub_stats(
806 &self,
807 hub_id: &str,
808 offset: Option<i64>,
809 limit: Option<i64>,
810 ) -> Result<HubStats, Error> {
811 let url = format!("{}/data/v4/hubs/{}/stats", self.base_url, hub_id);
812 let mut request = self.reqwest_client.get(&url);
813
814 if let Some(offset) = offset {
815 request = request.query(&[("offset", &offset.to_string())]);
816 }
817 if let Some(limit) = limit {
818 request = request.query(&[("limit", &limit.to_string())]);
819 }
820
821 let request = self.add_api_key_header(request);
822 let response = request.send().await?;
823 self.handle_response(response).await
824 }
825
826 // ============================================================================
827 // Championship Methods
828 // ============================================================================
829
830 /// Get championships for a game
831 ///
832 /// # Arguments
833 /// * `game` - The game ID (required)
834 /// * `championship_type` - Optional type filter ("all", "upcoming", "ongoing", "past")
835 /// * `offset` - Optional offset for pagination (default: 0)
836 /// * `limit` - Optional limit for pagination (default: 10, max: 10)
837 ///
838 /// # Examples
839 ///
840 /// ```no_run
841 /// # use faceit::HttpClient;
842 /// # async fn example() -> Result<(), faceit::error::Error> {
843 /// let client = HttpClient::new();
844 /// let championships = client.get_championships("cs2", Some("all"), Some(0), Some(10)).await?;
845 /// # Ok(())
846 /// # }
847 /// ```
848 pub async fn get_championships(
849 &self,
850 game: &str,
851 championship_type: Option<&str>,
852 offset: Option<i64>,
853 limit: Option<i64>,
854 ) -> Result<ChampionshipsList, Error> {
855 let url = format!("{}/data/v4/championships", self.base_url);
856 let mut request = self.reqwest_client.get(&url);
857
858 request = request.query(&[("game", game)]);
859 if let Some(championship_type) = championship_type {
860 request = request.query(&[("type", championship_type)]);
861 }
862 if let Some(offset) = offset {
863 request = request.query(&[("offset", &offset.to_string())]);
864 }
865 if let Some(limit) = limit {
866 request = request.query(&[("limit", &limit.to_string())]);
867 }
868
869 let request = self.add_api_key_header(request);
870 let response = request.send().await?;
871 self.handle_response(response).await
872 }
873
874 /// Get championship details
875 ///
876 /// # Arguments
877 /// * `championship_id` - The championship ID
878 /// * `expanded` - Optional list of entities to expand (e.g., ["organizer", "game"])
879 ///
880 /// # Examples
881 ///
882 /// ```no_run
883 /// # use faceit::HttpClient;
884 /// # async fn example() -> Result<(), faceit::error::Error> {
885 /// let client = HttpClient::new();
886 /// let championship = client.get_championship("championship-id", None).await?;
887 /// # Ok(())
888 /// # }
889 /// ```
890 pub async fn get_championship(
891 &self,
892 championship_id: &str,
893 expanded: Option<&[&str]>,
894 ) -> Result<Championship, Error> {
895 let url = format!(
896 "{}/data/v4/championships/{}",
897 self.base_url, championship_id
898 );
899 let mut request = self.reqwest_client.get(&url);
900
901 if let Some(expanded) = expanded {
902 let expanded_str = expanded.join(",");
903 request = request.query(&[("expanded", expanded_str.as_str())]);
904 }
905
906 let request = self.add_api_key_header(request);
907 let response = request.send().await?;
908 self.handle_response(response).await
909 }
910
911 /// Get championship matches
912 ///
913 /// # Arguments
914 /// * `championship_id` - The championship ID
915 /// * `match_type` - Optional match type filter ("all", "upcoming", "ongoing", "past")
916 /// * `offset` - Optional offset for pagination (default: 0)
917 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
918 ///
919 /// # Examples
920 ///
921 /// ```no_run
922 /// # use faceit::HttpClient;
923 /// # async fn example() -> Result<(), faceit::error::Error> {
924 /// let client = HttpClient::new();
925 /// let matches = client.get_championship_matches("championship-id", Some("all"), Some(0), Some(20)).await?;
926 /// # Ok(())
927 /// # }
928 /// ```
929 pub async fn get_championship_matches(
930 &self,
931 championship_id: &str,
932 match_type: Option<&str>,
933 offset: Option<i64>,
934 limit: Option<i64>,
935 ) -> Result<MatchesList, Error> {
936 let url = format!(
937 "{}/data/v4/championships/{}/matches",
938 self.base_url, championship_id
939 );
940 let mut request = self.reqwest_client.get(&url);
941
942 if let Some(match_type) = match_type {
943 request = request.query(&[("type", match_type)]);
944 }
945 if let Some(offset) = offset {
946 request = request.query(&[("offset", &offset.to_string())]);
947 }
948 if let Some(limit) = limit {
949 request = request.query(&[("limit", &limit.to_string())]);
950 }
951
952 let request = self.add_api_key_header(request);
953 let response = request.send().await?;
954 self.handle_response(response).await
955 }
956
957 // ============================================================================
958 // Search Methods
959 // ============================================================================
960
961 /// Search for players
962 ///
963 /// # Arguments
964 /// * `nickname` - Player nickname to search for (required)
965 /// * `game` - Optional game ID filter
966 /// * `country` - Optional country code filter (ISO 3166-1)
967 /// * `offset` - Optional offset for pagination (default: 0)
968 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
969 ///
970 /// # Examples
971 ///
972 /// ```no_run
973 /// # use faceit::HttpClient;
974 /// # async fn example() -> Result<(), faceit::error::Error> {
975 /// let client = HttpClient::new();
976 /// let results = client.search_players("player_name", Some("cs2"), None, Some(0), Some(20)).await?;
977 /// # Ok(())
978 /// # }
979 /// ```
980 pub async fn search_players(
981 &self,
982 nickname: &str,
983 game: Option<&str>,
984 country: Option<&str>,
985 offset: Option<i64>,
986 limit: Option<i64>,
987 ) -> Result<UsersSearchList, Error> {
988 let url = format!("{}/data/v4/search/players", self.base_url);
989 let mut request = self.reqwest_client.get(&url);
990
991 request = request.query(&[("nickname", nickname)]);
992 if let Some(game) = game {
993 request = request.query(&[("game", game)]);
994 }
995 if let Some(country) = country {
996 request = request.query(&[("country", country)]);
997 }
998 if let Some(offset) = offset {
999 request = request.query(&[("offset", &offset.to_string())]);
1000 }
1001 if let Some(limit) = limit {
1002 request = request.query(&[("limit", &limit.to_string())]);
1003 }
1004
1005 let request = self.add_api_key_header(request);
1006 let response = request.send().await?;
1007 self.handle_response(response).await
1008 }
1009
1010 /// Search for teams
1011 ///
1012 /// # Arguments
1013 /// * `nickname` - Team nickname to search for (required)
1014 /// * `game` - Optional game ID filter
1015 /// * `offset` - Optional offset for pagination (default: 0)
1016 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
1017 ///
1018 /// # Examples
1019 ///
1020 /// ```no_run
1021 /// # use faceit::HttpClient;
1022 /// # async fn example() -> Result<(), faceit::error::Error> {
1023 /// let client = HttpClient::new();
1024 /// let results = client.search_teams("team_name", Some("cs2"), Some(0), Some(20)).await?;
1025 /// # Ok(())
1026 /// # }
1027 /// ```
1028 pub async fn search_teams(
1029 &self,
1030 nickname: &str,
1031 game: Option<&str>,
1032 offset: Option<i64>,
1033 limit: Option<i64>,
1034 ) -> Result<TeamsSearchList, Error> {
1035 let url = format!("{}/data/v4/search/teams", self.base_url);
1036 let mut request = self.reqwest_client.get(&url);
1037
1038 request = request.query(&[("nickname", nickname)]);
1039 if let Some(game) = game {
1040 request = request.query(&[("game", game)]);
1041 }
1042 if let Some(offset) = offset {
1043 request = request.query(&[("offset", &offset.to_string())]);
1044 }
1045 if let Some(limit) = limit {
1046 request = request.query(&[("limit", &limit.to_string())]);
1047 }
1048
1049 let request = self.add_api_key_header(request);
1050 let response = request.send().await?;
1051 self.handle_response(response).await
1052 }
1053
1054 /// Search for hubs
1055 ///
1056 /// # Arguments
1057 /// * `name` - Hub name to search for (required)
1058 /// * `game` - Optional game ID filter
1059 /// * `region` - Optional region filter
1060 /// * `offset` - Optional offset for pagination (default: 0)
1061 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
1062 ///
1063 /// # Examples
1064 ///
1065 /// ```no_run
1066 /// # use faceit::HttpClient;
1067 /// # async fn example() -> Result<(), faceit::error::Error> {
1068 /// let client = HttpClient::new();
1069 /// let results = client.search_hubs("hub_name", Some("cs2"), Some("EU"), Some(0), Some(20)).await?;
1070 /// # Ok(())
1071 /// # }
1072 /// ```
1073 pub async fn search_hubs(
1074 &self,
1075 name: &str,
1076 game: Option<&str>,
1077 region: Option<&str>,
1078 offset: Option<i64>,
1079 limit: Option<i64>,
1080 ) -> Result<CompetitionsSearchList, Error> {
1081 let url = format!("{}/data/v4/search/hubs", self.base_url);
1082 let mut request = self.reqwest_client.get(&url);
1083
1084 request = request.query(&[("name", name)]);
1085 if let Some(game) = game {
1086 request = request.query(&[("game", game)]);
1087 }
1088 if let Some(region) = region {
1089 request = request.query(&[("region", region)]);
1090 }
1091 if let Some(offset) = offset {
1092 request = request.query(&[("offset", &offset.to_string())]);
1093 }
1094 if let Some(limit) = limit {
1095 request = request.query(&[("limit", &limit.to_string())]);
1096 }
1097
1098 let request = self.add_api_key_header(request);
1099 let response = request.send().await?;
1100 self.handle_response(response).await
1101 }
1102
1103 // ============================================================================
1104 // Ranking Methods
1105 // ============================================================================
1106
1107 /// Get global ranking for a game and region
1108 ///
1109 /// # Arguments
1110 /// * `game_id` - The game ID
1111 /// * `region` - The region (required)
1112 /// * `country` - Optional country code filter (ISO 3166-1)
1113 /// * `offset` - Optional offset for pagination (default: 0)
1114 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
1115 ///
1116 /// # Examples
1117 ///
1118 /// ```no_run
1119 /// # use faceit::HttpClient;
1120 /// # async fn example() -> Result<(), faceit::error::Error> {
1121 /// let client = HttpClient::new();
1122 /// let ranking = client.get_global_ranking("cs2", "EU", None, Some(0), Some(20)).await?;
1123 /// # Ok(())
1124 /// # }
1125 /// ```
1126 pub async fn get_global_ranking(
1127 &self,
1128 game_id: &str,
1129 region: &str,
1130 country: Option<&str>,
1131 offset: Option<i64>,
1132 limit: Option<i64>,
1133 ) -> Result<GlobalRankingList, Error> {
1134 let url = format!(
1135 "{}/data/v4/rankings/games/{}/regions/{}",
1136 self.base_url, game_id, region
1137 );
1138 let mut request = self.reqwest_client.get(&url);
1139
1140 if let Some(country) = country {
1141 request = request.query(&[("country", country)]);
1142 }
1143 if let Some(offset) = offset {
1144 request = request.query(&[("offset", &offset.to_string())]);
1145 }
1146 if let Some(limit) = limit {
1147 request = request.query(&[("limit", &limit.to_string())]);
1148 }
1149
1150 let request = self.add_api_key_header(request);
1151 let response = request.send().await?;
1152 self.handle_response(response).await
1153 }
1154
1155 /// Get player ranking in global ranking
1156 ///
1157 /// # Arguments
1158 /// * `game_id` - The game ID
1159 /// * `region` - The region (required)
1160 /// * `player_id` - The player ID (required)
1161 /// * `country` - Optional country code filter (ISO 3166-1)
1162 /// * `limit` - Optional limit for pagination (default: 20, max: 100)
1163 ///
1164 /// # Examples
1165 ///
1166 /// ```no_run
1167 /// # use faceit::HttpClient;
1168 /// # async fn example() -> Result<(), faceit::error::Error> {
1169 /// let client = HttpClient::new();
1170 /// let ranking = client.get_player_ranking("cs2", "EU", "player-id", None, Some(20)).await?;
1171 /// # Ok(())
1172 /// # }
1173 /// ```
1174 pub async fn get_player_ranking(
1175 &self,
1176 game_id: &str,
1177 region: &str,
1178 player_id: &str,
1179 country: Option<&str>,
1180 limit: Option<i64>,
1181 ) -> Result<PlayerGlobalRanking, Error> {
1182 let url = format!(
1183 "{}/data/v4/rankings/games/{}/regions/{}/players/{}",
1184 self.base_url, game_id, region, player_id
1185 );
1186 let mut request = self.reqwest_client.get(&url);
1187
1188 if let Some(country) = country {
1189 request = request.query(&[("country", country)]);
1190 }
1191 if let Some(limit) = limit {
1192 request = request.query(&[("limit", &limit.to_string())]);
1193 }
1194
1195 let request = self.add_api_key_header(request);
1196 let response = request.send().await?;
1197 self.handle_response(response).await
1198 }
1199
1200 // ============================================================================
1201 // Helper Methods
1202 // ============================================================================
1203
1204 fn add_api_key_header(&self, request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
1205 if let Some(ref api_key) = self.api_key {
1206 request.header("Authorization", format!("Bearer {}", api_key.as_str()))
1207 } else {
1208 request
1209 }
1210 }
1211
1212 async fn handle_response<T>(&self, response: reqwest::Response) -> Result<T, Error>
1213 where
1214 T: serde::de::DeserializeOwned,
1215 {
1216 let status = response.status();
1217 let response_text = response.text().await?;
1218
1219 if !status.is_success() {
1220 let status_code = status.as_u16();
1221 return match status_code {
1222 400 => Err(Error::Api(
1223 status_code,
1224 format!("Bad request: {}", response_text),
1225 )),
1226 401 => Err(Error::InvalidApiKey),
1227 403 => Err(Error::Api(
1228 status_code,
1229 format!("Forbidden: {}", response_text),
1230 )),
1231 404 => Err(Error::Api(
1232 status_code,
1233 format!("Not found: {}", response_text),
1234 )),
1235 429 => Err(Error::Api(
1236 status_code,
1237 format!("Too many requests: {}", response_text),
1238 )),
1239 500 => Err(Error::ServerError),
1240 503 => Err(Error::Api(
1241 status_code,
1242 format!("Service temporarily unavailable: {}", response_text),
1243 )),
1244 _ => Err(Error::Api(status_code, response_text)),
1245 };
1246 }
1247
1248 // Try to parse JSON, but provide better error message if it fails
1249 match serde_json::from_str::<T>(&response_text) {
1250 Ok(json) => Ok(json),
1251 Err(e) => {
1252 // If JSON parsing fails, create a more descriptive error
1253 // We'll wrap it in an Api error with the response text
1254 Err(Error::Api(
1255 status.as_u16(),
1256 format!(
1257 "Failed to parse JSON response: {}. Response body: {}",
1258 e, response_text
1259 ),
1260 ))
1261 }
1262 }
1263 }
1264}
1265
1266impl Default for Client {
1267 fn default() -> Self {
1268 Self::new()
1269 }
1270}
1271
1272#[cfg(test)]
1273mod tests {
1274 use super::*;
1275
1276 #[test]
1277 fn test_client_builder() {
1278 let builder = ClientBuilder::new();
1279 assert!(builder.base_url.is_none());
1280 assert!(builder.api_key.is_none());
1281 assert!(builder.timeout.is_some());
1282 }
1283
1284 #[test]
1285 fn test_client_builder_with_options() {
1286 let client = ClientBuilder::new()
1287 .base_url("https://test.example.com")
1288 .api_key("test-key")
1289 .timeout(Duration::from_secs(60))
1290 .build()
1291 .unwrap();
1292
1293 assert_eq!(client.base_url, "https://test.example.com");
1294 assert_eq!(client.api_key, Some("test-key".to_string()));
1295 }
1296
1297 #[test]
1298 fn test_client_default_base_url() {
1299 let client = Client::new();
1300 assert_eq!(client.base_url, "https://open.faceit.com");
1301 }
1302
1303 #[test]
1304 fn test_player_id_string() {
1305 // FACEIT uses simple string player IDs (UUID format)
1306 let player_id = "5ea07280-2399-4c7e-88ab-f2f7db0c449f";
1307 assert!(!player_id.is_empty());
1308 // Just verify it's a valid string
1309 assert_eq!(player_id.len(), 36);
1310 }
1311}