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#[serde_as]
60#[derive(Debug, Deserialize, PartialEq, Clone)]
61#[serde(rename_all = "camelCase")]
62#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
63pub struct PlayerWithGameData {
64 pub person: NamedPerson,
65 #[serde(default)]
66 #[serde_as(deserialize_as = "DefaultOnError")]
67 pub jersey_number: Option<JerseyNumber>,
68 pub position: NamedPosition,
69 pub status: RosterStatus,
70 #[serde(rename = "stats")]
71 pub game_stats: BoxscoreStatCollection,
72 pub season_stats: BoxscoreStatCollection,
73 pub game_status: PlayerGameStatusFlags,
74 #[serde(default)]
75 pub all_positions: Vec<NamedPosition>,
76 pub batting_order: Option<BattingOrderIndex>,
77
78 #[doc(hidden)]
79 #[serde(rename = "parentTeamId", default)]
80 pub __parent_team_id: IgnoredAny,
81
82 #[doc(hidden)]
84 #[serde(rename = "boxscoreName", default)]
85 pub __boxscore_name: IgnoredAny,
86}
87
88#[serde_as]
90#[derive(Debug, Deserialize, PartialEq, Clone)]
91#[serde(rename_all = "camelCase")]
92#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
93pub struct TeamWithGameData {
94 pub team: NamedTeam,
95 pub team_stats: BoxscoreStatCollection,
96 #[serde(deserialize_with = "super::deserialize_players_cache")]
97 pub players: FxHashMap<PersonId, PlayerWithGameData>,
98 pub batters: Vec<PersonId>,
99 pub pitchers: Vec<PersonId>,
100 pub bench: Vec<PersonId>,
101 pub bullpen: Vec<PersonId>,
102 #[serde_as(deserialize_as = "DefaultOnError")]
103 pub batting_order: Option<[PersonId; 9]>,
104 #[serde(rename = "info")]
105 pub sectioned_labelled_values: Vec<SectionedLabelledValues>,
106 #[serde(rename = "note")]
107 pub notes: Vec<LabelledValue>,
108}
109
110#[allow(private_interfaces, reason = "the underlying type is pub")]
112#[serde_as]
113#[derive(Debug, Deserialize, PartialEq, Clone)]
114#[serde(rename_all = "camelCase")]
115#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
116pub struct BoxscoreStatCollection {
117 #[serde(rename = "batting")]
118 #[serde_as(deserialize_as = "DefaultOnError")]
119 pub hitting: <__BoxscoreStatTypeStats as StatTypeStats>::Hitting,
120 #[serde_as(deserialize_as = "DefaultOnError")]
121 pub fielding: <__BoxscoreStatTypeStats as StatTypeStats>::Fielding,
122 #[serde_as(deserialize_as = "DefaultOnError")]
123 pub pitching: <__BoxscoreStatTypeStats as StatTypeStats>::Pitching,
124}
125
126#[derive(Builder)]
127#[builder(derive(Into))]
128pub struct BoxscoreRequest {
129 #[builder(into)]
130 id: GameId,
131}
132
133impl<S: boxscore_request_builder::State + boxscore_request_builder::IsComplete> crate::request::RequestURLBuilderExt for BoxscoreRequestBuilder<S> {
134 type Built = BoxscoreRequest;
135}
136
137impl Display for BoxscoreRequest {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 write!(f, "http://statsapi.mlb.com/api/v1/game/{}/boxscore", self.id)
140 }
141}
142
143impl RequestURL for BoxscoreRequest {
144 type Response = Boxscore;
145}
146
147#[cfg(test)]
148mod tests {
149 use crate::TEST_YEAR;
150 use crate::game::BoxscoreRequest;
151 use crate::meta::GameType;
152 use crate::request::RequestURLBuilderExt;
153 use crate::schedule::ScheduleRequest;
154 use crate::season::{Season, SeasonsRequest};
155 use crate::sport::SportId;
156
157 #[tokio::test]
158 async fn ws_gm7_2025_boxscore() {
159 let _ = BoxscoreRequest::builder().id(813_024).build_and_get().await.unwrap();
160 }
161
162 #[tokio::test]
163 async fn postseason_boxscore() {
164 let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
165 let postseason = season.postseason.expect("Expected the MLB to have a postseason");
166 let games = ScheduleRequest::<()>::builder().date_range(postseason).sport_id(SportId::MLB).build_and_get().await.unwrap();
167 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<_>>();
168 let mut has_errors = false;
169 for game in games {
170 if let Err(e) = BoxscoreRequest::builder().id(game).build_and_get().await {
171 dbg!(e);
172 has_errors = true;
173 }
174 }
175 assert!(!has_errors, "Has errors.");
176 }
177
178 #[cfg_attr(not(feature = "_heavy_tests"), ignore)]
179 #[tokio::test]
180 async fn regular_season_boxscore() {
181 let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
182 let regular_season = season.regular_season;
183 let games = ScheduleRequest::<()>::builder().date_range(regular_season).sport_id(SportId::MLB).build_and_get().await.unwrap();
184 let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type == GameType::RegularSeason).collect::<Vec<_>>();
185 let mut has_errors = false;
186 for game in games {
187 if let Err(e) = BoxscoreRequest::builder().id(game.game_id).build_and_get().await {
188 dbg!(e);
189 has_errors = true;
190 }
191 }
192 assert!(!has_errors, "Has errors.");
193 }
194}