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#[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#[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#[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 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#[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#[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}