Skip to main content

ergast_rs/apis/
request.rs

1use std::fmt;
2use std::fmt::{Display, Formatter};
3
4const DEFAULT_SCHEMA: &str = "http";
5const DEFAULT_LIMIT: u32 = 30;
6const DEFAULT_OFFSET: u32 = 0;
7const CURRENT_SEASON: &str = "current";
8const FIRST_ROUND: &str = "first";
9const LAST_ROUND: &str = "last";
10
11pub type Request = String;
12
13#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
14pub enum RequestType {
15    Seasons,
16    Circuit,
17    Schedule,
18    Constructors,
19    Drivers,
20    QualifyingResult,
21    SprintResult,
22    RaceResult,
23    DriverStanding,
24    ConstructorStanding,
25    FinishingStatus,
26    LapTimes,
27    PitStops,
28}
29
30#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
31pub enum RequestParameter {
32    Season(u32),
33    CurrentSeason,
34    Round(u32),
35    LastRound,
36    FirstRound,
37    Id(String),
38    Circuit(String),
39    Driver(String),
40    DriverStanding(u32),
41    Constructor(String),
42    ConstructorStanding(u32),
43    FinishingPosition(u32),
44    FinishingStatus(String),
45    Grid(u32),
46    RaceResult(u32),
47    SprintResult(u32),
48    FastestLap(Option<u32>),
49    Lap(u32),
50    PitStop(u32),
51}
52
53#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
54pub struct RequestBuilder {
55    protocol: String,
56    selection: Option<RequestType>,
57    season: String,
58    round: Option<String>,
59    id: Option<String>,
60    criteria: Vec<RequestParameter>,
61    limit: u32,
62    offset: u32,
63}
64
65impl Display for RequestType {
66    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
67        let str = match self {
68            RequestType::Seasons => "seasons",
69            RequestType::Circuit => "circuits",
70            RequestType::Schedule => "races",
71            RequestType::Constructors => "constructors",
72            RequestType::Drivers => "drivers",
73            RequestType::QualifyingResult => "qualifying",
74            RequestType::SprintResult => "sprint",
75            RequestType::RaceResult => "results",
76            RequestType::DriverStanding => "driverStandings",
77            RequestType::ConstructorStanding => "constructorStandings",
78            RequestType::FinishingStatus => "status",
79            RequestType::LapTimes => "laps",
80            RequestType::PitStops => "pitstops",
81        };
82
83        write!(f, "{}", str)
84    }
85}
86
87impl Display for RequestParameter {
88    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
89        match self {
90            RequestParameter::Season(s) => write!(f, "{}", s),
91            RequestParameter::CurrentSeason => write!(f, "{}", CURRENT_SEASON),
92            RequestParameter::Round(r) => write!(f, "{}", r),
93            RequestParameter::LastRound => write!(f, "{}", LAST_ROUND),
94            RequestParameter::FirstRound => write!(f, "{}", FIRST_ROUND),
95            RequestParameter::Id(id) => write!(f, "{}", id),
96            RequestParameter::Circuit(c) => write!(f, "circuits/{}", c),
97            RequestParameter::Driver(d) => write!(f, "drivers/{}", d),
98            RequestParameter::DriverStanding(s) => write!(f, "driverStandings/{}", s),
99            RequestParameter::Constructor(c) => write!(f, "constructors/{}", c),
100            RequestParameter::ConstructorStanding(s) => write!(f, "constructorStandings/{}", s),
101            RequestParameter::FinishingPosition(p) => write!(f, "{}", p),
102            RequestParameter::FinishingStatus(s) => write!(f, "status/{}", s),
103            RequestParameter::Grid(g) => write!(f, "grid/{}", g),
104            RequestParameter::RaceResult(r) => write!(f, "results/{}", r),
105            RequestParameter::SprintResult(s) => write!(f, "sprints/{}", s),
106            RequestParameter::FastestLap(l) => {
107                let r = if let Some(rank) = l { rank } else { &1u32 };
108                write!(f, "fastest/{}", r)
109            }
110            RequestParameter::Lap(l) => write!(f, "laps/{}", l),
111            RequestParameter::PitStop(p) => write!(f, "{}", p),
112        }
113    }
114}
115
116impl Default for RequestBuilder {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl RequestBuilder {
123    /// Create a new (minimal) request: http://ergast.com/api/f1/current
124    pub fn new() -> Self {
125        RequestBuilder {
126            protocol: String::from(DEFAULT_SCHEMA),
127            selection: None,
128            season: String::from(CURRENT_SEASON),
129            round: None,
130            id: None,
131            criteria: Vec::new(),
132            limit: DEFAULT_LIMIT,
133            offset: DEFAULT_OFFSET,
134        }
135    }
136
137    /// Specify what information to query
138    pub fn query(mut self, query: RequestType) -> RequestBuilder {
139        if (query == RequestType::PitStops || query == RequestType::LapTimes)
140            && self.round.is_none()
141        {
142            self.round = Some(LAST_ROUND.to_string())
143        }
144
145        self.selection = Some(query);
146        self
147    }
148
149    /// Add a list of criteria
150    pub fn add_parameter(mut self, param: RequestParameter) -> RequestBuilder {
151        match &param {
152            RequestParameter::Season(s) => {
153                if *s == 0 {
154                    self.season = String::from(CURRENT_SEASON);
155                } else {
156                    self.season = s.to_string();
157                }
158            }
159            RequestParameter::CurrentSeason => {
160                self.season = String::from(CURRENT_SEASON);
161            }
162            RequestParameter::Round(r) => {
163                if *r == 0 {
164                    self.round = Some(String::from(LAST_ROUND));
165                } else {
166                    self.round = Some(r.to_string());
167                }
168            }
169            RequestParameter::FirstRound => self.round = Some(String::from(FIRST_ROUND)),
170            RequestParameter::LastRound => self.round = Some(String::from(LAST_ROUND)),
171            RequestParameter::Lap(l) => {
172                if self.selection == Some(RequestType::LapTimes) {
173                    self.id = Some(l.to_string())
174                } else {
175                    self.criteria.push(param);
176                }
177            }
178            RequestParameter::FinishingStatus(s) => {
179                if self.selection == Some(RequestType::FinishingStatus) {
180                    self.id = Some(s.to_owned())
181                } else {
182                    self.criteria.push(param);
183                }
184            }
185            RequestParameter::PitStop(p) => {
186                if self.selection == Some(RequestType::PitStops) {
187                    self.id = Some(p.to_string())
188                } else {
189                    self.criteria.push(param);
190                }
191            }
192            _ => self.criteria.push(param),
193        }
194
195        self
196    }
197
198    /// Add a query criteria
199    pub fn add_parameters(mut self, params: Vec<RequestParameter>) -> RequestBuilder {
200        for param in params {
201            self = self.add_parameter(param);
202        }
203        self
204    }
205
206    /// Specify the Protocol (default: http)
207    pub fn protocol(mut self, protocol: String) -> RequestBuilder {
208        self.protocol = protocol;
209        self
210    }
211
212    /// Set the Result count limit (default: 30, max: 1000)
213    pub fn limit(mut self, limit: u32) -> RequestBuilder {
214        if limit > 1000 {
215            self.limit = 1000;
216            return self;
217        }
218
219        self.limit = limit;
220        self
221    }
222
223    /// Set the result offset (default: 0)
224    pub fn offset(mut self, offset: u32) -> RequestBuilder {
225        self.offset = offset;
226        self
227    }
228
229    /// Build the Request String
230    pub fn build(self) -> Request {
231        format!("{protocol}://ergast.com/api/f1/{season}{round}{criteria}{select}{id}.json?{limit},{offset}",
232                protocol = self.protocol,
233                season = self.season,
234                round = if let Some(round) = &self.round { format!("/{}", round) } else { "".to_string() },
235                criteria = self.build_criteria(),
236                select = if let Some(selection) = &self.selection { format!("/{}", selection) } else { "".to_string() },
237                id = if let Some(id) = &self.id { format!("/{}", id) } else { "".to_string() },
238                limit = self.limit,
239                offset = self.offset
240        )
241    }
242
243    fn build_criteria(&self) -> String {
244        if self.criteria.is_empty() {
245            return String::new();
246        }
247
248        let mut str = String::new();
249        for param in &self.criteria {
250            str.push('/');
251            str.push_str(param.to_string().as_str());
252        }
253
254        str
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_minimal() {
264        let expected = format!(
265            "http://ergast.com/api/f1/current.json?{},{}",
266            DEFAULT_LIMIT, DEFAULT_OFFSET
267        );
268
269        let request = RequestBuilder::new();
270        assert_eq!(expected, request.build());
271    }
272
273    #[test]
274    fn test_selection() {
275        let expected = format!(
276            "http://ergast.com/api/f1/current/results.json?{},{}",
277            DEFAULT_LIMIT, DEFAULT_OFFSET
278        );
279
280        let request = RequestBuilder::new().query(RequestType::RaceResult);
281        assert_eq!(expected, request.build());
282    }
283
284    #[test]
285    fn test_one_criteria_no_selection() {
286        let expected = format!(
287            "http://ergast.com/api/f1/current/drivers/verstappen.json?{},{}",
288            DEFAULT_LIMIT, DEFAULT_OFFSET
289        );
290
291        let request = RequestBuilder::new()
292            .add_parameter(RequestParameter::Driver(String::from("verstappen")));
293        assert_eq!(expected, request.build());
294    }
295
296    #[test]
297    fn test_multiple_criteria_no_selection() {
298        let expected = format!(
299            "http://ergast.com/api/f1/current/drivers/max_verstappen/grid/1.json?{},{}",
300            DEFAULT_LIMIT, DEFAULT_OFFSET
301        );
302
303        let request = RequestBuilder::new()
304            .add_parameter(RequestParameter::Driver(String::from("max_verstappen")))
305            .add_parameter(RequestParameter::Grid(1));
306        assert_eq!(expected, request.build());
307    }
308
309    #[test]
310    fn test_one_criteria_with_season() {
311        let expected = format!(
312            "http://ergast.com/api/f1/2022/drivers/max_verstappen.json?{},{}",
313            DEFAULT_LIMIT, DEFAULT_OFFSET
314        );
315
316        let request = RequestBuilder::new()
317            .add_parameter(RequestParameter::Season(2022))
318            .add_parameter(RequestParameter::Driver(String::from("max_verstappen")));
319        assert_eq!(expected, request.build());
320    }
321
322    #[test]
323    fn test_one_criteria_with_round() {
324        let expected = format!(
325            "http://ergast.com/api/f1/current/20/drivers/max_verstappen.json?{},{}",
326            DEFAULT_LIMIT, DEFAULT_OFFSET
327        );
328
329        let request = RequestBuilder::new()
330            .add_parameter(RequestParameter::Round(20))
331            .add_parameter(RequestParameter::Driver(String::from("max_verstappen")));
332        assert_eq!(expected, request.build());
333    }
334
335    #[test]
336    fn test_pit_selection_with_id() {
337        let expected = format!(
338            "http://ergast.com/api/f1/current/last/pitstops/2.json?{},{}",
339            DEFAULT_LIMIT, DEFAULT_OFFSET
340        );
341
342        let request = RequestBuilder::new()
343            .query(RequestType::PitStops)
344            .add_parameter(RequestParameter::PitStop(2));
345        assert_eq!(expected, request.build());
346    }
347}