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#[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 #[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#[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#[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 #[doc(hidden)]
87 #[serde(rename = "boxscoreName", default)]
88 pub __boxscore_name: IgnoredAny,
89}
90
91#[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#[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}