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