Skip to main content

mlb_api/requests/game/
mod.rs

1use std::fmt::{Display, Formatter};
2use bon::Builder;
3use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
4use derive_more::{Deref, DerefMut};
5use fxhash::FxHashMap;
6use serde::{Deserialize, Deserializer};
7use serde::de::{Error, MapAccess};
8use serde_with::{serde_as, DisplayFromStr};
9use crate::game_status::GameStatus;
10use crate::game_types::GameType;
11use crate::logical_events::LogicalEventId;
12use crate::person::{Ballplayer, NamedPerson, PersonId};
13use crate::request::RequestURL;
14use crate::season::SeasonId;
15use crate::sky::Sky;
16use crate::team::Team;
17use crate::types::{Copyright, HomeAwaySplits, DayHalf};
18use crate::venue::{Venue, VenueId};
19use crate::wind_direction::WindDirectionId;
20
21pub mod boxscore;
22pub mod changes;
23pub mod color;
24pub mod content;
25pub mod context_metrics;
26pub mod diff;
27pub mod linescore;
28pub mod pace;
29pub mod pbp;
30pub mod timestamps;
31pub mod uniforms;
32pub mod win_probability;
33
34id!(GameId { gamePk: u32 });
35
36#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
37#[serde(rename_all = "camelCase")]
38pub struct GameResponse {
39	pub copyright: Copyright,
40	#[serde(rename = "gamePk")]
41	pub id: GameId,
42	#[serde(rename = "metaData")]
43	pub meta: GameMetadata,
44	#[serde(rename = "gameData")]
45	pub data: GameData,
46	#[serde(rename = "liveData")]
47	pub live: GameLiveData,
48}
49
50#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
51#[serde(rename_all = "camelCase")]
52pub struct GameMetadata {
53	pub wait: u32,
54	// pub timestamp: String, // todo
55	pub game_events: Vec<String>, // todo: what is this type
56	pub logical_events: Vec<LogicalEventId>,
57}
58
59#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Deref, DerefMut)]
60#[serde(rename_all = "camelCase")]
61pub struct GameData {
62	#[deref]
63	#[deref_mut]
64	#[serde(rename = "game")]
65	game: GameDataMeta,
66	pub datetime: GameDateTime,
67	pub status: GameStatus,
68	pub teams: HomeAwaySplits<Team>,
69	#[serde(deserialize_with = "deserialize_players_cache")]
70	pub players: FxHashMap<PersonId, Ballplayer<()>>,
71	pub venue: Venue,
72	pub official_venue: VenueId,
73	pub weather: GameWeather,
74	#[serde(rename = "gameInfo")]
75	pub info: GameInfo,
76	pub review: GameReview,
77	#[serde(rename = "flags")]
78	pub live_tags: GameLiveTags,
79	// pub alerts: Vec<()>, // todo: type?
80	pub probable_pitchers: HomeAwaySplits<NamedPerson>,
81	pub official_scorer: NamedPerson,
82	pub primary_datacaster: NamedPerson,
83	pub mound_visits: HomeAwaySplits<ResourceUsage>,
84}
85
86#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
87#[serde(rename_all = "camelCase")]
88pub struct GameDataMeta {
89	#[serde(rename = "pk")]
90	pub id: GameId,
91	#[serde(rename = "type")]
92	pub game_type: GameType,
93	pub double_header: DoubleHeaderKind,
94	/// Will state `P` for [`GameType::Playoffs`] games rather than what playoff series it is.
95	pub gameday_type: GameType,
96	#[serde(deserialize_with = "crate::types::from_yes_no")]
97	pub tiebreaker: bool,
98	/// No clue what this means
99	pub game_number: u32,
100	pub season: SeasonId,
101	#[serde(rename = "seasonDisplay")]
102	pub displayed_season: SeasonId,
103}
104
105#[derive(Deserialize)]
106#[serde(rename_all = "camelCase")]
107#[doc(hidden)]
108struct __GameDateTimeStruct {
109	#[serde(rename = "dateTime", deserialize_with = "crate::types::deserialize_datetime")]
110	datetime: NaiveDateTime,
111	original_date: NaiveDate,
112	official_date: NaiveDate,
113	#[serde(rename = "dayNight")]
114	sky: Sky,
115	time: NaiveTime,
116	ampm: DayHalf,
117}
118
119#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
120#[serde(from = "__GameDateTimeStruct")]
121pub struct GameDateTime {
122	datetime: NaiveDateTime,
123	original_date: NaiveDate,
124	official_date: NaiveDate,
125	sky: Sky,
126}
127
128impl From<__GameDateTimeStruct> for GameDateTime {
129	fn from(value: __GameDateTimeStruct) -> Self {
130		let date = value.datetime.date();
131		let time = value.ampm.into_24_hour_time(value.time);
132		Self {
133			datetime: NaiveDateTime::new(date, time),
134			original_date: value.original_date,
135			official_date: value.official_date,
136			sky: value.sky,
137		}
138	}
139}
140
141#[derive(Debug, Deserialize, PartialEq, Clone)]
142#[serde(try_from = "__GameWeatherStruct")]
143pub struct GameWeather {
144	pub condition: String, // todo: type?
145	pub temp: uom::si::f64::ThermodynamicTemperature,
146	pub wind_speed: uom::si::f64::Velocity,
147	pub wind_direction: WindDirectionId,
148}
149
150impl Eq for GameWeather {}
151
152#[serde_as]
153#[derive(Deserialize)]
154#[doc(hidden)]
155struct __GameWeatherStruct {
156	condition: String,
157	#[serde_as(as = "DisplayFromStr")]
158	temp: i32,
159	wind: String,
160}
161
162impl TryFrom<__GameWeatherStruct> for GameWeather {
163	type Error = &'static str;
164
165	fn try_from(value: __GameWeatherStruct) -> Result<Self, Self::Error> {
166		let (speed, direction) = value.wind.split_once(" mph, ").ok_or("invalid wind format")?;
167		let speed = speed.parse::<i32>().map_err(|_| "invalid wind speed")?;
168		Ok(Self {
169			condition: value.condition,
170			temp: uom::si::f64::ThermodynamicTemperature::new::<uom::si::thermodynamic_temperature::degree_fahrenheit>(value.temp as f64),
171			wind_speed: uom::si::f64::Velocity::new::<uom::si::velocity::mile_per_hour>(speed as f64),
172			wind_direction: WindDirectionId::new(direction),
173		})
174	}
175}
176
177#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
178#[serde(rename_all = "camelCase")]
179pub struct GameInfo {
180	pub attendance: u32,
181	#[serde(deserialize_with = "crate::types::deserialize_datetime")]
182	pub first_pitch: NaiveDateTime,
183	/// Measured in minutes,
184	#[serde(rename = "gameDurationMinutes")]
185	pub game_duration: u32,
186}
187
188#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
189#[serde(rename_all = "camelCase")]
190pub struct GameReview {
191	pub has_challenges: bool,
192	#[serde(flatten)]
193	pub teams: HomeAwaySplits<ResourceUsage>,
194}
195
196#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
197#[serde(rename_all = "camelCase")]
198pub struct ResourceUsage {
199	used: u32,
200	remaining: u32,
201}
202
203#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
204#[serde(rename_all = "camelCase")]
205pub struct GameLiveTags {
206	no_hitter: bool,
207	perfect_game: bool,
208
209	away_team_no_hitter: bool,
210	away_team_perfect_game: bool,
211
212	home_team_no_hitter: bool,
213	home_team_perfect_game: bool,
214}
215
216#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
217pub struct GameLiveData {}
218
219#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
220pub enum DoubleHeaderKind {
221	#[serde(rename = "N")]
222	/// Not a doubleheader
223	Not,
224
225	#[serde(rename = "Y")]
226	/// First game in a double-header
227	FirstGame,
228
229	#[serde(rename = "S")]
230	/// Second game in a double-header.
231	SecondGame,
232}
233
234impl DoubleHeaderKind {
235	#[must_use]
236	pub const fn is_double_header(self) -> bool {
237		matches!(self, Self::FirstGame | Self::SecondGame)
238	}
239}
240
241fn deserialize_players_cache<'de, D: Deserializer<'de>>(deserializer: D) -> Result<FxHashMap<PersonId, Ballplayer<()>>, D::Error> {
242	struct PlayersCacheVisitor;
243
244	impl<'de2> serde::de::Visitor<'de2> for PlayersCacheVisitor {
245		type Value = FxHashMap<PersonId, Ballplayer<()>>;
246
247		fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
248			formatter.write_str("a map")
249		}
250
251		fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
252		where
253			A: MapAccess<'de2>,
254		{
255			let mut values = FxHashMap::default();
256
257			while let Some((key, value)) = map.next_entry()? {
258				let key: String = key;
259				let key = PersonId::new(key.strip_prefix("ID").ok_or(A::Error::custom("invalid id format"))?.parse::<u32>().map_err(A::Error::custom)?);
260				values.insert(key, value);
261			}
262
263			Ok(values)
264		}
265	}
266
267	deserializer.deserialize_map(PlayersCacheVisitor)
268}
269
270#[derive(Builder)]
271#[builder(derive(Into))]
272pub struct GameRequest {
273	#[builder(into)]
274	id: GameId,
275}
276
277impl<S: game_request_builder::State + game_request_builder::IsComplete> crate::request::RequestURLBuilderExt for GameRequestBuilder<S> {
278	type Built = GameRequest;
279}
280
281impl Display for GameRequest {
282	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
283		write!(f, "http://statsapi.mlb.com/api/v1.1/game/{}/feed/live", self.id)
284	}
285}
286
287impl RequestURL for GameRequest {
288	type Response = GameResponse;
289}
290
291#[cfg(test)]
292mod tests {
293	use crate::game::GameRequest;
294	use crate::request::RequestURLBuilderExt;
295
296	#[tokio::test]
297	async fn ws_gm7_2025() {
298		dbg!(GameRequest::builder().id(813024).build().to_string());
299		let response = GameRequest::builder().id(813024).build_and_get().await.unwrap();
300		dbg!(response);
301	}
302}