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