use crate::error::{Error, Result};
use crate::models::*;
use crate::types::*;
use chrono::{DateTime, Utc};
use reqwest::Client;
use url::Url;
const DEFAULT_BASE_URL: &str = "https://api.the-odds-api.com";
const IPV6_BASE_URL: &str = "https://ipv6-api.the-odds-api.com";
#[derive(Debug, Clone)]
pub struct TheOddsApiClientBuilder {
api_key: String,
base_url: String,
client: Option<Client>,
}
impl TheOddsApiClientBuilder {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
base_url: DEFAULT_BASE_URL.to_string(),
client: None,
}
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
pub fn use_ipv6(mut self) -> Self {
self.base_url = IPV6_BASE_URL.to_string();
self
}
pub fn client(mut self, client: Client) -> Self {
self.client = Some(client);
self
}
pub fn build(self) -> TheOddsApiClient {
TheOddsApiClient {
api_key: self.api_key,
base_url: self.base_url,
client: self.client.unwrap_or_default(),
}
}
}
#[derive(Debug, Clone)]
pub struct TheOddsApiClient {
api_key: String,
base_url: String,
client: Client,
}
impl TheOddsApiClient {
pub fn new(api_key: impl Into<String>) -> Self {
TheOddsApiClientBuilder::new(api_key).build()
}
pub fn builder(api_key: impl Into<String>) -> TheOddsApiClientBuilder {
TheOddsApiClientBuilder::new(api_key)
}
fn build_url(&self, path: &str, params: &[(&str, String)]) -> Result<Url> {
let mut url = Url::parse(&format!("{}{}", self.base_url, path))?;
{
let mut query = url.query_pairs_mut();
query.append_pair("apiKey", &self.api_key);
for (key, value) in params {
if !value.is_empty() {
query.append_pair(key, value);
}
}
}
Ok(url)
}
async fn get<T: serde::de::DeserializeOwned>(&self, url: Url) -> Result<Response<T>> {
let response = self.client.get(url).send().await?;
let usage = UsageInfo::from_headers(response.headers());
let status = response.status();
if status.is_success() {
let data = response.json().await?;
Ok(Response::new(data, usage))
} else if status == reqwest::StatusCode::UNAUTHORIZED {
Err(Error::Unauthorized)
} else if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
Err(Error::RateLimited {
requests_remaining: usage.requests_remaining,
})
} else {
let message = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
Err(Error::Api {
status: status.as_u16(),
message,
})
}
}
pub async fn get_sports(&self) -> Result<Response<Vec<Sport>>> {
let url = self.build_url("/v4/sports", &[])?;
self.get(url).await
}
pub async fn get_all_sports(&self) -> Result<Response<Vec<Sport>>> {
let url = self.build_url("/v4/sports", &[("all", "true".to_string())])?;
self.get(url).await
}
pub fn get_events(&self, sport: impl Into<String>) -> GetEventsRequest<'_> {
GetEventsRequest::new(self, sport.into())
}
pub fn get_odds(&self, sport: impl Into<String>) -> GetOddsRequest<'_> {
GetOddsRequest::new(self, sport.into())
}
pub fn get_upcoming_odds(&self) -> GetOddsRequest<'_> {
GetOddsRequest::new(self, "upcoming".to_string())
}
pub fn get_scores(&self, sport: impl Into<String>) -> GetScoresRequest<'_> {
GetScoresRequest::new(self, sport.into())
}
pub fn get_event_odds(
&self,
sport: impl Into<String>,
event_id: impl Into<String>,
) -> GetEventOddsRequest<'_> {
GetEventOddsRequest::new(self, sport.into(), event_id.into())
}
pub fn get_event_markets(
&self,
sport: impl Into<String>,
event_id: impl Into<String>,
) -> GetEventMarketsRequest<'_> {
GetEventMarketsRequest::new(self, sport.into(), event_id.into())
}
pub async fn get_participants(
&self,
sport: impl Into<String>,
) -> Result<Response<Vec<Participant>>> {
let url = self.build_url(&format!("/v4/sports/{}/participants", sport.into()), &[])?;
self.get(url).await
}
pub fn get_historical_odds(&self, sport: impl Into<String>) -> GetHistoricalOddsRequest<'_> {
GetHistoricalOddsRequest::new(self, sport.into())
}
pub fn get_historical_events(&self, sport: impl Into<String>) -> GetHistoricalEventsRequest<'_> {
GetHistoricalEventsRequest::new(self, sport.into())
}
pub fn get_historical_event_odds(
&self,
sport: impl Into<String>,
event_id: impl Into<String>,
) -> GetHistoricalEventOddsRequest<'_> {
GetHistoricalEventOddsRequest::new(self, sport.into(), event_id.into())
}
}
#[derive(Debug)]
pub struct GetEventsRequest<'a> {
client: &'a TheOddsApiClient,
sport: String,
date_format: Option<DateFormat>,
event_ids: Option<Vec<String>>,
commence_time_from: Option<DateTime<Utc>>,
commence_time_to: Option<DateTime<Utc>>,
}
impl<'a> GetEventsRequest<'a> {
fn new(client: &'a TheOddsApiClient, sport: String) -> Self {
Self {
client,
sport,
date_format: None,
event_ids: None,
commence_time_from: None,
commence_time_to: None,
}
}
pub fn date_format(mut self, format: DateFormat) -> Self {
self.date_format = Some(format);
self
}
pub fn event_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.event_ids = Some(ids.into_iter().map(Into::into).collect());
self
}
pub fn commence_time_from(mut self, time: DateTime<Utc>) -> Self {
self.commence_time_from = Some(time);
self
}
pub fn commence_time_to(mut self, time: DateTime<Utc>) -> Self {
self.commence_time_to = Some(time);
self
}
pub async fn send(self) -> Result<Response<Vec<Event>>> {
let mut params = Vec::new();
if let Some(fmt) = self.date_format {
params.push(("dateFormat", fmt.to_string()));
}
if let Some(ids) = self.event_ids {
params.push(("eventIds", ids.join(",")));
}
if let Some(time) = self.commence_time_from {
params.push(("commenceTimeFrom", time.to_rfc3339()));
}
if let Some(time) = self.commence_time_to {
params.push(("commenceTimeTo", time.to_rfc3339()));
}
let url = self
.client
.build_url(&format!("/v4/sports/{}/events", self.sport), ¶ms)?;
self.client.get(url).await
}
}
#[derive(Debug)]
pub struct GetOddsRequest<'a> {
client: &'a TheOddsApiClient,
sport: String,
regions: Vec<Region>,
markets: Option<Vec<Market>>,
date_format: Option<DateFormat>,
odds_format: Option<OddsFormat>,
event_ids: Option<Vec<String>>,
bookmakers: Option<Vec<String>>,
commence_time_from: Option<DateTime<Utc>>,
commence_time_to: Option<DateTime<Utc>>,
include_links: Option<bool>,
include_sids: Option<bool>,
include_bet_limits: Option<bool>,
}
impl<'a> GetOddsRequest<'a> {
fn new(client: &'a TheOddsApiClient, sport: String) -> Self {
Self {
client,
sport,
regions: Vec::new(),
markets: None,
date_format: None,
odds_format: None,
event_ids: None,
bookmakers: None,
commence_time_from: None,
commence_time_to: None,
include_links: None,
include_sids: None,
include_bet_limits: None,
}
}
pub fn regions(mut self, regions: &[Region]) -> Self {
self.regions = regions.to_vec();
self
}
pub fn region(mut self, region: Region) -> Self {
self.regions.push(region);
self
}
pub fn markets(mut self, markets: &[Market]) -> Self {
self.markets = Some(markets.to_vec());
self
}
pub fn market(mut self, market: Market) -> Self {
self.markets.get_or_insert_with(Vec::new).push(market);
self
}
pub fn date_format(mut self, format: DateFormat) -> Self {
self.date_format = Some(format);
self
}
pub fn odds_format(mut self, format: OddsFormat) -> Self {
self.odds_format = Some(format);
self
}
pub fn event_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.event_ids = Some(ids.into_iter().map(Into::into).collect());
self
}
pub fn bookmakers(mut self, bookmakers: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.bookmakers = Some(bookmakers.into_iter().map(Into::into).collect());
self
}
pub fn commence_time_from(mut self, time: DateTime<Utc>) -> Self {
self.commence_time_from = Some(time);
self
}
pub fn commence_time_to(mut self, time: DateTime<Utc>) -> Self {
self.commence_time_to = Some(time);
self
}
pub fn include_links(mut self, include: bool) -> Self {
self.include_links = Some(include);
self
}
pub fn include_sids(mut self, include: bool) -> Self {
self.include_sids = Some(include);
self
}
pub fn include_bet_limits(mut self, include: bool) -> Self {
self.include_bet_limits = Some(include);
self
}
pub async fn send(self) -> Result<Response<Vec<EventOdds>>> {
if self.regions.is_empty() {
return Err(Error::MissingParameter("regions"));
}
let mut params = vec![("regions", format_csv(&self.regions))];
if let Some(markets) = self.markets {
params.push(("markets", format_csv(&markets)));
}
if let Some(fmt) = self.date_format {
params.push(("dateFormat", fmt.to_string()));
}
if let Some(fmt) = self.odds_format {
params.push(("oddsFormat", fmt.to_string()));
}
if let Some(ids) = self.event_ids {
params.push(("eventIds", ids.join(",")));
}
if let Some(bookmakers) = self.bookmakers {
params.push(("bookmakers", bookmakers.join(",")));
}
if let Some(time) = self.commence_time_from {
params.push(("commenceTimeFrom", time.to_rfc3339()));
}
if let Some(time) = self.commence_time_to {
params.push(("commenceTimeTo", time.to_rfc3339()));
}
if let Some(true) = self.include_links {
params.push(("includeLinks", "true".to_string()));
}
if let Some(true) = self.include_sids {
params.push(("includeSids", "true".to_string()));
}
if let Some(true) = self.include_bet_limits {
params.push(("includeBetLimits", "true".to_string()));
}
let url = self
.client
.build_url(&format!("/v4/sports/{}/odds", self.sport), ¶ms)?;
self.client.get(url).await
}
}
#[derive(Debug)]
pub struct GetScoresRequest<'a> {
client: &'a TheOddsApiClient,
sport: String,
days_from: Option<u8>,
date_format: Option<DateFormat>,
event_ids: Option<Vec<String>>,
}
impl<'a> GetScoresRequest<'a> {
fn new(client: &'a TheOddsApiClient, sport: String) -> Self {
Self {
client,
sport,
days_from: None,
date_format: None,
event_ids: None,
}
}
pub fn days_from(mut self, days: u8) -> Self {
self.days_from = Some(days.clamp(1, 3));
self
}
pub fn date_format(mut self, format: DateFormat) -> Self {
self.date_format = Some(format);
self
}
pub fn event_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.event_ids = Some(ids.into_iter().map(Into::into).collect());
self
}
pub async fn send(self) -> Result<Response<Vec<EventScore>>> {
let mut params = Vec::new();
if let Some(days) = self.days_from {
params.push(("daysFrom", days.to_string()));
}
if let Some(fmt) = self.date_format {
params.push(("dateFormat", fmt.to_string()));
}
if let Some(ids) = self.event_ids {
params.push(("eventIds", ids.join(",")));
}
let url = self
.client
.build_url(&format!("/v4/sports/{}/scores", self.sport), ¶ms)?;
self.client.get(url).await
}
}
#[derive(Debug)]
pub struct GetEventOddsRequest<'a> {
client: &'a TheOddsApiClient,
sport: String,
event_id: String,
regions: Vec<Region>,
markets: Option<Vec<Market>>,
date_format: Option<DateFormat>,
odds_format: Option<OddsFormat>,
bookmakers: Option<Vec<String>>,
include_links: Option<bool>,
include_sids: Option<bool>,
include_multipliers: Option<bool>,
}
impl<'a> GetEventOddsRequest<'a> {
fn new(client: &'a TheOddsApiClient, sport: String, event_id: String) -> Self {
Self {
client,
sport,
event_id,
regions: Vec::new(),
markets: None,
date_format: None,
odds_format: None,
bookmakers: None,
include_links: None,
include_sids: None,
include_multipliers: None,
}
}
pub fn regions(mut self, regions: &[Region]) -> Self {
self.regions = regions.to_vec();
self
}
pub fn region(mut self, region: Region) -> Self {
self.regions.push(region);
self
}
pub fn markets(mut self, markets: &[Market]) -> Self {
self.markets = Some(markets.to_vec());
self
}
pub fn market(mut self, market: Market) -> Self {
self.markets.get_or_insert_with(Vec::new).push(market);
self
}
pub fn custom_market(mut self, key: impl Into<String>) -> Self {
self.markets
.get_or_insert_with(Vec::new)
.push(Market::Custom(key.into()));
self
}
pub fn date_format(mut self, format: DateFormat) -> Self {
self.date_format = Some(format);
self
}
pub fn odds_format(mut self, format: OddsFormat) -> Self {
self.odds_format = Some(format);
self
}
pub fn bookmakers(mut self, bookmakers: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.bookmakers = Some(bookmakers.into_iter().map(Into::into).collect());
self
}
pub fn include_links(mut self, include: bool) -> Self {
self.include_links = Some(include);
self
}
pub fn include_sids(mut self, include: bool) -> Self {
self.include_sids = Some(include);
self
}
pub fn include_multipliers(mut self, include: bool) -> Self {
self.include_multipliers = Some(include);
self
}
pub async fn send(self) -> Result<Response<EventOdds>> {
if self.regions.is_empty() {
return Err(Error::MissingParameter("regions"));
}
let mut params = vec![("regions", format_csv(&self.regions))];
if let Some(markets) = self.markets {
params.push(("markets", format_csv(&markets)));
}
if let Some(fmt) = self.date_format {
params.push(("dateFormat", fmt.to_string()));
}
if let Some(fmt) = self.odds_format {
params.push(("oddsFormat", fmt.to_string()));
}
if let Some(bookmakers) = self.bookmakers {
params.push(("bookmakers", bookmakers.join(",")));
}
if let Some(true) = self.include_links {
params.push(("includeLinks", "true".to_string()));
}
if let Some(true) = self.include_sids {
params.push(("includeSids", "true".to_string()));
}
if let Some(true) = self.include_multipliers {
params.push(("includeMultipliers", "true".to_string()));
}
let url = self.client.build_url(
&format!("/v4/sports/{}/events/{}/odds", self.sport, self.event_id),
¶ms,
)?;
self.client.get(url).await
}
}
#[derive(Debug)]
pub struct GetEventMarketsRequest<'a> {
client: &'a TheOddsApiClient,
sport: String,
event_id: String,
regions: Vec<Region>,
bookmakers: Option<Vec<String>>,
date_format: Option<DateFormat>,
}
impl<'a> GetEventMarketsRequest<'a> {
fn new(client: &'a TheOddsApiClient, sport: String, event_id: String) -> Self {
Self {
client,
sport,
event_id,
regions: Vec::new(),
bookmakers: None,
date_format: None,
}
}
pub fn regions(mut self, regions: &[Region]) -> Self {
self.regions = regions.to_vec();
self
}
pub fn region(mut self, region: Region) -> Self {
self.regions.push(region);
self
}
pub fn bookmakers(mut self, bookmakers: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.bookmakers = Some(bookmakers.into_iter().map(Into::into).collect());
self
}
pub fn date_format(mut self, format: DateFormat) -> Self {
self.date_format = Some(format);
self
}
pub async fn send(self) -> Result<Response<EventMarkets>> {
if self.regions.is_empty() {
return Err(Error::MissingParameter("regions"));
}
let mut params = vec![("regions", format_csv(&self.regions))];
if let Some(bookmakers) = self.bookmakers {
params.push(("bookmakers", bookmakers.join(",")));
}
if let Some(fmt) = self.date_format {
params.push(("dateFormat", fmt.to_string()));
}
let url = self.client.build_url(
&format!(
"/v4/sports/{}/events/{}/markets",
self.sport, self.event_id
),
¶ms,
)?;
self.client.get(url).await
}
}
#[derive(Debug)]
pub struct GetHistoricalOddsRequest<'a> {
client: &'a TheOddsApiClient,
sport: String,
date: Option<DateTime<Utc>>,
regions: Vec<Region>,
markets: Option<Vec<Market>>,
date_format: Option<DateFormat>,
odds_format: Option<OddsFormat>,
event_ids: Option<Vec<String>>,
bookmakers: Option<Vec<String>>,
commence_time_from: Option<DateTime<Utc>>,
commence_time_to: Option<DateTime<Utc>>,
}
impl<'a> GetHistoricalOddsRequest<'a> {
fn new(client: &'a TheOddsApiClient, sport: String) -> Self {
Self {
client,
sport,
date: None,
regions: Vec::new(),
markets: None,
date_format: None,
odds_format: None,
event_ids: None,
bookmakers: None,
commence_time_from: None,
commence_time_to: None,
}
}
pub fn date(mut self, date: DateTime<Utc>) -> Self {
self.date = Some(date);
self
}
pub fn regions(mut self, regions: &[Region]) -> Self {
self.regions = regions.to_vec();
self
}
pub fn region(mut self, region: Region) -> Self {
self.regions.push(region);
self
}
pub fn markets(mut self, markets: &[Market]) -> Self {
self.markets = Some(markets.to_vec());
self
}
pub fn date_format(mut self, format: DateFormat) -> Self {
self.date_format = Some(format);
self
}
pub fn odds_format(mut self, format: OddsFormat) -> Self {
self.odds_format = Some(format);
self
}
pub fn event_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.event_ids = Some(ids.into_iter().map(Into::into).collect());
self
}
pub fn bookmakers(mut self, bookmakers: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.bookmakers = Some(bookmakers.into_iter().map(Into::into).collect());
self
}
pub fn commence_time_from(mut self, time: DateTime<Utc>) -> Self {
self.commence_time_from = Some(time);
self
}
pub fn commence_time_to(mut self, time: DateTime<Utc>) -> Self {
self.commence_time_to = Some(time);
self
}
pub async fn send(self) -> Result<Response<HistoricalResponse<Vec<EventOdds>>>> {
let date = self.date.ok_or(Error::MissingParameter("date"))?;
if self.regions.is_empty() {
return Err(Error::MissingParameter("regions"));
}
let mut params = vec![
("date", date.to_rfc3339()),
("regions", format_csv(&self.regions)),
];
if let Some(markets) = self.markets {
params.push(("markets", format_csv(&markets)));
}
if let Some(fmt) = self.date_format {
params.push(("dateFormat", fmt.to_string()));
}
if let Some(fmt) = self.odds_format {
params.push(("oddsFormat", fmt.to_string()));
}
if let Some(ids) = self.event_ids {
params.push(("eventIds", ids.join(",")));
}
if let Some(bookmakers) = self.bookmakers {
params.push(("bookmakers", bookmakers.join(",")));
}
if let Some(time) = self.commence_time_from {
params.push(("commenceTimeFrom", time.to_rfc3339()));
}
if let Some(time) = self.commence_time_to {
params.push(("commenceTimeTo", time.to_rfc3339()));
}
let url = self
.client
.build_url(&format!("/v4/historical/sports/{}/odds", self.sport), ¶ms)?;
self.client.get(url).await
}
}
#[derive(Debug)]
pub struct GetHistoricalEventsRequest<'a> {
client: &'a TheOddsApiClient,
sport: String,
date: Option<DateTime<Utc>>,
date_format: Option<DateFormat>,
event_ids: Option<Vec<String>>,
commence_time_from: Option<DateTime<Utc>>,
commence_time_to: Option<DateTime<Utc>>,
}
impl<'a> GetHistoricalEventsRequest<'a> {
fn new(client: &'a TheOddsApiClient, sport: String) -> Self {
Self {
client,
sport,
date: None,
date_format: None,
event_ids: None,
commence_time_from: None,
commence_time_to: None,
}
}
pub fn date(mut self, date: DateTime<Utc>) -> Self {
self.date = Some(date);
self
}
pub fn date_format(mut self, format: DateFormat) -> Self {
self.date_format = Some(format);
self
}
pub fn event_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.event_ids = Some(ids.into_iter().map(Into::into).collect());
self
}
pub fn commence_time_from(mut self, time: DateTime<Utc>) -> Self {
self.commence_time_from = Some(time);
self
}
pub fn commence_time_to(mut self, time: DateTime<Utc>) -> Self {
self.commence_time_to = Some(time);
self
}
pub async fn send(self) -> Result<Response<HistoricalResponse<Vec<Event>>>> {
let date = self.date.ok_or(Error::MissingParameter("date"))?;
let mut params = vec![("date", date.to_rfc3339())];
if let Some(fmt) = self.date_format {
params.push(("dateFormat", fmt.to_string()));
}
if let Some(ids) = self.event_ids {
params.push(("eventIds", ids.join(",")));
}
if let Some(time) = self.commence_time_from {
params.push(("commenceTimeFrom", time.to_rfc3339()));
}
if let Some(time) = self.commence_time_to {
params.push(("commenceTimeTo", time.to_rfc3339()));
}
let url = self.client.build_url(
&format!("/v4/historical/sports/{}/events", self.sport),
¶ms,
)?;
self.client.get(url).await
}
}
#[derive(Debug)]
pub struct GetHistoricalEventOddsRequest<'a> {
client: &'a TheOddsApiClient,
sport: String,
event_id: String,
date: Option<DateTime<Utc>>,
regions: Vec<Region>,
markets: Option<Vec<Market>>,
date_format: Option<DateFormat>,
odds_format: Option<OddsFormat>,
include_multipliers: Option<bool>,
}
impl<'a> GetHistoricalEventOddsRequest<'a> {
fn new(client: &'a TheOddsApiClient, sport: String, event_id: String) -> Self {
Self {
client,
sport,
event_id,
date: None,
regions: Vec::new(),
markets: None,
date_format: None,
odds_format: None,
include_multipliers: None,
}
}
pub fn date(mut self, date: DateTime<Utc>) -> Self {
self.date = Some(date);
self
}
pub fn regions(mut self, regions: &[Region]) -> Self {
self.regions = regions.to_vec();
self
}
pub fn region(mut self, region: Region) -> Self {
self.regions.push(region);
self
}
pub fn markets(mut self, markets: &[Market]) -> Self {
self.markets = Some(markets.to_vec());
self
}
pub fn date_format(mut self, format: DateFormat) -> Self {
self.date_format = Some(format);
self
}
pub fn odds_format(mut self, format: OddsFormat) -> Self {
self.odds_format = Some(format);
self
}
pub fn include_multipliers(mut self, include: bool) -> Self {
self.include_multipliers = Some(include);
self
}
pub async fn send(self) -> Result<Response<HistoricalResponse<EventOdds>>> {
let date = self.date.ok_or(Error::MissingParameter("date"))?;
if self.regions.is_empty() {
return Err(Error::MissingParameter("regions"));
}
let mut params = vec![
("date", date.to_rfc3339()),
("regions", format_csv(&self.regions)),
];
if let Some(markets) = self.markets {
params.push(("markets", format_csv(&markets)));
}
if let Some(fmt) = self.date_format {
params.push(("dateFormat", fmt.to_string()));
}
if let Some(fmt) = self.odds_format {
params.push(("oddsFormat", fmt.to_string()));
}
if let Some(true) = self.include_multipliers {
params.push(("includeMultipliers", "true".to_string()));
}
let url = self.client.build_url(
&format!(
"/v4/historical/sports/{}/events/{}/odds",
self.sport, self.event_id
),
¶ms,
)?;
self.client.get(url).await
}
}