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,
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 {
#[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 {
#[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 {
#[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>,
}
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)
}
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();
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);
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,
}
}
}