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#[serde_as]
18#[derive(Debug, Deserialize, PartialEq, Clone)]
19#[serde(rename_all = "camelCase")]
20#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
21pub struct Boxscore {
22    #[serde(default)]
23    pub copyright: Copyright,
24    #[serde(rename = "info")]
25    pub misc: Vec<LabelledValue>,
26    #[serde_as(deserialize_as = "DefaultOnError")]
27    pub top_performers: Option<[TopPerformer; 3]>,
28    pub pitching_notes: Vec<String>,
29    pub teams: HomeAway<TeamWithGameData>,
30    pub officials: Vec<Official>,
31}
32
33impl Boxscore {
34    /// Returns a [`PlayerWithGameData`] if present in the baseball game.
35    #[must_use]
36    pub fn find_player_with_game_data(&self, id: PersonId) -> Option<&PlayerWithGameData> {
37        self.teams.home.players.get(&id).or_else(|| self.teams.away.players.get(&id))
38    }
39}
40
41/// One of three "top performers" of the game, measured by game score.
42///
43/// Originally an enum but the amount of two-way-players that exist make it pointlessly annoying and easy to break.
44#[derive(Debug, Deserialize, PartialEq, Clone)]
45#[serde(rename_all = "camelCase")]
46#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
47pub struct TopPerformer {
48    pub player: PlayerWithGameData,
49    pub game_score: usize,
50    #[serde(rename = "type")]
51    pub player_kind: String,
52
53    #[doc(hidden)]
54    #[serde(rename = "pitchingGameScore", default)]
55    pub __pitching_game_score: IgnoredAny,
56    #[doc(hidden)]
57    #[serde(rename = "hittingGameScore", default)]
58    pub __hitting_game_score: IgnoredAny,
59}
60
61/// A person with some potentially useful information regarding their performance in the current game.
62#[serde_as]
63#[derive(Debug, Deserialize, PartialEq, Clone)]
64#[serde(rename_all = "camelCase")]
65#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
66pub struct PlayerWithGameData {
67	pub person: NamedPerson,
68	#[serde(default)]
69	#[serde_as(deserialize_as = "DefaultOnError")]
70	pub jersey_number: Option<JerseyNumber>,
71	pub position: NamedPosition,
72	pub status: RosterStatus,
73	#[serde(rename = "stats")]
74	pub game_stats: BoxscoreStatCollection,
75	pub season_stats: BoxscoreStatCollection,
76	pub game_status: PlayerGameStatusFlags,
77	#[serde(default)]
78	pub all_positions: Vec<NamedPosition>,
79	pub batting_order: Option<BattingOrderIndex>,
80
81    #[doc(hidden)]
82    #[serde(rename = "parentTeamId", default)]
83	pub __parent_team_id: IgnoredAny,
84
85	// only sometimes present
86	#[doc(hidden)]
87	#[serde(rename = "boxscoreName", default)]
88	pub __boxscore_name: IgnoredAny,
89}
90
91/// A team with some potentially useful information regarding their performance in the current game.
92#[serde_as]
93#[derive(Debug, Deserialize, PartialEq, Clone)]
94#[serde(rename_all = "camelCase")]
95#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
96pub struct TeamWithGameData {
97    pub team: NamedTeam,
98    pub team_stats: BoxscoreStatCollection,
99    #[serde(deserialize_with = "super::deserialize_players_cache")]
100    pub players: FxHashMap<PersonId, PlayerWithGameData>,
101    pub batters: Vec<PersonId>,
102    pub pitchers: Vec<PersonId>,
103    pub bench: Vec<PersonId>,
104    pub bullpen: Vec<PersonId>,
105    #[serde_as(deserialize_as = "DefaultOnError")]
106    pub batting_order: Option<[PersonId; 9]>,
107    #[serde(rename = "info")]
108    pub sectioned_labelled_values: Vec<SectionedLabelledValues>,
109    #[serde(rename = "note")]
110    pub notes: Vec<LabelledValue>,
111}
112
113/// Hitting, Pitching, and Fielding stats.
114#[allow(private_interfaces, reason = "the underlying type is pub")]
115#[serde_as]
116#[derive(Debug, Deserialize, PartialEq, Clone)]
117#[serde(rename_all = "camelCase")]
118#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
119pub struct BoxscoreStatCollection {
120    #[serde(rename = "batting")]
121    #[serde_as(deserialize_as = "DefaultOnError")]
122    pub hitting: <__BoxscoreStatTypeStats as StatTypeStats>::Hitting,
123    #[serde_as(deserialize_as = "DefaultOnError")]
124    pub fielding: <__BoxscoreStatTypeStats as StatTypeStats>::Fielding,
125    #[serde_as(deserialize_as = "DefaultOnError")]
126    pub pitching: <__BoxscoreStatTypeStats as StatTypeStats>::Pitching,
127}
128
129#[derive(Builder)]
130#[builder(derive(Into))]
131pub struct BoxscoreRequest {
132    #[builder(into)]
133    id: GameId,
134}
135
136impl<S: boxscore_request_builder::State + boxscore_request_builder::IsComplete> crate::request::RequestURLBuilderExt for BoxscoreRequestBuilder<S> {
137    type Built = BoxscoreRequest;
138}
139
140impl Display for BoxscoreRequest {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        write!(f, "http://statsapi.mlb.com/api/v1/game/{}/boxscore", self.id)
143    }
144}
145
146impl RequestURL for BoxscoreRequest {
147    type Response = Boxscore;
148}
149
150#[cfg(test)]
151mod tests {
152    use crate::TEST_YEAR;
153    use crate::game::BoxscoreRequest;
154    use crate::meta::GameType;
155    use crate::request::RequestURLBuilderExt;
156    use crate::schedule::ScheduleRequest;
157    use crate::season::{Season, SeasonsRequest};
158    use crate::sport::SportId;
159
160    #[tokio::test]
161    async fn ws_gm7_2025_boxscore() {
162        let _ = BoxscoreRequest::builder().id(813_024).build_and_get().await.unwrap();
163    }
164
165    #[tokio::test]
166	async fn postseason_boxscore() {
167		let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
168		let postseason = season.postseason.expect("Expected the MLB to have a postseason");
169		let games = ScheduleRequest::<()>::builder().date_range(postseason).sport_id(SportId::MLB).build_and_get().await.unwrap();
170		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<_>>();
171		let mut has_errors = false;
172		for game in games {
173			if let Err(e) = BoxscoreRequest::builder().id(game).build_and_get().await {
174			    dbg!(e);
175			    has_errors = true;
176			}
177		}
178		assert!(!has_errors, "Has errors.");
179	}
180
181	#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
182    #[tokio::test]
183    async fn regular_season_boxscore() {
184        let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
185        let regular_season = season.regular_season;
186        let games = ScheduleRequest::<()>::builder().date_range(regular_season).sport_id(SportId::MLB).build_and_get().await.unwrap();
187        let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type == GameType::RegularSeason).collect::<Vec<_>>();
188        let mut has_errors = false;
189        for game in games {
190            if let Err(e) = BoxscoreRequest::builder().id(game.game_id).build_and_get().await {
191                dbg!(e);
192                has_errors = true;
193            }
194        }
195        assert!(!has_errors, "Has errors.");
196    }
197}