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}