ergast-rs 0.3.2

An async client for getting Formula 1 schedules, qualifying, and race results powered by the Ergast API
Documentation
use async_trait::async_trait;
use eyre::{Context, ContextCompat, Result};
use reqwest::Client;

use crate::apis::request::{RequestBuilder, RequestParameter, RequestType};
use crate::apis::{race_table::RaceTable, request::Request, response::Response};

#[async_trait]
pub trait Ergast {
    /// Main method to query the Ergast API
    /// Use a request build via the api::request::RequestBuilder or create one by hand
    async fn query(&self, request: Request) -> Result<RaceTable>;

    /// Easy to use method to query the race schedule without further query criteria
    ///
    /// Get the race schedule (including all sub-events) and other related information for either a
    /// specific season or the current one.
    ///
    /// For example, to get the race schedule fo the current season
    ///
    /// ```
    /// # use eyre::Result;
    /// # use ergast_rs::{Ergast, ErgastMock as ErgastClient};
    /// # #[tokio::main(flavor = "current_thread")]
    /// # async fn main() -> Result<()> {
    /// let client = ErgastClient::new()?;
    /// let race_results = client
    ///     .schedule(None)
    ///     .await?;
    /// #    Ok(())
    /// # }
    /// ```
    async fn schedule(&self, season: Option<u32>) -> Result<RaceTable>;

    /// Easy to use method to query only race results without further query criteria
    ///
    /// Get the race results for a specific season (otherwise the current season will be used),
    /// and a round (otherwise the last round will be used).
    ///
    /// For example, to get the results of the Sakhir Grand Prix in the 2020 season (when Checo
    /// won!)
    ///
    /// ```
    /// # use eyre::Result;
    /// # use ergast_rs::{Ergast, ErgastMock as ErgastClient};
    /// # #[tokio::main(flavor = "current_thread")]
    /// # async fn main() -> Result<()> {
    /// let client = ErgastClient::new()?;
    /// let race_results = client
    ///     .race_results(Some(2020), Some(16))
    ///     .await?;
    /// #    Ok(())
    /// # }
    /// ```
    async fn race_results(&self, season: Option<u32>, round: Option<u32>) -> Result<RaceTable>;

    /// Easy to use method to query only sprint results without further query criteria
    ///
    /// Get the sprint results for a specific season (otherwise the current season will be used),
    /// and a round (otherwise the last round will be used).
    ///
    /// For example, to get the sprint results of the British Grand Prix (10th round) of the
    /// 2021 season (when Verstappen won)
    ///
    /// ```
    /// # use eyre::Result;
    /// # use ergast_rs::{Ergast, ErgastMock as ErgastClient};
    /// # #[tokio::main(flavor = "current_thread")]
    /// # async fn main() -> Result<()> {
    /// let client = ErgastClient::new()?;
    /// let sprint_result = client
    ///     .sprint_results(Some(2021), Some(10))
    ///     .await?;
    /// #    Ok(())
    /// # }
    /// ```
    async fn sprint_results(&self, season: Option<u32>, round: Option<u32>) -> Result<RaceTable>;

    /// Easy to use method to query only qualifying results without further query criteria
    ///
    /// Get the qualifying results for a specific season (otherwise the current season will be used),
    /// and a round (otherwise the last round will be used).
    ///
    /// For example, to get the qualifying results of the Saudi Arabian Grand Prix (2nd race) of the
    /// 2022 season (when Checo was on pole!)
    ///
    /// ```
    /// # use eyre::Result;
    /// # use ergast_rs::{Ergast, ErgastMock as ErgastClient};
    /// # #[tokio::main(flavor = "current_thread")]
    /// # async fn main() -> Result<()> {
    /// let client = ErgastClient::new()?;
    /// let qualifying_results = client
    ///     .qualifying_results(Some(2022), Some(2))
    ///     .await?;
    /// #    Ok(())
    /// # }
    /// ```
    async fn qualifying_results(
        &self,
        season: Option<u32>,
        round: Option<u32>,
    ) -> Result<RaceTable>;
}

/// An asynchronous client which is used to fetch Formula 1 schedules and results.
pub struct ErgastClient {
    client: Client,
}

#[async_trait]
impl Ergast for ErgastClient {
    async fn query(&self, request: Request) -> Result<RaceTable> {
        let data = self
            .client
            .get(request)
            .send()
            .await
            .wrap_err("Failed to make request")?
            .json::<Response>()
            .await
            .wrap_err("Failed to parse the JSON response")?
            .data;

        let races = data.race_table.wrap_err("didn't find the race table")?;

        Ok(races)
    }

    async fn schedule(&self, season: Option<u32>) -> Result<RaceTable> {
        let request = RequestBuilder::new()
            .query(RequestType::Schedule)
            .add_parameter(if let Some(s) = season {
                RequestParameter::Season(s)
            } else {
                RequestParameter::CurrentSeason
            })
            .build();

        self.query(request).await
    }

    async fn race_results(&self, season: Option<u32>, round: Option<u32>) -> Result<RaceTable> {
        let request = RequestBuilder::new()
            .query(RequestType::RaceResult)
            .add_parameter(if let Some(s) = season {
                RequestParameter::Season(s)
            } else {
                RequestParameter::CurrentSeason
            })
            .add_parameter(if let Some(r) = round {
                RequestParameter::Round(r)
            } else {
                RequestParameter::LastRound
            })
            .build();

        self.query(request).await
    }

    async fn sprint_results(&self, season: Option<u32>, round: Option<u32>) -> Result<RaceTable> {
        let request = RequestBuilder::new()
            .query(RequestType::SprintResult)
            .add_parameter(if let Some(s) = season {
                RequestParameter::Season(s)
            } else {
                RequestParameter::CurrentSeason
            })
            .add_parameter(if let Some(r) = round {
                RequestParameter::Round(r)
            } else {
                RequestParameter::LastRound
            })
            .build();

        self.query(request).await
    }

    async fn qualifying_results(
        &self,
        season: Option<u32>,
        round: Option<u32>,
    ) -> Result<RaceTable> {
        let request = RequestBuilder::new()
            .query(RequestType::QualifyingResult)
            .add_parameter(if let Some(s) = season {
                RequestParameter::Season(s)
            } else {
                RequestParameter::CurrentSeason
            })
            .add_parameter(if let Some(r) = round {
                RequestParameter::Round(r)
            } else {
                RequestParameter::LastRound
            })
            .build();

        self.query(request).await
    }
}

impl ErgastClient {
    /// Builds a new client, can fail if the underlying HTTP client fails to build.
    #[allow(dead_code)]
    pub fn new() -> Result<Self> {
        let client = Client::builder()
            .build()
            .wrap_err("Failed to build client")?;
        Ok(Self { client })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn smokecheck_fetch_schedule_without_practice_time() {
        let client = ErgastClient::new().expect("failed to build client");
        let races = client
            .schedule(Some(2021))
            .await
            .expect("failed to get races");
        dbg!(&races);

        assert_eq!(races.races.len(), 22);
    }

    #[tokio::test]
    async fn smokecheck_fetch_schedule_with_practice_time() {
        let client = ErgastClient::new().expect("failed to build client");
        let races = client
            .schedule(Some(2022))
            .await
            .expect("failed to get races");
        dbg!(&races);

        assert_eq!(races.races.len(), 22);
    }

    #[tokio::test]
    async fn smokecheck_fetch_qualifying_results() {
        let client = ErgastClient::new().expect("failed to build client");
        let qualifying = client
            .qualifying_results(Some(2021), Some(1))
            .await
            .expect("failed to get qualifying results");
        dbg!(&qualifying);
    }

    #[tokio::test]
    async fn smokecheck_fetch_latest_qualifying_results() {
        let client = ErgastClient::new().expect("failed to build client");
        let qualifying = client
            .qualifying_results(None, None)
            .await
            .expect("failed to get qualifying results");
        dbg!(&qualifying);
    }

    #[tokio::test]
    async fn smokecheck_fetch_sprint_results() {
        let client = ErgastClient::new().expect("failed to build client");
        let qualifying = client
            .sprint_results(Some(2021), Some(10))
            .await
            .expect("failed to get qualifying results");
        dbg!(&qualifying);
    }

    #[tokio::test]
    async fn smokecheck_fetch_race_results() {
        let client = ErgastClient::new().expect("failed to build client");
        let race_results = client
            .race_results(Some(2021), Some(1))
            .await
            .expect("failed to get race results");
        dbg!(&race_results);
    }

    #[tokio::test]
    async fn smokecheck_fetch_latest_race_results() {
        let client = ErgastClient::new().expect("failed to build client");
        let race_results = client
            .race_results(None, None)
            .await
            .expect("failed to get race results");
        dbg!(&race_results);
    }
}