use serde::{Deserialize, Deserializer};
mod serde_helpers {
use std::fmt;
use super::*;
pub fn null_as_default_string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
Option::<String>::deserialize(deserializer).map(|value| value.unwrap_or_default())
}
pub fn number_as_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
D: Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = i64;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an integer or floating point number")
}
fn visit_i64<E>(self, value: i64) -> Result<i64, E> {
Ok(value)
}
fn visit_u64<E>(self, value: u64) -> Result<i64, E>
where
E: serde::de::Error,
{
i64::try_from(value).map_err(serde::de::Error::custom)
}
fn visit_f64<E>(self, value: f64) -> Result<i64, E>
where
E: serde::de::Error,
{
if !value.is_finite() {
return Err(serde::de::Error::custom("non-finite number"));
}
Ok(value.round() as i64)
}
fn visit_none<E>(self) -> Result<i64, E> {
Ok(0)
}
fn visit_unit<E>(self) -> Result<i64, E> {
Ok(0)
}
}
deserializer.deserialize_any(Visitor)
}
pub fn optional_number_as_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
match value {
None | Some(serde_json::Value::Null) => Ok(None),
Some(serde_json::Value::Number(number)) => number
.as_f64()
.map(|value| Some(value.round() as i64))
.ok_or_else(|| serde::de::Error::custom("invalid number")),
Some(serde_json::Value::Array(items)) => items
.last()
.and_then(|value| value.as_f64())
.map(|value| Some(value.round() as i64))
.ok_or_else(|| serde::de::Error::custom("invalid number array")),
_ => Err(serde::de::Error::custom("expected number")),
}
}
pub fn optional_f64_flexible<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
Ok(match value {
None | Some(serde_json::Value::Null) => None,
Some(serde_json::Value::Number(number)) => number.as_f64(),
Some(serde_json::Value::Array(items)) => items
.last()
.and_then(|value| value.as_f64()),
_ => None,
})
}
pub fn optional_f64_points<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
where
D: Deserializer<'de>,
{
optional_f64_flexible(deserializer).map(|value| value.map(|points| points.round() as i64))
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct CarData {
pub brake: i64,
pub date: String,
pub driver_number: i64,
pub drs: i64,
pub meeting_key: i64,
pub n_gear: i64,
pub rpm: i64,
pub session_key: i64,
pub speed: i64,
pub throttle: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChampionshipDriver {
pub driver_number: i64,
pub meeting_key: i64,
#[serde(deserialize_with = "serde_helpers::number_as_i64")]
pub points_current: i64,
#[serde(deserialize_with = "serde_helpers::number_as_i64")]
pub points_start: i64,
#[serde(deserialize_with = "serde_helpers::number_as_i64")]
pub position_current: i64,
#[serde(deserialize_with = "serde_helpers::number_as_i64")]
pub position_start: i64,
pub session_key: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ChampionshipTeam {
pub meeting_key: i64,
#[serde(deserialize_with = "serde_helpers::number_as_i64")]
pub points_current: i64,
#[serde(deserialize_with = "serde_helpers::number_as_i64")]
pub points_start: i64,
#[serde(deserialize_with = "serde_helpers::number_as_i64")]
pub position_current: i64,
#[serde(deserialize_with = "serde_helpers::number_as_i64")]
pub position_start: i64,
pub session_key: i64,
pub team_name: String,
}
#[derive(Debug, Clone, Deserialize, serde::Serialize)]
pub struct Driver {
pub broadcast_name: String,
#[serde(default, deserialize_with = "serde_helpers::null_as_default_string")]
pub country_code: String,
pub driver_number: i64,
pub first_name: String,
pub full_name: String,
pub headshot_url: String,
pub last_name: String,
pub meeting_key: i64,
pub name_acronym: String,
pub session_key: i64,
pub team_colour: String,
pub team_name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Interval {
pub date: String,
pub driver_number: i64,
pub gap_to_leader: f64,
pub interval: f64,
pub meeting_key: i64,
pub session_key: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Lap {
pub date_start: String,
pub driver_number: i64,
pub duration_sector_1: f64,
pub duration_sector_2: f64,
pub duration_sector_3: f64,
pub i1_speed: i64,
pub i2_speed: i64,
pub is_pit_out_lap: bool,
pub lap_duration: f64,
pub lap_number: i64,
pub meeting_key: i64,
pub segments_sector_1: Vec<i64>,
pub segments_sector_2: Vec<i64>,
pub segments_sector_3: Vec<i64>,
pub session_key: i64,
pub st_speed: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Location {
pub date: String,
pub driver_number: i64,
pub meeting_key: i64,
pub session_key: i64,
pub x: i64,
pub y: i64,
pub z: i64,
}
#[derive(Debug, Clone, Deserialize, serde::Serialize)]
pub struct Meeting {
pub circuit_image: String,
pub circuit_info_url: String,
pub circuit_key: i64,
pub circuit_short_name: String,
pub circuit_type: String,
pub country_code: String,
pub country_flag: String,
pub country_key: i64,
pub country_name: String,
pub date_end: String,
pub date_start: String,
pub gmt_offset: String,
pub is_cancelled: bool,
pub location: String,
pub meeting_key: i64,
pub meeting_name: String,
pub meeting_official_name: String,
pub year: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Overtake {
pub date: String,
pub meeting_key: i64,
pub overtaken_driver_number: i64,
pub overtaking_driver_number: i64,
pub position: i64,
pub session_key: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Pit {
pub date: String,
pub driver_number: i64,
pub lane_duration: Option<f64>,
pub lap_number: i64,
pub meeting_key: i64,
pub pit_duration: Option<f64>,
pub session_key: i64,
pub stop_duration: Option<f64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Position {
pub date: String,
pub driver_number: i64,
pub meeting_key: i64,
pub position: i64,
pub session_key: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RaceControl {
pub category: String,
pub date: String,
pub driver_number: Option<i64>,
pub flag: Option<String>,
pub lap_number: Option<i64>,
pub meeting_key: i64,
pub message: String,
pub qualifying_phase: Option<i64>,
pub scope: Option<String>,
pub sector: Option<i64>,
pub session_key: i64,
}
#[derive(Debug, Clone, Deserialize, serde::Serialize)]
pub struct Session {
pub circuit_key: i64,
pub circuit_short_name: String,
pub country_code: String,
pub country_key: i64,
pub country_name: String,
pub date_end: String,
pub date_start: String,
pub gmt_offset: String,
pub is_cancelled: bool,
pub location: String,
pub meeting_key: i64,
pub session_key: i64,
pub session_name: String,
pub session_type: String,
pub year: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SessionResult {
pub dnf: bool,
pub dns: bool,
pub driver_number: i64,
pub dsq: bool,
#[serde(default, deserialize_with = "serde_helpers::optional_f64_flexible")]
pub duration: Option<f64>,
#[serde(default, deserialize_with = "serde_helpers::optional_f64_flexible")]
pub gap_to_leader: Option<f64>,
pub meeting_key: i64,
#[serde(default, deserialize_with = "serde_helpers::number_as_i64")]
pub number_of_laps: i64,
#[serde(default, deserialize_with = "serde_helpers::optional_number_as_i64")]
pub position: Option<i64>,
pub session_key: i64,
#[serde(default, deserialize_with = "serde_helpers::optional_f64_points")]
pub points: Option<i64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct StartingGrid {
pub driver_number: i64,
pub lap_duration: Option<f64>,
pub meeting_key: i64,
pub position: i64,
pub session_key: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Stint {
pub compound: String,
pub driver_number: i64,
pub lap_end: i64,
pub lap_start: i64,
pub meeting_key: i64,
pub session_key: i64,
pub stint_number: i64,
pub tyre_age_at_start: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct TeamRadio {
pub date: String,
pub driver_number: i64,
pub meeting_key: i64,
pub recording_url: String,
pub session_key: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Weather {
pub air_temperature: f64,
pub date: String,
#[serde(deserialize_with = "serde_helpers::number_as_i64")]
pub humidity: i64,
pub meeting_key: i64,
pub pressure: f64,
#[serde(deserialize_with = "serde_helpers::number_as_i64")]
pub rainfall: i64,
pub session_key: i64,
pub track_temperature: f64,
#[serde(deserialize_with = "serde_helpers::number_as_i64")]
pub wind_direction: i64,
pub wind_speed: f64,
}
#[cfg(test)]
mod tests {
use super::{Driver, Weather};
#[test]
fn weather_deserializes_fractional_humidity() {
let raw = r#"{
"date": "2023-09-16T12:45:18.528000+00:00",
"session_key": 9161,
"pressure": 1009.2,
"track_temperature": 34.8,
"rainfall": 0,
"wind_speed": 1.7,
"meeting_key": 1219,
"wind_direction": 193,
"humidity": 64.7,
"air_temperature": 29.6
}"#;
let weather: Weather = serde_json::from_str(raw).expect("deserialize weather");
assert_eq!(weather.humidity, 65);
}
#[test]
fn driver_deserializes_null_country_code() {
let raw = r#"{
"broadcast_name": "L NORRIS",
"country_code": null,
"driver_number": 1,
"first_name": "Lando",
"full_name": "Lando NORRIS",
"headshot_url": "https://example.com/nor.png",
"last_name": "Norris",
"meeting_key": 1286,
"name_acronym": "NOR",
"session_key": 11299,
"team_colour": "F47600",
"team_name": "McLaren"
}"#;
let driver: Driver = serde_json::from_str(raw).expect("deserialize driver");
assert_eq!(driver.country_code, "");
assert_eq!(driver.driver_number, 1);
}
}