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 game_events: Vec<String>, 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 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 pub gameday_type: GameType,
96 #[serde(deserialize_with = "crate::types::from_yes_no")]
97 pub tiebreaker: bool,
98 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, 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 #[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,
224
225 #[serde(rename = "Y")]
226 FirstGame,
228
229 #[serde(rename = "S")]
230 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}