ergast-rs 0.3.2

An async client for getting Formula 1 schedules, qualifying, and race results powered by the Ergast API
Documentation
use std::fmt;
use std::fmt::{Display, Formatter};

const DEFAULT_SCHEMA: &str = "http";
const DEFAULT_LIMIT: u32 = 30;
const DEFAULT_OFFSET: u32 = 0;
const CURRENT_SEASON: &str = "current";
const FIRST_ROUND: &str = "first";
const LAST_ROUND: &str = "last";

pub type Request = String;

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum RequestType {
    Seasons,
    Circuit,
    Schedule,
    Constructors,
    Drivers,
    QualifyingResult,
    SprintResult,
    RaceResult,
    DriverStanding,
    ConstructorStanding,
    FinishingStatus,
    LapTimes,
    PitStops,
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum RequestParameter {
    Season(u32),
    CurrentSeason,
    Round(u32),
    LastRound,
    FirstRound,
    Id(String),
    Circuit(String),
    Driver(String),
    DriverStanding(u32),
    Constructor(String),
    ConstructorStanding(u32),
    FinishingPosition(u32),
    FinishingStatus(String),
    Grid(u32),
    RaceResult(u32),
    SprintResult(u32),
    FastestLap(Option<u32>),
    Lap(u32),
    PitStop(u32),
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct RequestBuilder {
    protocol: String,
    selection: Option<RequestType>,
    season: String,
    round: Option<String>,
    id: Option<String>,
    criteria: Vec<RequestParameter>,
    limit: u32,
    offset: u32,
}

impl Display for RequestType {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let str = match self {
            RequestType::Seasons => "seasons",
            RequestType::Circuit => "circuits",
            RequestType::Schedule => "races",
            RequestType::Constructors => "constructors",
            RequestType::Drivers => "drivers",
            RequestType::QualifyingResult => "qualifying",
            RequestType::SprintResult => "sprint",
            RequestType::RaceResult => "results",
            RequestType::DriverStanding => "driverStandings",
            RequestType::ConstructorStanding => "constructorStandings",
            RequestType::FinishingStatus => "status",
            RequestType::LapTimes => "laps",
            RequestType::PitStops => "pitstops",
        };

        write!(f, "{}", str)
    }
}

impl Display for RequestParameter {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            RequestParameter::Season(s) => write!(f, "{}", s),
            RequestParameter::CurrentSeason => write!(f, "{}", CURRENT_SEASON),
            RequestParameter::Round(r) => write!(f, "{}", r),
            RequestParameter::LastRound => write!(f, "{}", LAST_ROUND),
            RequestParameter::FirstRound => write!(f, "{}", FIRST_ROUND),
            RequestParameter::Id(id) => write!(f, "{}", id),
            RequestParameter::Circuit(c) => write!(f, "circuits/{}", c),
            RequestParameter::Driver(d) => write!(f, "drivers/{}", d),
            RequestParameter::DriverStanding(s) => write!(f, "driverStandings/{}", s),
            RequestParameter::Constructor(c) => write!(f, "constructors/{}", c),
            RequestParameter::ConstructorStanding(s) => write!(f, "constructorStandings/{}", s),
            RequestParameter::FinishingPosition(p) => write!(f, "{}", p),
            RequestParameter::FinishingStatus(s) => write!(f, "status/{}", s),
            RequestParameter::Grid(g) => write!(f, "grid/{}", g),
            RequestParameter::RaceResult(r) => write!(f, "results/{}", r),
            RequestParameter::SprintResult(s) => write!(f, "sprints/{}", s),
            RequestParameter::FastestLap(l) => {
                let r = if let Some(rank) = l { rank } else { &1u32 };
                write!(f, "fastest/{}", r)
            }
            RequestParameter::Lap(l) => write!(f, "laps/{}", l),
            RequestParameter::PitStop(p) => write!(f, "{}", p),
        }
    }
}

impl Default for RequestBuilder {
    fn default() -> Self {
        Self::new()
    }
}

impl RequestBuilder {
    /// Create a new (minimal) request: http://ergast.com/api/f1/current
    pub fn new() -> Self {
        RequestBuilder {
            protocol: String::from(DEFAULT_SCHEMA),
            selection: None,
            season: String::from(CURRENT_SEASON),
            round: None,
            id: None,
            criteria: Vec::new(),
            limit: DEFAULT_LIMIT,
            offset: DEFAULT_OFFSET,
        }
    }

    /// Specify what information to query
    pub fn query(mut self, query: RequestType) -> RequestBuilder {
        if (query == RequestType::PitStops || query == RequestType::LapTimes)
            && self.round.is_none()
        {
            self.round = Some(LAST_ROUND.to_string())
        }

        self.selection = Some(query);
        self
    }

    /// Add a list of criteria
    pub fn add_parameter(mut self, param: RequestParameter) -> RequestBuilder {
        match &param {
            RequestParameter::Season(s) => {
                if *s == 0 {
                    self.season = String::from(CURRENT_SEASON);
                } else {
                    self.season = s.to_string();
                }
            }
            RequestParameter::CurrentSeason => {
                self.season = String::from(CURRENT_SEASON);
            }
            RequestParameter::Round(r) => {
                if *r == 0 {
                    self.round = Some(String::from(LAST_ROUND));
                } else {
                    self.round = Some(r.to_string());
                }
            }
            RequestParameter::FirstRound => self.round = Some(String::from(FIRST_ROUND)),
            RequestParameter::LastRound => self.round = Some(String::from(LAST_ROUND)),
            RequestParameter::Lap(l) => {
                if self.selection == Some(RequestType::LapTimes) {
                    self.id = Some(l.to_string())
                } else {
                    self.criteria.push(param);
                }
            }
            RequestParameter::FinishingStatus(s) => {
                if self.selection == Some(RequestType::FinishingStatus) {
                    self.id = Some(s.to_owned())
                } else {
                    self.criteria.push(param);
                }
            }
            RequestParameter::PitStop(p) => {
                if self.selection == Some(RequestType::PitStops) {
                    self.id = Some(p.to_string())
                } else {
                    self.criteria.push(param);
                }
            }
            _ => self.criteria.push(param),
        }

        self
    }

    /// Add a query criteria
    pub fn add_parameters(mut self, params: Vec<RequestParameter>) -> RequestBuilder {
        for param in params {
            self = self.add_parameter(param);
        }
        self
    }

    /// Specify the Protocol (default: http)
    pub fn protocol(mut self, protocol: String) -> RequestBuilder {
        self.protocol = protocol;
        self
    }

    /// Set the Result count limit (default: 30, max: 1000)
    pub fn limit(mut self, limit: u32) -> RequestBuilder {
        if limit > 1000 {
            self.limit = 1000;
            return self;
        }

        self.limit = limit;
        self
    }

    /// Set the result offset (default: 0)
    pub fn offset(mut self, offset: u32) -> RequestBuilder {
        self.offset = offset;
        self
    }

    /// Build the Request String
    pub fn build(self) -> Request {
        format!("{protocol}://ergast.com/api/f1/{season}{round}{criteria}{select}{id}.json?{limit},{offset}",
                protocol = self.protocol,
                season = self.season,
                round = if let Some(round) = &self.round { format!("/{}", round) } else { "".to_string() },
                criteria = self.build_criteria(),
                select = if let Some(selection) = &self.selection { format!("/{}", selection) } else { "".to_string() },
                id = if let Some(id) = &self.id { format!("/{}", id) } else { "".to_string() },
                limit = self.limit,
                offset = self.offset
        )
    }

    fn build_criteria(&self) -> String {
        if self.criteria.is_empty() {
            return String::new();
        }

        let mut str = String::new();
        for param in &self.criteria {
            str.push('/');
            str.push_str(param.to_string().as_str());
        }

        str
    }
}

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

    #[test]
    fn test_minimal() {
        let expected = format!(
            "http://ergast.com/api/f1/current.json?{},{}",
            DEFAULT_LIMIT, DEFAULT_OFFSET
        );

        let request = RequestBuilder::new();
        assert_eq!(expected, request.build());
    }

    #[test]
    fn test_selection() {
        let expected = format!(
            "http://ergast.com/api/f1/current/results.json?{},{}",
            DEFAULT_LIMIT, DEFAULT_OFFSET
        );

        let request = RequestBuilder::new().query(RequestType::RaceResult);
        assert_eq!(expected, request.build());
    }

    #[test]
    fn test_one_criteria_no_selection() {
        let expected = format!(
            "http://ergast.com/api/f1/current/drivers/verstappen.json?{},{}",
            DEFAULT_LIMIT, DEFAULT_OFFSET
        );

        let request = RequestBuilder::new()
            .add_parameter(RequestParameter::Driver(String::from("verstappen")));
        assert_eq!(expected, request.build());
    }

    #[test]
    fn test_multiple_criteria_no_selection() {
        let expected = format!(
            "http://ergast.com/api/f1/current/drivers/max_verstappen/grid/1.json?{},{}",
            DEFAULT_LIMIT, DEFAULT_OFFSET
        );

        let request = RequestBuilder::new()
            .add_parameter(RequestParameter::Driver(String::from("max_verstappen")))
            .add_parameter(RequestParameter::Grid(1));
        assert_eq!(expected, request.build());
    }

    #[test]
    fn test_one_criteria_with_season() {
        let expected = format!(
            "http://ergast.com/api/f1/2022/drivers/max_verstappen.json?{},{}",
            DEFAULT_LIMIT, DEFAULT_OFFSET
        );

        let request = RequestBuilder::new()
            .add_parameter(RequestParameter::Season(2022))
            .add_parameter(RequestParameter::Driver(String::from("max_verstappen")));
        assert_eq!(expected, request.build());
    }

    #[test]
    fn test_one_criteria_with_round() {
        let expected = format!(
            "http://ergast.com/api/f1/current/20/drivers/max_verstappen.json?{},{}",
            DEFAULT_LIMIT, DEFAULT_OFFSET
        );

        let request = RequestBuilder::new()
            .add_parameter(RequestParameter::Round(20))
            .add_parameter(RequestParameter::Driver(String::from("max_verstappen")));
        assert_eq!(expected, request.build());
    }

    #[test]
    fn test_pit_selection_with_id() {
        let expected = format!(
            "http://ergast.com/api/f1/current/last/pitstops/2.json?{},{}",
            DEFAULT_LIMIT, DEFAULT_OFFSET
        );

        let request = RequestBuilder::new()
            .query(RequestType::PitStops)
            .add_parameter(RequestParameter::PitStop(2));
        assert_eq!(expected, request.build());
    }
}