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 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 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 pub fn add_parameter(mut self, param: RequestParameter) -> RequestBuilder {
151 match ¶m {
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 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 pub fn protocol(mut self, protocol: String) -> RequestBuilder {
208 self.protocol = protocol;
209 self
210 }
211
212 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 pub fn offset(mut self, offset: u32) -> RequestBuilder {
225 self.offset = offset;
226 self
227 }
228
229 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}