Skip to main content

the_odds_api/
client.rs

1//! The Odds API client implementation.
2
3use crate::error::{Error, Result};
4use crate::models::*;
5use crate::types::*;
6use chrono::{DateTime, Utc};
7use reqwest::Client;
8use url::Url;
9
10const DEFAULT_BASE_URL: &str = "https://api.the-odds-api.com";
11const IPV6_BASE_URL: &str = "https://ipv6-api.the-odds-api.com";
12
13/// Builder for configuring the API client.
14#[derive(Debug, Clone)]
15pub struct TheOddsApiClientBuilder {
16    api_key: String,
17    base_url: String,
18    client: Option<Client>,
19}
20
21impl TheOddsApiClientBuilder {
22    /// Create a new builder with the given API key.
23    pub fn new(api_key: impl Into<String>) -> Self {
24        Self {
25            api_key: api_key.into(),
26            base_url: DEFAULT_BASE_URL.to_string(),
27            client: None,
28        }
29    }
30
31    /// Use a custom base URL.
32    pub fn base_url(mut self, url: impl Into<String>) -> Self {
33        self.base_url = url.into();
34        self
35    }
36
37    /// Use the IPv6 endpoint.
38    pub fn use_ipv6(mut self) -> Self {
39        self.base_url = IPV6_BASE_URL.to_string();
40        self
41    }
42
43    /// Use a custom reqwest client.
44    pub fn client(mut self, client: Client) -> Self {
45        self.client = Some(client);
46        self
47    }
48
49    /// Build the API client.
50    pub fn build(self) -> TheOddsApiClient {
51        TheOddsApiClient {
52            api_key: self.api_key,
53            base_url: self.base_url,
54            client: self.client.unwrap_or_default(),
55        }
56    }
57}
58
59/// The main client for interacting with The Odds API.
60#[derive(Debug, Clone)]
61pub struct TheOddsApiClient {
62    api_key: String,
63    base_url: String,
64    client: Client,
65}
66
67impl TheOddsApiClient {
68    /// Create a new client with the given API key.
69    pub fn new(api_key: impl Into<String>) -> Self {
70        TheOddsApiClientBuilder::new(api_key).build()
71    }
72
73    /// Create a builder for more advanced configuration.
74    pub fn builder(api_key: impl Into<String>) -> TheOddsApiClientBuilder {
75        TheOddsApiClientBuilder::new(api_key)
76    }
77
78    /// Build a URL with the given path and query parameters.
79    fn build_url(&self, path: &str, params: &[(&str, String)]) -> Result<Url> {
80        let mut url = Url::parse(&format!("{}{}", self.base_url, path))?;
81        {
82            let mut query = url.query_pairs_mut();
83            query.append_pair("apiKey", &self.api_key);
84            for (key, value) in params {
85                if !value.is_empty() {
86                    query.append_pair(key, value);
87                }
88            }
89        }
90        Ok(url)
91    }
92
93    /// Execute a GET request and parse the response.
94    async fn get<T: serde::de::DeserializeOwned>(&self, url: Url) -> Result<Response<T>> {
95        let response = self.client.get(url).send().await?;
96        let usage = UsageInfo::from_headers(response.headers());
97        let status = response.status();
98
99        if status.is_success() {
100            let data = response.json().await?;
101            Ok(Response::new(data, usage))
102        } else if status == reqwest::StatusCode::UNAUTHORIZED {
103            Err(Error::Unauthorized)
104        } else if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
105            Err(Error::RateLimited {
106                requests_remaining: usage.requests_remaining,
107            })
108        } else {
109            let message = response
110                .text()
111                .await
112                .unwrap_or_else(|_| "Unknown error".to_string());
113            Err(Error::Api {
114                status: status.as_u16(),
115                message,
116            })
117        }
118    }
119
120    // =========================================================================
121    // Sports Endpoint
122    // =========================================================================
123
124    /// Get all in-season sports.
125    ///
126    /// This endpoint does not count against your usage quota.
127    ///
128    /// # Example
129    ///
130    /// ```no_run
131    /// # async fn example() -> the_odds_api::Result<()> {
132    /// let client = the_odds_api::TheOddsApiClient::new("your-api-key");
133    /// let sports = client.get_sports().await?;
134    /// for sport in sports.data {
135    ///     println!("{}: {}", sport.key, sport.title);
136    /// }
137    /// # Ok(())
138    /// # }
139    /// ```
140    pub async fn get_sports(&self) -> Result<Response<Vec<Sport>>> {
141        let url = self.build_url("/v4/sports", &[])?;
142        self.get(url).await
143    }
144
145    /// Get all sports, including out-of-season ones.
146    ///
147    /// This endpoint does not count against your usage quota.
148    pub async fn get_all_sports(&self) -> Result<Response<Vec<Sport>>> {
149        let url = self.build_url("/v4/sports", &[("all", "true".to_string())])?;
150        self.get(url).await
151    }
152
153    // =========================================================================
154    // Events Endpoint
155    // =========================================================================
156
157    /// Get events for a sport without odds.
158    ///
159    /// This endpoint does not count against your usage quota.
160    pub fn get_events(&self, sport: impl Into<String>) -> GetEventsRequest<'_> {
161        GetEventsRequest::new(self, sport.into())
162    }
163
164    // =========================================================================
165    // Odds Endpoint
166    // =========================================================================
167
168    /// Get odds for a sport.
169    ///
170    /// # Quota Cost
171    /// Cost = number of markets × number of regions
172    ///
173    /// # Example
174    ///
175    /// ```no_run
176    /// # use the_odds_api::{Region, Market};
177    /// # async fn example() -> the_odds_api::Result<()> {
178    /// let client = the_odds_api::TheOddsApiClient::new("your-api-key");
179    /// let odds = client
180    ///     .get_odds("americanfootball_nfl")
181    ///     .regions(&[Region::Us])
182    ///     .markets(&[Market::H2h, Market::Spreads])
183    ///     .send()
184    ///     .await?;
185    /// # Ok(())
186    /// # }
187    /// ```
188    pub fn get_odds(&self, sport: impl Into<String>) -> GetOddsRequest<'_> {
189        GetOddsRequest::new(self, sport.into())
190    }
191
192    /// Get odds for upcoming events across all sports.
193    pub fn get_upcoming_odds(&self) -> GetOddsRequest<'_> {
194        GetOddsRequest::new(self, "upcoming".to_string())
195    }
196
197    // =========================================================================
198    // Scores Endpoint
199    // =========================================================================
200
201    /// Get scores for a sport.
202    ///
203    /// # Quota Cost
204    /// 1 for live/upcoming scores, 2 when using `days_from`.
205    pub fn get_scores(&self, sport: impl Into<String>) -> GetScoresRequest<'_> {
206        GetScoresRequest::new(self, sport.into())
207    }
208
209    // =========================================================================
210    // Event Odds Endpoint
211    // =========================================================================
212
213    /// Get detailed odds for a single event.
214    ///
215    /// This endpoint supports all available markets including player props.
216    pub fn get_event_odds(
217        &self,
218        sport: impl Into<String>,
219        event_id: impl Into<String>,
220    ) -> GetEventOddsRequest<'_> {
221        GetEventOddsRequest::new(self, sport.into(), event_id.into())
222    }
223
224    // =========================================================================
225    // Event Markets Endpoint
226    // =========================================================================
227
228    /// Discover available markets for an event.
229    ///
230    /// # Quota Cost
231    /// 1
232    pub fn get_event_markets(
233        &self,
234        sport: impl Into<String>,
235        event_id: impl Into<String>,
236    ) -> GetEventMarketsRequest<'_> {
237        GetEventMarketsRequest::new(self, sport.into(), event_id.into())
238    }
239
240    // =========================================================================
241    // Participants Endpoint
242    // =========================================================================
243
244    /// Get all participants (teams/players) for a sport.
245    ///
246    /// # Quota Cost
247    /// 1
248    pub async fn get_participants(
249        &self,
250        sport: impl Into<String>,
251    ) -> Result<Response<Vec<Participant>>> {
252        let url = self.build_url(&format!("/v4/sports/{}/participants", sport.into()), &[])?;
253        self.get(url).await
254    }
255
256    // =========================================================================
257    // Historical Endpoints
258    // =========================================================================
259
260    /// Get historical odds for a sport at a specific timestamp.
261    ///
262    /// # Quota Cost
263    /// 10 × number of markets × number of regions
264    pub fn get_historical_odds(&self, sport: impl Into<String>) -> GetHistoricalOddsRequest<'_> {
265        GetHistoricalOddsRequest::new(self, sport.into())
266    }
267
268    /// Get historical events for a sport at a specific timestamp.
269    ///
270    /// # Quota Cost
271    /// 1 (0 if no events found)
272    pub fn get_historical_events(&self, sport: impl Into<String>) -> GetHistoricalEventsRequest<'_> {
273        GetHistoricalEventsRequest::new(self, sport.into())
274    }
275
276    /// Get historical odds for a single event at a specific timestamp.
277    pub fn get_historical_event_odds(
278        &self,
279        sport: impl Into<String>,
280        event_id: impl Into<String>,
281    ) -> GetHistoricalEventOddsRequest<'_> {
282        GetHistoricalEventOddsRequest::new(self, sport.into(), event_id.into())
283    }
284}
285
286// =============================================================================
287// Request Builders
288// =============================================================================
289
290/// Request builder for getting events.
291#[derive(Debug)]
292pub struct GetEventsRequest<'a> {
293    client: &'a TheOddsApiClient,
294    sport: String,
295    date_format: Option<DateFormat>,
296    event_ids: Option<Vec<String>>,
297    commence_time_from: Option<DateTime<Utc>>,
298    commence_time_to: Option<DateTime<Utc>>,
299}
300
301impl<'a> GetEventsRequest<'a> {
302    fn new(client: &'a TheOddsApiClient, sport: String) -> Self {
303        Self {
304            client,
305            sport,
306            date_format: None,
307            event_ids: None,
308            commence_time_from: None,
309            commence_time_to: None,
310        }
311    }
312
313    /// Set the date format for the response.
314    pub fn date_format(mut self, format: DateFormat) -> Self {
315        self.date_format = Some(format);
316        self
317    }
318
319    /// Filter by specific event IDs.
320    pub fn event_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
321        self.event_ids = Some(ids.into_iter().map(Into::into).collect());
322        self
323    }
324
325    /// Filter events starting from this time.
326    pub fn commence_time_from(mut self, time: DateTime<Utc>) -> Self {
327        self.commence_time_from = Some(time);
328        self
329    }
330
331    /// Filter events starting until this time.
332    pub fn commence_time_to(mut self, time: DateTime<Utc>) -> Self {
333        self.commence_time_to = Some(time);
334        self
335    }
336
337    /// Execute the request.
338    pub async fn send(self) -> Result<Response<Vec<Event>>> {
339        let mut params = Vec::new();
340
341        if let Some(fmt) = self.date_format {
342            params.push(("dateFormat", fmt.to_string()));
343        }
344        if let Some(ids) = self.event_ids {
345            params.push(("eventIds", ids.join(",")));
346        }
347        if let Some(time) = self.commence_time_from {
348            params.push(("commenceTimeFrom", time.to_rfc3339()));
349        }
350        if let Some(time) = self.commence_time_to {
351            params.push(("commenceTimeTo", time.to_rfc3339()));
352        }
353
354        let url = self
355            .client
356            .build_url(&format!("/v4/sports/{}/events", self.sport), &params)?;
357        self.client.get(url).await
358    }
359}
360
361/// Request builder for getting odds.
362#[derive(Debug)]
363pub struct GetOddsRequest<'a> {
364    client: &'a TheOddsApiClient,
365    sport: String,
366    regions: Vec<Region>,
367    markets: Option<Vec<Market>>,
368    date_format: Option<DateFormat>,
369    odds_format: Option<OddsFormat>,
370    event_ids: Option<Vec<String>>,
371    bookmakers: Option<Vec<String>>,
372    commence_time_from: Option<DateTime<Utc>>,
373    commence_time_to: Option<DateTime<Utc>>,
374    include_links: Option<bool>,
375    include_sids: Option<bool>,
376    include_bet_limits: Option<bool>,
377}
378
379impl<'a> GetOddsRequest<'a> {
380    fn new(client: &'a TheOddsApiClient, sport: String) -> Self {
381        Self {
382            client,
383            sport,
384            regions: Vec::new(),
385            markets: None,
386            date_format: None,
387            odds_format: None,
388            event_ids: None,
389            bookmakers: None,
390            commence_time_from: None,
391            commence_time_to: None,
392            include_links: None,
393            include_sids: None,
394            include_bet_limits: None,
395        }
396    }
397
398    /// Set the regions (required).
399    pub fn regions(mut self, regions: &[Region]) -> Self {
400        self.regions = regions.to_vec();
401        self
402    }
403
404    /// Add a single region.
405    pub fn region(mut self, region: Region) -> Self {
406        self.regions.push(region);
407        self
408    }
409
410    /// Set the markets to retrieve.
411    pub fn markets(mut self, markets: &[Market]) -> Self {
412        self.markets = Some(markets.to_vec());
413        self
414    }
415
416    /// Add a single market.
417    pub fn market(mut self, market: Market) -> Self {
418        self.markets.get_or_insert_with(Vec::new).push(market);
419        self
420    }
421
422    /// Set the date format.
423    pub fn date_format(mut self, format: DateFormat) -> Self {
424        self.date_format = Some(format);
425        self
426    }
427
428    /// Set the odds format.
429    pub fn odds_format(mut self, format: OddsFormat) -> Self {
430        self.odds_format = Some(format);
431        self
432    }
433
434    /// Filter by specific event IDs.
435    pub fn event_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
436        self.event_ids = Some(ids.into_iter().map(Into::into).collect());
437        self
438    }
439
440    /// Filter by specific bookmakers.
441    pub fn bookmakers(mut self, bookmakers: impl IntoIterator<Item = impl Into<String>>) -> Self {
442        self.bookmakers = Some(bookmakers.into_iter().map(Into::into).collect());
443        self
444    }
445
446    /// Filter events starting from this time.
447    pub fn commence_time_from(mut self, time: DateTime<Utc>) -> Self {
448        self.commence_time_from = Some(time);
449        self
450    }
451
452    /// Filter events starting until this time.
453    pub fn commence_time_to(mut self, time: DateTime<Utc>) -> Self {
454        self.commence_time_to = Some(time);
455        self
456    }
457
458    /// Include deep links to bookmaker pages.
459    pub fn include_links(mut self, include: bool) -> Self {
460        self.include_links = Some(include);
461        self
462    }
463
464    /// Include site IDs.
465    pub fn include_sids(mut self, include: bool) -> Self {
466        self.include_sids = Some(include);
467        self
468    }
469
470    /// Include bet limits.
471    pub fn include_bet_limits(mut self, include: bool) -> Self {
472        self.include_bet_limits = Some(include);
473        self
474    }
475
476    /// Execute the request.
477    pub async fn send(self) -> Result<Response<Vec<EventOdds>>> {
478        if self.regions.is_empty() {
479            return Err(Error::MissingParameter("regions"));
480        }
481
482        let mut params = vec![("regions", format_csv(&self.regions))];
483
484        if let Some(markets) = self.markets {
485            params.push(("markets", format_csv(&markets)));
486        }
487        if let Some(fmt) = self.date_format {
488            params.push(("dateFormat", fmt.to_string()));
489        }
490        if let Some(fmt) = self.odds_format {
491            params.push(("oddsFormat", fmt.to_string()));
492        }
493        if let Some(ids) = self.event_ids {
494            params.push(("eventIds", ids.join(",")));
495        }
496        if let Some(bookmakers) = self.bookmakers {
497            params.push(("bookmakers", bookmakers.join(",")));
498        }
499        if let Some(time) = self.commence_time_from {
500            params.push(("commenceTimeFrom", time.to_rfc3339()));
501        }
502        if let Some(time) = self.commence_time_to {
503            params.push(("commenceTimeTo", time.to_rfc3339()));
504        }
505        if let Some(true) = self.include_links {
506            params.push(("includeLinks", "true".to_string()));
507        }
508        if let Some(true) = self.include_sids {
509            params.push(("includeSids", "true".to_string()));
510        }
511        if let Some(true) = self.include_bet_limits {
512            params.push(("includeBetLimits", "true".to_string()));
513        }
514
515        let url = self
516            .client
517            .build_url(&format!("/v4/sports/{}/odds", self.sport), &params)?;
518        self.client.get(url).await
519    }
520}
521
522/// Request builder for getting scores.
523#[derive(Debug)]
524pub struct GetScoresRequest<'a> {
525    client: &'a TheOddsApiClient,
526    sport: String,
527    days_from: Option<u8>,
528    date_format: Option<DateFormat>,
529    event_ids: Option<Vec<String>>,
530}
531
532impl<'a> GetScoresRequest<'a> {
533    fn new(client: &'a TheOddsApiClient, sport: String) -> Self {
534        Self {
535            client,
536            sport,
537            days_from: None,
538            date_format: None,
539            event_ids: None,
540        }
541    }
542
543    /// Include games from the past 1-3 days.
544    pub fn days_from(mut self, days: u8) -> Self {
545        self.days_from = Some(days.clamp(1, 3));
546        self
547    }
548
549    /// Set the date format.
550    pub fn date_format(mut self, format: DateFormat) -> Self {
551        self.date_format = Some(format);
552        self
553    }
554
555    /// Filter by specific event IDs.
556    pub fn event_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
557        self.event_ids = Some(ids.into_iter().map(Into::into).collect());
558        self
559    }
560
561    /// Execute the request.
562    pub async fn send(self) -> Result<Response<Vec<EventScore>>> {
563        let mut params = Vec::new();
564
565        if let Some(days) = self.days_from {
566            params.push(("daysFrom", days.to_string()));
567        }
568        if let Some(fmt) = self.date_format {
569            params.push(("dateFormat", fmt.to_string()));
570        }
571        if let Some(ids) = self.event_ids {
572            params.push(("eventIds", ids.join(",")));
573        }
574
575        let url = self
576            .client
577            .build_url(&format!("/v4/sports/{}/scores", self.sport), &params)?;
578        self.client.get(url).await
579    }
580}
581
582/// Request builder for getting event odds.
583#[derive(Debug)]
584pub struct GetEventOddsRequest<'a> {
585    client: &'a TheOddsApiClient,
586    sport: String,
587    event_id: String,
588    regions: Vec<Region>,
589    markets: Option<Vec<Market>>,
590    date_format: Option<DateFormat>,
591    odds_format: Option<OddsFormat>,
592    bookmakers: Option<Vec<String>>,
593    include_links: Option<bool>,
594    include_sids: Option<bool>,
595    include_multipliers: Option<bool>,
596}
597
598impl<'a> GetEventOddsRequest<'a> {
599    fn new(client: &'a TheOddsApiClient, sport: String, event_id: String) -> Self {
600        Self {
601            client,
602            sport,
603            event_id,
604            regions: Vec::new(),
605            markets: None,
606            date_format: None,
607            odds_format: None,
608            bookmakers: None,
609            include_links: None,
610            include_sids: None,
611            include_multipliers: None,
612        }
613    }
614
615    /// Set the regions (required).
616    pub fn regions(mut self, regions: &[Region]) -> Self {
617        self.regions = regions.to_vec();
618        self
619    }
620
621    /// Add a single region.
622    pub fn region(mut self, region: Region) -> Self {
623        self.regions.push(region);
624        self
625    }
626
627    /// Set the markets to retrieve.
628    pub fn markets(mut self, markets: &[Market]) -> Self {
629        self.markets = Some(markets.to_vec());
630        self
631    }
632
633    /// Add a single market.
634    pub fn market(mut self, market: Market) -> Self {
635        self.markets.get_or_insert_with(Vec::new).push(market);
636        self
637    }
638
639    /// Add a custom market by key.
640    pub fn custom_market(mut self, key: impl Into<String>) -> Self {
641        self.markets
642            .get_or_insert_with(Vec::new)
643            .push(Market::Custom(key.into()));
644        self
645    }
646
647    /// Set the date format.
648    pub fn date_format(mut self, format: DateFormat) -> Self {
649        self.date_format = Some(format);
650        self
651    }
652
653    /// Set the odds format.
654    pub fn odds_format(mut self, format: OddsFormat) -> Self {
655        self.odds_format = Some(format);
656        self
657    }
658
659    /// Filter by specific bookmakers.
660    pub fn bookmakers(mut self, bookmakers: impl IntoIterator<Item = impl Into<String>>) -> Self {
661        self.bookmakers = Some(bookmakers.into_iter().map(Into::into).collect());
662        self
663    }
664
665    /// Include deep links.
666    pub fn include_links(mut self, include: bool) -> Self {
667        self.include_links = Some(include);
668        self
669    }
670
671    /// Include site IDs.
672    pub fn include_sids(mut self, include: bool) -> Self {
673        self.include_sids = Some(include);
674        self
675    }
676
677    /// Include DFS multipliers.
678    pub fn include_multipliers(mut self, include: bool) -> Self {
679        self.include_multipliers = Some(include);
680        self
681    }
682
683    /// Execute the request.
684    pub async fn send(self) -> Result<Response<EventOdds>> {
685        if self.regions.is_empty() {
686            return Err(Error::MissingParameter("regions"));
687        }
688
689        let mut params = vec![("regions", format_csv(&self.regions))];
690
691        if let Some(markets) = self.markets {
692            params.push(("markets", format_csv(&markets)));
693        }
694        if let Some(fmt) = self.date_format {
695            params.push(("dateFormat", fmt.to_string()));
696        }
697        if let Some(fmt) = self.odds_format {
698            params.push(("oddsFormat", fmt.to_string()));
699        }
700        if let Some(bookmakers) = self.bookmakers {
701            params.push(("bookmakers", bookmakers.join(",")));
702        }
703        if let Some(true) = self.include_links {
704            params.push(("includeLinks", "true".to_string()));
705        }
706        if let Some(true) = self.include_sids {
707            params.push(("includeSids", "true".to_string()));
708        }
709        if let Some(true) = self.include_multipliers {
710            params.push(("includeMultipliers", "true".to_string()));
711        }
712
713        let url = self.client.build_url(
714            &format!("/v4/sports/{}/events/{}/odds", self.sport, self.event_id),
715            &params,
716        )?;
717        self.client.get(url).await
718    }
719}
720
721/// Request builder for getting event markets.
722#[derive(Debug)]
723pub struct GetEventMarketsRequest<'a> {
724    client: &'a TheOddsApiClient,
725    sport: String,
726    event_id: String,
727    regions: Vec<Region>,
728    bookmakers: Option<Vec<String>>,
729    date_format: Option<DateFormat>,
730}
731
732impl<'a> GetEventMarketsRequest<'a> {
733    fn new(client: &'a TheOddsApiClient, sport: String, event_id: String) -> Self {
734        Self {
735            client,
736            sport,
737            event_id,
738            regions: Vec::new(),
739            bookmakers: None,
740            date_format: None,
741        }
742    }
743
744    /// Set the regions (required).
745    pub fn regions(mut self, regions: &[Region]) -> Self {
746        self.regions = regions.to_vec();
747        self
748    }
749
750    /// Add a single region.
751    pub fn region(mut self, region: Region) -> Self {
752        self.regions.push(region);
753        self
754    }
755
756    /// Filter by specific bookmakers.
757    pub fn bookmakers(mut self, bookmakers: impl IntoIterator<Item = impl Into<String>>) -> Self {
758        self.bookmakers = Some(bookmakers.into_iter().map(Into::into).collect());
759        self
760    }
761
762    /// Set the date format.
763    pub fn date_format(mut self, format: DateFormat) -> Self {
764        self.date_format = Some(format);
765        self
766    }
767
768    /// Execute the request.
769    pub async fn send(self) -> Result<Response<EventMarkets>> {
770        if self.regions.is_empty() {
771            return Err(Error::MissingParameter("regions"));
772        }
773
774        let mut params = vec![("regions", format_csv(&self.regions))];
775
776        if let Some(bookmakers) = self.bookmakers {
777            params.push(("bookmakers", bookmakers.join(",")));
778        }
779        if let Some(fmt) = self.date_format {
780            params.push(("dateFormat", fmt.to_string()));
781        }
782
783        let url = self.client.build_url(
784            &format!(
785                "/v4/sports/{}/events/{}/markets",
786                self.sport, self.event_id
787            ),
788            &params,
789        )?;
790        self.client.get(url).await
791    }
792}
793
794/// Request builder for historical odds.
795#[derive(Debug)]
796pub struct GetHistoricalOddsRequest<'a> {
797    client: &'a TheOddsApiClient,
798    sport: String,
799    date: Option<DateTime<Utc>>,
800    regions: Vec<Region>,
801    markets: Option<Vec<Market>>,
802    date_format: Option<DateFormat>,
803    odds_format: Option<OddsFormat>,
804    event_ids: Option<Vec<String>>,
805    bookmakers: Option<Vec<String>>,
806    commence_time_from: Option<DateTime<Utc>>,
807    commence_time_to: Option<DateTime<Utc>>,
808}
809
810impl<'a> GetHistoricalOddsRequest<'a> {
811    fn new(client: &'a TheOddsApiClient, sport: String) -> Self {
812        Self {
813            client,
814            sport,
815            date: None,
816            regions: Vec::new(),
817            markets: None,
818            date_format: None,
819            odds_format: None,
820            event_ids: None,
821            bookmakers: None,
822            commence_time_from: None,
823            commence_time_to: None,
824        }
825    }
826
827    /// Set the historical snapshot date (required).
828    pub fn date(mut self, date: DateTime<Utc>) -> Self {
829        self.date = Some(date);
830        self
831    }
832
833    /// Set the regions (required).
834    pub fn regions(mut self, regions: &[Region]) -> Self {
835        self.regions = regions.to_vec();
836        self
837    }
838
839    /// Add a single region.
840    pub fn region(mut self, region: Region) -> Self {
841        self.regions.push(region);
842        self
843    }
844
845    /// Set the markets to retrieve.
846    pub fn markets(mut self, markets: &[Market]) -> Self {
847        self.markets = Some(markets.to_vec());
848        self
849    }
850
851    /// Set the date format.
852    pub fn date_format(mut self, format: DateFormat) -> Self {
853        self.date_format = Some(format);
854        self
855    }
856
857    /// Set the odds format.
858    pub fn odds_format(mut self, format: OddsFormat) -> Self {
859        self.odds_format = Some(format);
860        self
861    }
862
863    /// Filter by specific event IDs.
864    pub fn event_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
865        self.event_ids = Some(ids.into_iter().map(Into::into).collect());
866        self
867    }
868
869    /// Filter by specific bookmakers.
870    pub fn bookmakers(mut self, bookmakers: impl IntoIterator<Item = impl Into<String>>) -> Self {
871        self.bookmakers = Some(bookmakers.into_iter().map(Into::into).collect());
872        self
873    }
874
875    /// Filter events starting from this time.
876    pub fn commence_time_from(mut self, time: DateTime<Utc>) -> Self {
877        self.commence_time_from = Some(time);
878        self
879    }
880
881    /// Filter events starting until this time.
882    pub fn commence_time_to(mut self, time: DateTime<Utc>) -> Self {
883        self.commence_time_to = Some(time);
884        self
885    }
886
887    /// Execute the request.
888    pub async fn send(self) -> Result<Response<HistoricalResponse<Vec<EventOdds>>>> {
889        let date = self.date.ok_or(Error::MissingParameter("date"))?;
890        if self.regions.is_empty() {
891            return Err(Error::MissingParameter("regions"));
892        }
893
894        let mut params = vec![
895            ("date", date.to_rfc3339()),
896            ("regions", format_csv(&self.regions)),
897        ];
898
899        if let Some(markets) = self.markets {
900            params.push(("markets", format_csv(&markets)));
901        }
902        if let Some(fmt) = self.date_format {
903            params.push(("dateFormat", fmt.to_string()));
904        }
905        if let Some(fmt) = self.odds_format {
906            params.push(("oddsFormat", fmt.to_string()));
907        }
908        if let Some(ids) = self.event_ids {
909            params.push(("eventIds", ids.join(",")));
910        }
911        if let Some(bookmakers) = self.bookmakers {
912            params.push(("bookmakers", bookmakers.join(",")));
913        }
914        if let Some(time) = self.commence_time_from {
915            params.push(("commenceTimeFrom", time.to_rfc3339()));
916        }
917        if let Some(time) = self.commence_time_to {
918            params.push(("commenceTimeTo", time.to_rfc3339()));
919        }
920
921        let url = self
922            .client
923            .build_url(&format!("/v4/historical/sports/{}/odds", self.sport), &params)?;
924        self.client.get(url).await
925    }
926}
927
928/// Request builder for historical events.
929#[derive(Debug)]
930pub struct GetHistoricalEventsRequest<'a> {
931    client: &'a TheOddsApiClient,
932    sport: String,
933    date: Option<DateTime<Utc>>,
934    date_format: Option<DateFormat>,
935    event_ids: Option<Vec<String>>,
936    commence_time_from: Option<DateTime<Utc>>,
937    commence_time_to: Option<DateTime<Utc>>,
938}
939
940impl<'a> GetHistoricalEventsRequest<'a> {
941    fn new(client: &'a TheOddsApiClient, sport: String) -> Self {
942        Self {
943            client,
944            sport,
945            date: None,
946            date_format: None,
947            event_ids: None,
948            commence_time_from: None,
949            commence_time_to: None,
950        }
951    }
952
953    /// Set the historical snapshot date (required).
954    pub fn date(mut self, date: DateTime<Utc>) -> Self {
955        self.date = Some(date);
956        self
957    }
958
959    /// Set the date format.
960    pub fn date_format(mut self, format: DateFormat) -> Self {
961        self.date_format = Some(format);
962        self
963    }
964
965    /// Filter by specific event IDs.
966    pub fn event_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
967        self.event_ids = Some(ids.into_iter().map(Into::into).collect());
968        self
969    }
970
971    /// Filter events starting from this time.
972    pub fn commence_time_from(mut self, time: DateTime<Utc>) -> Self {
973        self.commence_time_from = Some(time);
974        self
975    }
976
977    /// Filter events starting until this time.
978    pub fn commence_time_to(mut self, time: DateTime<Utc>) -> Self {
979        self.commence_time_to = Some(time);
980        self
981    }
982
983    /// Execute the request.
984    pub async fn send(self) -> Result<Response<HistoricalResponse<Vec<Event>>>> {
985        let date = self.date.ok_or(Error::MissingParameter("date"))?;
986
987        let mut params = vec![("date", date.to_rfc3339())];
988
989        if let Some(fmt) = self.date_format {
990            params.push(("dateFormat", fmt.to_string()));
991        }
992        if let Some(ids) = self.event_ids {
993            params.push(("eventIds", ids.join(",")));
994        }
995        if let Some(time) = self.commence_time_from {
996            params.push(("commenceTimeFrom", time.to_rfc3339()));
997        }
998        if let Some(time) = self.commence_time_to {
999            params.push(("commenceTimeTo", time.to_rfc3339()));
1000        }
1001
1002        let url = self.client.build_url(
1003            &format!("/v4/historical/sports/{}/events", self.sport),
1004            &params,
1005        )?;
1006        self.client.get(url).await
1007    }
1008}
1009
1010/// Request builder for historical event odds.
1011#[derive(Debug)]
1012pub struct GetHistoricalEventOddsRequest<'a> {
1013    client: &'a TheOddsApiClient,
1014    sport: String,
1015    event_id: String,
1016    date: Option<DateTime<Utc>>,
1017    regions: Vec<Region>,
1018    markets: Option<Vec<Market>>,
1019    date_format: Option<DateFormat>,
1020    odds_format: Option<OddsFormat>,
1021    include_multipliers: Option<bool>,
1022}
1023
1024impl<'a> GetHistoricalEventOddsRequest<'a> {
1025    fn new(client: &'a TheOddsApiClient, sport: String, event_id: String) -> Self {
1026        Self {
1027            client,
1028            sport,
1029            event_id,
1030            date: None,
1031            regions: Vec::new(),
1032            markets: None,
1033            date_format: None,
1034            odds_format: None,
1035            include_multipliers: None,
1036        }
1037    }
1038
1039    /// Set the historical snapshot date (required).
1040    pub fn date(mut self, date: DateTime<Utc>) -> Self {
1041        self.date = Some(date);
1042        self
1043    }
1044
1045    /// Set the regions (required).
1046    pub fn regions(mut self, regions: &[Region]) -> Self {
1047        self.regions = regions.to_vec();
1048        self
1049    }
1050
1051    /// Add a single region.
1052    pub fn region(mut self, region: Region) -> Self {
1053        self.regions.push(region);
1054        self
1055    }
1056
1057    /// Set the markets to retrieve.
1058    pub fn markets(mut self, markets: &[Market]) -> Self {
1059        self.markets = Some(markets.to_vec());
1060        self
1061    }
1062
1063    /// Set the date format.
1064    pub fn date_format(mut self, format: DateFormat) -> Self {
1065        self.date_format = Some(format);
1066        self
1067    }
1068
1069    /// Set the odds format.
1070    pub fn odds_format(mut self, format: OddsFormat) -> Self {
1071        self.odds_format = Some(format);
1072        self
1073    }
1074
1075    /// Include DFS multipliers.
1076    pub fn include_multipliers(mut self, include: bool) -> Self {
1077        self.include_multipliers = Some(include);
1078        self
1079    }
1080
1081    /// Execute the request.
1082    pub async fn send(self) -> Result<Response<HistoricalResponse<EventOdds>>> {
1083        let date = self.date.ok_or(Error::MissingParameter("date"))?;
1084        if self.regions.is_empty() {
1085            return Err(Error::MissingParameter("regions"));
1086        }
1087
1088        let mut params = vec![
1089            ("date", date.to_rfc3339()),
1090            ("regions", format_csv(&self.regions)),
1091        ];
1092
1093        if let Some(markets) = self.markets {
1094            params.push(("markets", format_csv(&markets)));
1095        }
1096        if let Some(fmt) = self.date_format {
1097            params.push(("dateFormat", fmt.to_string()));
1098        }
1099        if let Some(fmt) = self.odds_format {
1100            params.push(("oddsFormat", fmt.to_string()));
1101        }
1102        if let Some(true) = self.include_multipliers {
1103            params.push(("includeMultipliers", "true".to_string()));
1104        }
1105
1106        let url = self.client.build_url(
1107            &format!(
1108                "/v4/historical/sports/{}/events/{}/odds",
1109                self.sport, self.event_id
1110            ),
1111            &params,
1112        )?;
1113        self.client.get(url).await
1114    }
1115}