1mod keys;
2mod models;
3mod params;
4mod query;
5mod resource;
6
7pub use keys::OpenF1Key;
8pub use models::{
9 CarData, ChampionshipDriver, ChampionshipTeam, Driver, Interval, Lap, Location, Meeting,
10 Overtake, Pit, Position, RaceControl, Session, SessionResult, StartingGrid, Stint, TeamRadio,
11 Weather,
12};
13pub use params::{
14 CarDataListParams, ChampionshipDriversListParams, ChampionshipTeamsListParams,
15 DriversListParams, IntervalsListParams, LapsListParams, LocationListParams, MeetingsListParams,
16 OvertakesListParams, PitListParams, PositionListParams, QueryFilter, RaceControlListParams,
17 SessionResultListParams, SessionsListParams, StartingGridListParams, StintsListParams,
18 TeamRadioListParams, WeatherListParams,
19};
20pub use query::{build_query_params, QueryParams, QueryValue};
21pub use resource::ResourceClient;
22use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
23use serde::de::DeserializeOwned;
24use serde_json::Value;
25use std::time::Duration;
26use thiserror::Error;
27
28pub const BASE_URL: &str = "https://api.openf1.org/v1";
29pub const TOKEN_URL: &str = "https://api.openf1.org/token";
30
31pub const ENDPOINTS: &[&str] = &[
32 "car_data", "championship_drivers", "championship_teams", "drivers", "intervals", "laps",
33 "location", "meetings", "overtakes", "pit", "position", "race_control", "sessions",
34 "session_result", "starting_grid", "stints", "team_radio", "weather",
35];
36
37#[derive(Debug, Error)]
38pub enum OpenF1Error {
39 #[error("http error: {0}")]
40 Http(#[from] reqwest::Error),
41 #[error("json error: {0}")]
42 Json(#[from] serde_json::Error),
43 #[error("api error ({status}): {body}")]
44 Api { status: u16, body: String },
45}
46
47pub struct OpenF1Client {
48 http: reqwest::Client,
49 base_url: String,
50 token: Option<String>,
51}
52
53impl OpenF1Client {
54 pub fn new(token: Option<String>) -> Self {
55 Self {
56 http: reqwest::Client::builder()
57 .timeout(Duration::from_secs(30))
58 .build()
59 .expect("build reqwest client"),
60 base_url: BASE_URL.to_string(),
61 token,
62 }
63 }
64
65 pub fn car_data(&self) -> ResourceClient<'_, CarData> {
66 ResourceClient::new(self, "car_data")
67 }
68
69 pub fn championship_drivers(&self) -> ResourceClient<'_, ChampionshipDriver> {
70 ResourceClient::new(self, "championship_drivers")
71 }
72
73 pub fn championship_teams(&self) -> ResourceClient<'_, ChampionshipTeam> {
74 ResourceClient::new(self, "championship_teams")
75 }
76
77 pub fn drivers(&self) -> ResourceClient<'_, Driver> {
78 ResourceClient::new(self, "drivers")
79 }
80
81 pub fn intervals(&self) -> ResourceClient<'_, Interval> {
82 ResourceClient::new(self, "intervals")
83 }
84
85 pub fn laps(&self) -> ResourceClient<'_, Lap> {
86 ResourceClient::new(self, "laps")
87 }
88
89 pub fn location(&self) -> ResourceClient<'_, Location> {
90 ResourceClient::new(self, "location")
91 }
92
93 pub fn meetings(&self) -> ResourceClient<'_, Meeting> {
94 ResourceClient::new(self, "meetings")
95 }
96
97 pub fn overtakes(&self) -> ResourceClient<'_, Overtake> {
98 ResourceClient::new(self, "overtakes")
99 }
100
101 pub fn pit(&self) -> ResourceClient<'_, Pit> {
102 ResourceClient::new(self, "pit")
103 }
104
105 pub fn position(&self) -> ResourceClient<'_, Position> {
106 ResourceClient::new(self, "position")
107 }
108
109 pub fn race_control(&self) -> ResourceClient<'_, RaceControl> {
110 ResourceClient::new(self, "race_control")
111 }
112
113 pub fn sessions(&self) -> ResourceClient<'_, Session> {
114 ResourceClient::new(self, "sessions")
115 }
116
117 pub fn session_result(&self) -> ResourceClient<'_, SessionResult> {
118 ResourceClient::new(self, "session_result")
119 }
120
121 pub fn starting_grid(&self) -> ResourceClient<'_, StartingGrid> {
122 ResourceClient::new(self, "starting_grid")
123 }
124
125 pub fn stints(&self) -> ResourceClient<'_, Stint> {
126 ResourceClient::new(self, "stints")
127 }
128
129 pub fn team_radio(&self) -> ResourceClient<'_, TeamRadio> {
130 ResourceClient::new(self, "team_radio")
131 }
132
133 pub fn weather(&self) -> ResourceClient<'_, Weather> {
134 ResourceClient::new(self, "weather")
135 }
136
137 pub async fn list(
138 &self,
139 resource: &str,
140 params: QueryParams,
141 ) -> Result<Vec<Value>, OpenF1Error> {
142 let body = self.request(resource, params, false).await?;
143 serde_json::from_str(&body).map_err(OpenF1Error::Json)
144 }
145
146 pub async fn list_typed<T: DeserializeOwned>(
147 &self,
148 resource: &str,
149 params: QueryParams,
150 ) -> Result<Vec<T>, OpenF1Error> {
151 let body = self.request(resource, params, false).await?;
152 serde_json::from_str(&body).map_err(OpenF1Error::Json)
153 }
154
155 pub async fn list_csv(
156 &self,
157 resource: &str,
158 params: QueryParams,
159 ) -> Result<String, OpenF1Error> {
160 self.request(resource, params, true).await
161 }
162
163 async fn request(
164 &self,
165 resource: &str,
166 mut params: QueryParams,
167 csv: bool,
168 ) -> Result<String, OpenF1Error> {
169 const MAX_RETRIES: u32 = 4;
170
171 if csv {
172 params.insert("csv".to_string(), QueryValue::Bool(true));
173 }
174
175 let built = build_query_params(params);
176 let url = format!("{}/{}", self.base_url.trim_end_matches('/'), resource);
177
178 let mut headers = HeaderMap::new();
179 if csv {
180 headers.insert("Accept", HeaderValue::from_static("text/csv"));
181 }
182 if let Some(token) = &self.token {
183 headers.insert(
184 AUTHORIZATION,
185 HeaderValue::from_str(&format!("Bearer {token}"))
186 .expect("valid bearer header"),
187 );
188 }
189
190 for attempt in 0..=MAX_RETRIES {
191 let response = self
192 .http
193 .get(&url)
194 .headers(headers.clone())
195 .query(&built)
196 .send()
197 .await?;
198
199 let status = response.status();
200 let body = response.text().await?;
201
202 if status.as_u16() == 429 && attempt < MAX_RETRIES {
203 let delay_ms = 500 * 2u64.pow(attempt);
204 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
205 continue;
206 }
207
208 if status.is_success() {
209 return Ok(body);
210 }
211
212 return Err(OpenF1Error::Api {
213 status: status.as_u16(),
214 body,
215 });
216 }
217
218 unreachable!("request retry loop exits via return")
219 }
220}
221
222#[cfg(test)]
223mod models_tests {
224 use super::models::*;
225 use std::fs;
226
227 fn fixture(name: &str) -> String {
228 fs::read_to_string(format!(
229 "{}/../../../specs/fixtures/{name}.json",
230 env!("CARGO_MANIFEST_DIR")
231 ))
232 .unwrap_or_else(|error| panic!("read fixture {name}: {error}"))
233 }
234
235 macro_rules! fixture_test {
236 ($name:ident, $ty:ty) => {
237 #[test]
238 fn $name() {
239 let raw = fixture(stringify!($name));
240 let rows: Vec<$ty> =
241 serde_json::from_str(&raw).expect(concat!("deserialize ", stringify!($name)));
242 assert!(!rows.is_empty(), "{}", stringify!($name));
243 }
244 };
245 }
246
247 fixture_test!(car_data, CarData);
248 fixture_test!(championship_drivers, ChampionshipDriver);
249 fixture_test!(championship_teams, ChampionshipTeam);
250 fixture_test!(drivers, Driver);
251 fixture_test!(intervals, Interval);
252 fixture_test!(laps, Lap);
253 fixture_test!(location, Location);
254 fixture_test!(meetings, Meeting);
255 fixture_test!(overtakes, Overtake);
256 fixture_test!(pit, Pit);
257 fixture_test!(position, Position);
258 fixture_test!(race_control, RaceControl);
259 fixture_test!(session_result, SessionResult);
260 fixture_test!(sessions, Session);
261 fixture_test!(starting_grid, StartingGrid);
262 fixture_test!(stints, Stint);
263 fixture_test!(team_radio, TeamRadio);
264 fixture_test!(weather, Weather);
265
266 #[test]
267 fn session_result_accepts_null_laps_and_position() {
268 let raw = r#"[
269 {
270 "position": null,
271 "driver_number": 16,
272 "number_of_laps": null,
273 "points": 0.0,
274 "dnf": false,
275 "dns": false,
276 "dsq": true,
277 "gap_to_leader": null,
278 "duration": null,
279 "meeting_key": 1255,
280 "session_key": 9998
281 }
282 ]"#;
283 let rows: Vec<SessionResult> = serde_json::from_str(raw).expect("deserialize live shape");
284 assert_eq!(rows[0].number_of_laps, 0);
285 assert_eq!(rows[0].position, None);
286 }
287}
288
289#[cfg(test)]
290mod params_tests {
291 use super::params::*;
292 use super::query::QueryValue;
293 use super::OpenF1Key;
294 use std::fs;
295
296 #[test]
297 fn list_params_conformance_matches_typescript() {
298 #[derive(serde::Deserialize)]
299 struct Manifest {
300 params: std::collections::HashMap<String, Vec<String>>,
301 }
302
303 let raw = fs::read_to_string(format!(
304 "{}/../../../specs/conformance/list-params.json",
305 env!("CARGO_MANIFEST_DIR")
306 ))
307 .expect("read list-params manifest");
308 let manifest: Manifest = serde_json::from_str(&raw).expect("parse manifest");
309
310 let rust_params: std::collections::HashMap<&str, &[&str]> = [
311 ("CarDataListParams", &["driver_number", "session_key", "speed"][..]),
312 (
313 "ChampionshipDriversListParams",
314 &["driver_number", "session_key"][..],
315 ),
316 (
317 "ChampionshipTeamsListParams",
318 &["session_key", "team_name"][..],
319 ),
320 ("DriversListParams", &["driver_number", "session_key"][..]),
321 ("IntervalsListParams", &["interval", "session_key"][..]),
322 ("LapsListParams", &["driver_number", "lap_number", "session_key"][..]),
323 ("LocationListParams", &["date", "driver_number", "session_key"][..]),
324 ("MeetingsListParams", &["country_name", "year"][..]),
325 ("OvertakesListParams", &["session_key"][..]),
326 ("PitListParams", &["session_key"][..]),
327 ("PositionListParams", &["driver_number", "session_key"][..]),
328 ("RaceControlListParams", &["session_key"][..]),
329 ("SessionsListParams", &["country_name", "session_name", "year"][..]),
330 ("SessionResultListParams", &["position", "session_key"][..]),
331 ("StartingGridListParams", &["session_key"][..]),
332 ("StintsListParams", &["driver_number", "session_key"][..]),
333 ("TeamRadioListParams", &["driver_number", "session_key"][..]),
334 ("WeatherListParams", &["session_key"][..]),
335 ]
336 .into_iter()
337 .collect();
338
339 for (name, fields) in rust_params {
340 let expected = manifest
341 .params
342 .get(name)
343 .unwrap_or_else(|| panic!("missing manifest entry for {name}"));
344 let mut got = fields.to_vec();
345 got.sort();
346 let mut want = expected.clone();
347 want.sort();
348 assert_eq!(got, want, "{name}");
349 }
350 }
351
352 #[test]
353 fn meetings_list_params_build_query() {
354 let params = MeetingsListParams {
355 year: Some(2026),
356 country_name: None,
357 }
358 .into();
359 assert_eq!(
360 super::build_query_params(params),
361 [("year".to_string(), "2026".to_string())]
362 .into_iter()
363 .collect()
364 );
365 }
366
367 #[test]
368 fn drivers_list_params_support_latest_session_key() {
369 let params = DriversListParams {
370 session_key: Some(OpenF1Key::Latest),
371 driver_number: Some(44),
372 }
373 .into();
374 let built = super::build_query_params(params);
375 assert_eq!(built.get("session_key").map(String::as_str), Some("latest"));
376 assert_eq!(built.get("driver_number").map(String::as_str), Some("44"));
377 }
378
379 #[test]
380 fn laps_list_params_support_filter_operators() {
381 let params = LapsListParams {
382 session_key: Some(OpenF1Key::Id(9161)),
383 driver_number: Some(63),
384 lap_number: Some(QueryFilter {
385 lte: Some(QueryValue::Number(3)),
386 ..QueryFilter::default()
387 }),
388 }
389 .into();
390 let built = super::build_query_params(params);
391 assert_eq!(built.get("lap_number<="), Some(&"3".to_string()));
392 }
393}