ergast-rs 0.3.2

An async client for getting Formula 1 schedules, qualifying, and race results powered by the Ergast API
Documentation
use eyre::Result;
use serde::{Deserialize, Deserializer};
use serde_aux::field_attributes::{
    deserialize_number_from_string, deserialize_option_number_from_string,
};
use time::{macros::format_description, Date, OffsetDateTime, Time};

#[derive(Debug, Deserialize)]
pub struct RaceTable {
    pub season: String,
    #[serde(rename = "Races")]
    pub races: Vec<Race>,
}

impl RaceTable {
    pub fn next_race(&self) -> Option<&Race> {
        let today = OffsetDateTime::now_utc().date();
        self.next_race_with_today(today)
    }

    fn next_race_with_today(&self, today: Date) -> Option<&Race> {
        self.races.iter().find(|r| r.date >= today)
    }
}

#[derive(Debug, Deserialize, PartialEq, PartialOrd, Clone)]
pub struct Race {
    pub season: String,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub round: u32,
    #[serde(rename = "raceName")]
    pub name: String,
    pub date: Date,
    #[serde(deserialize_with = "deserialize_time")]
    pub time: Time,
    #[serde(rename = "Circuit")]
    pub circuit: Circuit,
    #[serde(rename = "FirstPractice")]
    pub first_practice: Option<SessionInfo>,
    #[serde(rename = "SecondPractice")]
    pub second_practice: Option<SessionInfo>,
    #[serde(rename = "ThirdPractice")]
    pub third_practice: Option<SessionInfo>,
    #[serde(rename = "Qualifying")]
    pub qualifying: Option<SessionInfo>,
    #[serde(rename = "Sprint")]
    pub sprint_date: Option<SessionInfo>,
    #[serde(rename = "QualifyingResults")]
    pub qualifying_results: Option<Vec<QualifyingResult>>,
    #[serde(rename = "SprintResults")]
    pub sprint_results: Option<Vec<SprintResult>>,
    #[serde(rename = "Results")]
    pub race_results: Option<Vec<RaceResult>>,
}

#[derive(Debug, Deserialize, PartialEq, PartialOrd, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SessionInfo {
    pub date: Date,
    #[serde(default, deserialize_with = "deserialize_optional_time")]
    pub time: Option<Time>,
}

#[derive(Debug, Deserialize, PartialEq, PartialOrd, Eq, Clone)]
#[cfg_attr(test, derive(Default))]
#[serde(rename_all = "camelCase")]
pub struct Circuit {
    pub circuit_id: String,
    pub url: String,
    pub circuit_name: String,
    #[serde(rename = "Location")]
    pub location: CircuitLocation,
}

#[derive(Debug, Deserialize, PartialEq, PartialOrd, Eq, Clone)]
#[cfg_attr(test, derive(Default))]
pub struct CircuitLocation {
    pub lat: String,
    pub long: String,
    pub locality: String,
    pub country: String,
}

#[derive(Debug, Deserialize, PartialEq, PartialOrd, Eq, Clone)]
#[cfg_attr(test, derive(Default))]
#[serde(rename_all = "camelCase")]
pub struct Driver {
    pub driver_id: String,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub permanent_number: u32,
    /// Three letter code used as the shortened name
    pub code: String,
    pub url: String,
    pub given_name: String,
    pub family_name: String,
    pub date_of_birth: String,
    pub nationality: String,
}

#[derive(Debug, Deserialize, PartialEq, PartialOrd, Eq, Clone)]
#[cfg_attr(test, derive(Default))]
#[serde(rename_all = "camelCase")]
pub struct Constructor {
    pub constructor_id: String,
    pub url: String,
    pub name: String,
    pub nationality: String,
}

#[derive(Debug, Deserialize, PartialEq, PartialOrd, Eq, Clone)]
#[cfg_attr(test, derive(Default))]
pub struct QualifyingResult {
    /// Driver number
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub number: u32,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub position: u32,
    #[serde(rename = "Driver")]
    pub driver: Driver,
    #[serde(rename = "Constructor")]
    pub constructor: Constructor,
    #[serde(rename = "Q1")]
    pub q1: Option<String>,
    #[serde(rename = "Q2")]
    pub q2: Option<String>,
    #[serde(rename = "Q3")]
    pub q3: Option<String>,
}

#[derive(Debug, Deserialize, PartialEq, PartialOrd, Clone)]
#[cfg_attr(test, derive(Default))]
#[serde(rename_all = "camelCase")]
pub struct SprintResult {
    /// Driver number
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub number: u32,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub position: u32,
    pub position_text: String,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub points: f32,
    #[serde(rename = "Driver")]
    pub driver: Driver,
    #[serde(rename = "Constructor")]
    pub constructor: Constructor,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub grid: u32,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub laps: u32,
    pub status: String,
    #[serde(rename = "Time")]
    pub race_time: Option<RaceTime>,
    #[serde(rename = "FastestLap")]
    pub fastest_lap_info: Option<FastestLapInfo>,
}

#[derive(Debug, Deserialize, PartialEq, PartialOrd, Clone)]
#[cfg_attr(test, derive(Default))]
#[serde(rename = "Results")]
#[serde(rename_all = "camelCase")]
pub struct RaceResult {
    /// Driver number
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub number: u32,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub position: u32,
    pub position_text: String,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub points: f32,
    #[serde(rename = "Driver")]
    pub driver: Driver,
    #[serde(rename = "Constructor")]
    pub constructor: Constructor,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub grid: u32,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    pub laps: u32,
    pub status: String,
    #[serde(rename = "Time")]
    pub race_time: Option<RaceTime>,
    #[serde(rename = "FastestLap")]
    pub fastest_lap_info: Option<FastestLapInfo>,
}

#[derive(Debug, Deserialize, PartialEq, PartialOrd, Ord, Eq, Clone)]
#[cfg_attr(test, derive(Default))]
#[serde(rename = "Time")]
pub struct RaceTime {
    millis: Option<String>,
    time: String,
}

#[derive(Debug, Deserialize, PartialEq, PartialOrd, Ord, Eq, Clone)]
#[cfg_attr(test, derive(Default))]
pub struct FastestLapInfo {
    #[serde(default, deserialize_with = "deserialize_option_number_from_string")]
    rank: Option<u32>,
    #[serde(deserialize_with = "deserialize_number_from_string")]
    lap: u32,
    #[serde(rename = "Time")]
    time: Option<RaceTime>,
}

/// Helper function to parse the times returned by Ergast. In particular they add the Z UTC
/// suffix, so we ignore it since our time representation is locale agnostic.
fn deserialize_time<'de, D>(deserializer: D) -> Result<Time, D::Error>
where
    D: Deserializer<'de>,
{
    let as_str = String::deserialize(deserializer)?;
    let format = format_description!("[hour repr:24]:[minute]:[second]Z");
    Time::parse(&as_str, &format).map_err(serde::de::Error::custom)
}

/// Helper function to parse the optional times returned by Ergast.
fn deserialize_optional_time<'de, D>(deserializer: D) -> Result<Option<Time>, D::Error>
where
    D: Deserializer<'de>,
{
    let format = format_description!("[hour repr:24]:[minute]:[second]Z");

    #[derive(Deserialize)]
    #[serde(untagged)]
    enum TimeOrNull<'a> {
        Str(&'a str),
        Null,
    }

    match TimeOrNull::deserialize(deserializer)? {
        TimeOrNull::Str(as_str) => match as_str {
            "" => Ok(None),
            _ => Time::parse(as_str, &format)
                .map(Some)
                .map_err(serde::de::Error::custom),
        },
        TimeOrNull::Null => Ok(None),
    }
}

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

    #[test]
    fn test_next_race() {
        let today_date = OffsetDateTime::now_utc().date();

        // create 4 races
        let yesterday = today_date - time::Duration::DAY;
        let before_yesterday = yesterday - time::Duration::DAY;
        let tomorrow = today_date + time::Duration::DAY;
        let after_tomorrow = tomorrow + time::Duration::DAY;

        let before_yesterday = mock_race(before_yesterday);
        let yesterday = mock_race(yesterday);
        let mut tomorrow = mock_race(tomorrow);
        let after_tomorrow = mock_race(after_tomorrow);

        // modify the name as this is the one we expect to be next
        tomorrow.name = "THIS IS THE ONE".to_string();

        let races = vec![
            before_yesterday,
            yesterday,
            tomorrow.clone(),
            after_tomorrow,
        ];
        let race_table = RaceTable {
            season: "mock-season".to_string(),
            races,
        };

        let next_race = race_table.next_race_with_today(today_date);
        assert_eq!(Some(&tomorrow), next_race)
    }

    fn mock_race(date: Date) -> Race {
        Race {
            season: "mock-season".to_string(),
            round: 42,
            name: "mock-race".to_string(),
            date,
            time: Time::MIDNIGHT,
            circuit: Default::default(),
            first_practice: None,
            second_practice: None,
            third_practice: None,
            qualifying: None,
            sprint_date: None,
            qualifying_results: None,
            sprint_results: None,
            race_results: None,
        }
    }
}