use std::fmt::Display;
use bon::Builder;
use fxhash::FxHashMap;
use serde::{Deserialize, de::IgnoredAny};
use serde_with::{serde_as, DefaultOnError};
use 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}};
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct Boxscore {
#[serde(default)]
pub copyright: Copyright,
#[serde(rename = "info")]
pub misc: Vec<LabelledValue>,
pub top_performers: Option<[TopPerformer; 3]>,
pub pitching_notes: Vec<String>,
pub teams: HomeAway<TeamWithGameData>,
pub officials: Vec<Official>,
}
impl Boxscore {
pub fn find_player_with_game_data(&self, id: PersonId) -> Option<&PlayerWithGameData> {
self.teams.home.players.get(&id).or_else(|| self.teams.away.players.get(&id))
}
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct TopPerformer {
pub player: PlayerWithGameData,
pub game_score: usize,
#[serde(rename = "type")]
pub player_kind: String,
#[doc(hidden)]
#[serde(rename = "pitchingGameScore", default)]
pub __pitching_game_score: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "hittingGameScore", default)]
pub __hitting_game_score: IgnoredAny,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(from = "__PlayerWithGameDataStruct")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct PlayerWithGameData {
pub person: NamedPerson,
pub boxscore_name: String,
pub jersey_number: Option<JerseyNumber>,
pub position: NamedPosition,
pub status: RosterStatus,
pub stats: BoxscoreStatCollection,
pub season_stats: BoxscoreStatCollection,
pub game_status: PlayerGameStatusFlags,
pub all_positions: Vec<NamedPosition>,
pub batting_order: Option<BattingOrderIndex>,
pub __parent_team_id: IgnoredAny,
}
#[doc(hidden)]
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
struct __PlayerWithGameDataStruct {
person: __NamedPersonWithBoxscoreName,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
jersey_number: Option<JerseyNumber>,
position: NamedPosition,
status: RosterStatus,
stats: BoxscoreStatCollection,
season_stats: BoxscoreStatCollection,
game_status: PlayerGameStatusFlags,
#[serde(default)]
all_positions: Vec<NamedPosition>,
batting_order: Option<BattingOrderIndex>,
#[doc(hidden)]
#[serde(rename = "parentTeamId", default)]
__parent_team_id: IgnoredAny,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct __NamedPersonWithBoxscoreName {
id: PersonId,
full_name: String,
boxscore_name: String,
}
impl From<__PlayerWithGameDataStruct> for PlayerWithGameData {
fn from(__PlayerWithGameDataStruct {
person,
jersey_number,
position,
status,
stats,
season_stats,
game_status,
all_positions,
batting_order,
__parent_team_id
}: __PlayerWithGameDataStruct) -> Self {
Self {
person: NamedPerson { full_name: person.full_name, id: person.id },
boxscore_name: person.boxscore_name,
jersey_number,
position,
status,
stats,
season_stats,
game_status,
all_positions,
batting_order,
__parent_team_id
}
}
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct TeamWithGameData {
pub team: NamedTeam,
pub team_stats: BoxscoreStatCollection,
#[serde(deserialize_with = "super::deserialize_players_cache")]
pub players: FxHashMap<PersonId, PlayerWithGameData>,
pub batters: Vec<PersonId>,
pub pitchers: Vec<PersonId>,
pub bench: Vec<PersonId>,
pub bullpen: Vec<PersonId>,
pub batting_order: [PersonId; 9],
#[serde(rename = "info")]
pub sectioned_labelled_values: Vec<SectionedLabelledValues>,
#[serde(rename = "note")]
pub notes: Vec<LabelledValue>,
}
#[allow(private_interfaces, reason = "the underlying type is pub")]
#[serde_as]
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct BoxscoreStatCollection {
#[serde(rename = "batting")]
#[serde_as(deserialize_as = "DefaultOnError")]
pub hitting: <__BoxscoreStatTypeStats as StatTypeStats>::Hitting,
#[serde_as(deserialize_as = "DefaultOnError")]
pub fielding: <__BoxscoreStatTypeStats as StatTypeStats>::Fielding,
#[serde_as(deserialize_as = "DefaultOnError")]
pub pitching: <__BoxscoreStatTypeStats as StatTypeStats>::Pitching,
}
#[derive(Builder)]
#[builder(derive(Into))]
pub struct BoxscoreRequest {
#[builder(into)]
id: GameId,
}
impl<S: boxscore_request_builder::State + boxscore_request_builder::IsComplete> crate::request::RequestURLBuilderExt for BoxscoreRequestBuilder<S> {
type Built = BoxscoreRequest;
}
impl Display for BoxscoreRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "http://statsapi.mlb.com/api/v1/game/{}/boxscore", self.id)
}
}
impl RequestURL for BoxscoreRequest {
type Response = Boxscore;
}
#[cfg(test)]
mod tests {
use crate::TEST_YEAR;
use crate::game::BoxscoreRequest;
use crate::meta::GameType;
use crate::request::RequestURLBuilderExt;
use crate::schedule::ScheduleRequest;
use crate::season::{Season, SeasonsRequest};
use crate::sport::SportId;
#[tokio::test]
async fn ws_gm7_2025_boxscore() {
let _ = BoxscoreRequest::builder().id(813_024).build_and_get().await.unwrap();
}
#[tokio::test]
async fn postseason_boxscore() {
let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
let postseason = season.postseason.expect("Expected the MLB to have a postseason");
let games = ScheduleRequest::<()>::builder().date_range(postseason).sport_id(SportId::MLB).build_and_get().await.unwrap();
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<_>>();
let mut has_errors = false;
for game in games {
if let Err(e) = BoxscoreRequest::builder().id(game).build_and_get().await {
dbg!(e);
has_errors = true;
}
}
assert!(!has_errors, "Has errors.");
}
#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
#[tokio::test]
async fn regular_season_boxscore() {
let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
let regular_season = season.regular_season;
let games = ScheduleRequest::<()>::builder().date_range(regular_season).sport_id(SportId::MLB).build_and_get().await.unwrap();
let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type == GameType::RegularSeason).collect::<Vec<_>>();
let mut has_errors = false;
for game in games {
if let Err(e) = BoxscoreRequest::builder().id(game.game_id).build_and_get().await {
dbg!(e);
has_errors = true;
}
}
assert!(!has_errors, "Has errors.");
}
}