1use std::fmt::Display;
8
9use bon::Builder;
10use fxhash::FxHashMap;
11use serde::{Deserialize, de::IgnoredAny};
12use serde_with::{serde_as, DefaultOnError};
13
14use crate::{Copyright, HomeAway, game::{BattingOrderIndex, GameId, LabelledValue, Official, PlayerGameStatusFlags, SectionedLabelledValues}, meta::NamedPosition, person::{Ballplayer, JerseyNumber, NamedPerson, PersonId}, request::RequestURL, stats::{StatTypeStats, stat_types::__BoxscoreStatTypeStats}, team::{NamedTeam, Team, TeamId, roster::RosterStatus}};
15
16#[derive(Debug, Deserialize, PartialEq, Clone)]
18#[serde(rename_all = "camelCase")]
19#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
20pub struct Boxscore {
21 #[serde(default)]
22 pub copyright: Copyright,
23 #[serde(rename = "info")]
24 pub misc: Vec<LabelledValue>,
25 pub top_performers: Option<[TopPerformer; 3]>,
26 pub pitching_notes: Vec<String>,
27 pub teams: HomeAway<TeamWithGameData>,
28 pub officials: Vec<Official>,
29}
30
31impl Boxscore {
32 pub fn find_player_with_game_data(&self, id: PersonId) -> Option<&PlayerWithGameData> {
34 self.teams.home.players.get(&id).or_else(|| self.teams.away.players.get(&id))
35 }
36}
37
38#[derive(Debug, Deserialize, PartialEq, Clone)]
42#[serde(rename_all = "camelCase")]
43#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
44pub struct TopPerformer {
45 pub player: PlayerWithGameData,
46 pub game_score: usize,
47 #[serde(rename = "type")]
48 pub player_kind: String,
49
50 #[doc(hidden)]
51 #[serde(rename = "pitchingGameScore", default)]
52 pub __pitching_game_score: IgnoredAny,
53 #[doc(hidden)]
54 #[serde(rename = "hittingGameScore", default)]
55 pub __hitting_game_score: IgnoredAny,
56}
57
58#[derive(Debug, Deserialize, PartialEq, Clone)]
60#[serde(from = "__PlayerWithGameDataStruct")]
61#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
62pub struct PlayerWithGameData {
63 pub person: NamedPerson,
64 pub boxscore_name: String,
65 pub jersey_number: Option<JerseyNumber>,
66 pub position: NamedPosition,
67 pub status: RosterStatus,
68 pub game_stats: BoxscoreStatCollection,
69 pub season_stats: BoxscoreStatCollection,
71 pub game_status: PlayerGameStatusFlags,
72 pub all_positions: Vec<NamedPosition>,
73 pub batting_order: Option<BattingOrderIndex>,
74
75 pub __parent_team_id: IgnoredAny,
76}
77
78#[doc(hidden)]
79#[serde_as]
80#[derive(Deserialize)]
81#[serde(rename_all = "camelCase")]
82#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
83struct __PlayerWithGameDataStruct {
84 person: __NamedPersonWithBoxscoreName,
85 #[serde(default)]
86 #[serde_as(deserialize_as = "DefaultOnError")]
87 jersey_number: Option<JerseyNumber>,
88 position: NamedPosition,
89 status: RosterStatus,
90 #[serde(rename = "stats")]
91 game_stats: BoxscoreStatCollection,
92 season_stats: BoxscoreStatCollection,
93 game_status: PlayerGameStatusFlags,
94 #[serde(default)]
95 all_positions: Vec<NamedPosition>,
96 batting_order: Option<BattingOrderIndex>,
97
98 #[doc(hidden)]
99 #[serde(rename = "parentTeamId", default)]
100 __parent_team_id: IgnoredAny,
101}
102
103#[derive(Deserialize)]
104#[serde(rename_all = "camelCase")]
105struct __NamedPersonWithBoxscoreName {
106 id: PersonId,
107 full_name: String,
108 boxscore_name: String,
109}
110
111impl From<__PlayerWithGameDataStruct> for PlayerWithGameData {
112 fn from(__PlayerWithGameDataStruct {
113 person,
114 jersey_number,
115 position,
116 status,
117 game_stats,
118 season_stats,
119 game_status,
120 all_positions,
121 batting_order,
122 __parent_team_id
123 }: __PlayerWithGameDataStruct) -> Self {
124 Self {
125 person: NamedPerson { full_name: person.full_name, id: person.id },
126 boxscore_name: person.boxscore_name,
127 jersey_number,
128 position,
129 status,
130 game_stats,
131 season_stats,
132 game_status,
133 all_positions,
134 batting_order,
135 __parent_team_id
136 }
137 }
138}
139
140#[serde_as]
142#[derive(Debug, Deserialize, PartialEq, Clone)]
143#[serde(rename_all = "camelCase")]
144#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
145pub struct TeamWithGameData {
146 pub team: NamedTeam,
147 pub team_stats: BoxscoreStatCollection,
148 #[serde(deserialize_with = "super::deserialize_players_cache")]
149 pub players: FxHashMap<PersonId, PlayerWithGameData>,
150 pub batters: Vec<PersonId>,
151 pub pitchers: Vec<PersonId>,
152 pub bench: Vec<PersonId>,
153 pub bullpen: Vec<PersonId>,
154 #[serde_as(deserialize_as = "DefaultOnError")]
155 pub batting_order: Option<[PersonId; 9]>,
156 #[serde(rename = "info")]
157 pub sectioned_labelled_values: Vec<SectionedLabelledValues>,
158 #[serde(rename = "note")]
159 pub notes: Vec<LabelledValue>,
160}
161
162#[allow(private_interfaces, reason = "the underlying type is pub")]
164#[serde_as]
165#[derive(Debug, Deserialize, PartialEq, Clone)]
166#[serde(rename_all = "camelCase")]
167#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
168pub struct BoxscoreStatCollection {
169 #[serde(rename = "batting")]
170 #[serde_as(deserialize_as = "DefaultOnError")]
171 pub hitting: <__BoxscoreStatTypeStats as StatTypeStats>::Hitting,
172 #[serde_as(deserialize_as = "DefaultOnError")]
173 pub fielding: <__BoxscoreStatTypeStats as StatTypeStats>::Fielding,
174 #[serde_as(deserialize_as = "DefaultOnError")]
175 pub pitching: <__BoxscoreStatTypeStats as StatTypeStats>::Pitching,
176}
177
178#[derive(Builder)]
179#[builder(derive(Into))]
180pub struct BoxscoreRequest {
181 #[builder(into)]
182 id: GameId,
183}
184
185impl<S: boxscore_request_builder::State + boxscore_request_builder::IsComplete> crate::request::RequestURLBuilderExt for BoxscoreRequestBuilder<S> {
186 type Built = BoxscoreRequest;
187}
188
189impl Display for BoxscoreRequest {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 write!(f, "http://statsapi.mlb.com/api/v1/game/{}/boxscore", self.id)
192 }
193}
194
195impl RequestURL for BoxscoreRequest {
196 type Response = Boxscore;
197}
198
199#[cfg(test)]
200mod tests {
201 use crate::TEST_YEAR;
202 use crate::game::BoxscoreRequest;
203 use crate::meta::GameType;
204 use crate::request::RequestURLBuilderExt;
205 use crate::schedule::ScheduleRequest;
206 use crate::season::{Season, SeasonsRequest};
207 use crate::sport::SportId;
208
209 #[tokio::test]
210 async fn ws_gm7_2025_boxscore() {
211 let _ = BoxscoreRequest::builder().id(813_024).build_and_get().await.unwrap();
212 }
213
214 #[tokio::test]
215 async fn postseason_boxscore() {
216 let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
217 let postseason = season.postseason.expect("Expected the MLB to have a postseason");
218 let games = ScheduleRequest::<()>::builder().date_range(postseason).sport_id(SportId::MLB).build_and_get().await.unwrap();
219 let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type.is_postseason()).map(|game| game.game_id).collect::<Vec<_>>();
220 let mut has_errors = false;
221 for game in games {
222 if let Err(e) = BoxscoreRequest::builder().id(game).build_and_get().await {
223 dbg!(e);
224 has_errors = true;
225 }
226 }
227 assert!(!has_errors, "Has errors.");
228 }
229
230 #[cfg_attr(not(feature = "_heavy_tests"), ignore)]
231 #[tokio::test]
232 async fn regular_season_boxscore() {
233 let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
234 let regular_season = season.regular_season;
235 let games = ScheduleRequest::<()>::builder().date_range(regular_season).sport_id(SportId::MLB).build_and_get().await.unwrap();
236 let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type == GameType::RegularSeason).collect::<Vec<_>>();
237 let mut has_errors = false;
238 for game in games {
239 if let Err(e) = BoxscoreRequest::builder().id(game.game_id).build_and_get().await {
240 dbg!(e);
241 has_errors = true;
242 }
243 }
244 assert!(!has_errors, "Has errors.");
245 }
246}