Skip to main content

mlb_api/requests/game/
boxscore.rs

1//! Various live information & collection stats about the ongoing game.
2//!
3//! Teams have pitching, hitting, and fielding stats, rosters, batting orders, etc.
4//!
5//! Lists of umpires, top performers, etc.
6
7use 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/// See [`self`]
17#[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    /// Returns a [`PlayerWithGameData`] if present in the baseball game.
33    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/// One of three "top performers" of the game, measured by game score.
39///
40/// Originally an enum but the amount of two-way-players that exist make it pointlessly annoying and easy to break.
41#[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/// A person with some potentially useful information regarding their performance in the current game.
59#[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	/// Uses the active game's [`GameType`], not the regular season stats.
70	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/// A team with some potentially useful information regarding their performance in the current game.
141#[derive(Debug, Deserialize, PartialEq, Clone)]
142#[serde(rename_all = "camelCase")]
143#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
144pub struct TeamWithGameData {
145    pub team: NamedTeam,
146    pub team_stats: BoxscoreStatCollection,
147    #[serde(deserialize_with = "super::deserialize_players_cache")]
148    pub players: FxHashMap<PersonId, PlayerWithGameData>,
149    pub batters: Vec<PersonId>,
150    pub pitchers: Vec<PersonId>,
151    pub bench: Vec<PersonId>,
152    pub bullpen: Vec<PersonId>,
153    pub batting_order: [PersonId; 9],
154    #[serde(rename = "info")]
155    pub sectioned_labelled_values: Vec<SectionedLabelledValues>,
156    #[serde(rename = "note")]
157    pub notes: Vec<LabelledValue>,
158}
159
160/// Hitting, Pitching, and Fielding stats.
161#[allow(private_interfaces, reason = "the underlying type is pub")]
162#[serde_as]
163#[derive(Debug, Deserialize, PartialEq, Clone)]
164#[serde(rename_all = "camelCase")]
165#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
166pub struct BoxscoreStatCollection {
167    #[serde(rename = "batting")]
168    #[serde_as(deserialize_as = "DefaultOnError")]
169    pub hitting: <__BoxscoreStatTypeStats as StatTypeStats>::Hitting,
170    #[serde_as(deserialize_as = "DefaultOnError")]
171    pub fielding: <__BoxscoreStatTypeStats as StatTypeStats>::Fielding,
172    #[serde_as(deserialize_as = "DefaultOnError")]
173    pub pitching: <__BoxscoreStatTypeStats as StatTypeStats>::Pitching,
174}
175
176#[derive(Builder)]
177#[builder(derive(Into))]
178pub struct BoxscoreRequest {
179    #[builder(into)]
180    id: GameId,
181}
182
183impl<S: boxscore_request_builder::State + boxscore_request_builder::IsComplete> crate::request::RequestURLBuilderExt for BoxscoreRequestBuilder<S> {
184    type Built = BoxscoreRequest;
185}
186
187impl Display for BoxscoreRequest {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        write!(f, "http://statsapi.mlb.com/api/v1/game/{}/boxscore", self.id)
190    }
191}
192
193impl RequestURL for BoxscoreRequest {
194    type Response = Boxscore;
195}
196
197#[cfg(test)]
198mod tests {
199    use crate::TEST_YEAR;
200    use crate::game::BoxscoreRequest;
201    use crate::meta::GameType;
202    use crate::request::RequestURLBuilderExt;
203    use crate::schedule::ScheduleRequest;
204    use crate::season::{Season, SeasonsRequest};
205    use crate::sport::SportId;
206
207    #[tokio::test]
208    async fn ws_gm7_2025_boxscore() {
209        let _ = BoxscoreRequest::builder().id(813_024).build_and_get().await.unwrap();
210    }
211
212    #[tokio::test]
213	async fn postseason_boxscore() {
214		let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
215		let postseason = season.postseason.expect("Expected the MLB to have a postseason");
216		let games = ScheduleRequest::<()>::builder().date_range(postseason).sport_id(SportId::MLB).build_and_get().await.unwrap();
217		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<_>>();
218		let mut has_errors = false;
219		for game in games {
220			if let Err(e) = BoxscoreRequest::builder().id(game).build_and_get().await {
221			    dbg!(e);
222			    has_errors = true;
223			}
224		}
225		assert!(!has_errors, "Has errors.");
226	}
227
228	#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
229    #[tokio::test]
230    async fn regular_season_boxscore() {
231        let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
232        let regular_season = season.regular_season;
233        let games = ScheduleRequest::<()>::builder().date_range(regular_season).sport_id(SportId::MLB).build_and_get().await.unwrap();
234        let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type == GameType::RegularSeason).collect::<Vec<_>>();
235        let mut has_errors = false;
236        for game in games {
237            if let Err(e) = BoxscoreRequest::builder().id(game.game_id).build_and_get().await {
238                dbg!(e);
239                has_errors = true;
240            }
241        }
242        assert!(!has_errors, "Has errors.");
243    }
244}