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