use std::{fmt::Display, num::NonZeroUsize, ops::{Deref, DerefMut}};
use bon::Builder;
use chrono::{DateTime, Utc};
use derive_more::{Deref, DerefMut, Display};
use serde::{Deserialize, Deserializer, de::IgnoredAny};
use serde_with::{serde_as, DefaultOnNull, DefaultOnError};
use uuid::Uuid;
use crate::{Copyright, Handedness, HomeAway, game::{AtBatCount, Base, BattingOrderIndex, ContactHardness, GameId, Inning, InningHalf}, meta::{EventType, HitTrajectory, NamedPosition, PitchCodeId, PitchType, ReviewReasonId}, person::{NamedPerson, PersonId}, request::RequestURL, stats::raw::{HittingHotColdZones, PitchingHotColdZones, StrikeZoneSection}, team::TeamId};
#[allow(clippy::struct_field_names, clippy::unsafe_derive_deserialize, reason = "not relevant here")]
#[derive(Debug, Deserialize, PartialEq, Clone, Deref)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct Plays {
#[serde(default)]
pub copyright: Copyright,
#[deref]
#[serde(rename = "allPlays")]
plays: Vec<Play>,
pub current_play: Option<Play>,
#[serde(rename = "scoringPlays")]
pub(super) scoring_play_indices: Vec<usize>,
#[serde(rename = "playsByInning")]
pub(super) play_indices_by_inning: Vec<InningPlaysIndices>,
}
impl IntoIterator for Plays {
type Item = Play;
type IntoIter = <Vec<Play> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.plays.into_iter()
}
}
impl<'a> IntoIterator for &'a Plays {
type Item = &'a Play;
type IntoIter = std::slice::Iter<'a, Play>;
fn into_iter(self) -> Self::IntoIter {
self.plays.iter()
}
}
impl Plays {
#[must_use]
pub const unsafe fn plays_mut(&mut self) -> &mut Vec<Play> {
&mut self.plays
}
#[must_use]
pub fn into_plays(self) -> Vec<Play> {
self.plays
}
pub fn scoring_plays(&self) -> impl Iterator<Item=&Play> {
self.scoring_play_indices.iter()
.filter_map(|&idx| self.plays.get(idx))
}
pub fn by_inning(&self) -> impl Iterator<Item=impl Iterator<Item=&Play>> {
self.play_indices_by_inning.iter()
.map(|inning| (inning.start..=inning.end)
.filter_map(|idx| self.plays.get(idx)))
}
pub fn by_inning_halves(&self) -> impl Iterator<Item=(impl Iterator<Item=&Play>, impl Iterator<Item=&Play>)> {
self.play_indices_by_inning.iter()
.map(|inning| (
inning.top_indices.iter().filter_map(|&idx| self.plays.get(idx)),
inning.bottom_indices.iter().filter_map(|&idx| self.plays.get(idx))
))
}
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub(super) struct InningPlaysIndices {
#[serde(rename = "startIndex")]
pub(super) start: usize,
#[serde(rename = "endIndex")]
pub(super) end: usize,
#[serde(rename = "top")]
pub(super) top_indices: Vec<usize>,
#[serde(rename = "bottom")]
pub(super) bottom_indices: Vec<usize>,
#[doc(hidden)]
#[serde(rename = "hits", default)]
pub(super) __balls_in_play: IgnoredAny,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct Play {
pub result: PlayDetails,
pub about: PlayAbout,
pub count: AtBatCount,
pub matchup: PlayMatchup,
pub play_events: Vec<PlayEvent>,
pub runners: Vec<RunnerData>,
#[serde(rename = "reviewDetails", default, deserialize_with = "deserialize_review_data")]
pub reviews: Vec<ReviewData>,
#[serde(rename = "playEndTime", deserialize_with = "crate::deserialize_datetime")]
pub play_end_timestamp: DateTime<Utc>,
#[doc(hidden)]
#[serde(rename = "pitchIndex", default)]
pub __pitch_indices: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "actionIndex", default)]
pub __action_indices: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "runnerIndex", default)]
pub __runner_indices: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "atBatIndex", default)]
pub __at_bat_index: IgnoredAny,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct PlayDetails {
#[serde(flatten, default)]
pub completed_play_details: Option<CompletedPlayDetails>,
pub away_score: usize,
pub home_score: usize,
#[doc(hidden)]
#[serde(rename = "event", default)]
pub __event: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "type", default)]
pub __type: IgnoredAny,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct CompletedPlayDetails {
#[serde(rename = "eventType")]
pub event: EventType,
pub description: String,
pub rbi: usize,
pub is_out: bool,
}
#[allow(clippy::struct_excessive_bools, reason = "inapplicable")]
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct PlayAbout {
#[serde(rename = "atBatIndex")]
pub at_bat_idx: usize,
#[serde(rename = "halfInning")]
pub inning_half: InningHalf,
pub inning: Inning,
#[serde(rename = "startTime", deserialize_with = "crate::deserialize_datetime")]
pub start_timestamp: DateTime<Utc>,
#[serde(rename = "endTime", deserialize_with = "crate::deserialize_datetime")]
pub end_timestamp: DateTime<Utc>,
pub is_complete: bool,
pub is_scoring_play: Option<bool>,
#[serde(default)]
pub has_review: bool,
#[serde(default)]
pub has_out: bool,
#[serde(default)]
pub captivating_index: usize,
#[doc(hidden)]
#[serde(rename = "isTopInning")]
pub __is_top_inning: IgnoredAny,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct PlayMatchup {
pub batter: NamedPerson,
pub pitcher: NamedPerson,
pub bat_side: Handedness,
pub pitch_hand: Handedness,
pub post_on_first: Option<NamedPerson>,
pub post_on_second: Option<NamedPerson>,
pub post_on_third: Option<NamedPerson>,
#[doc(hidden)]
#[serde(rename = "batterHotColdZones", default)]
pub __batter_hot_cold_zones: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "pitcherHotColdZones", default)]
pub __pitcher_hot_cold_zones: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "batterHotColdZoneStats", default)]
pub __batter_hot_cold_zone_stats: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "pitcherHotColdZoneStats", default)]
pub __pitcher_hot_cold_zone_stats: IgnoredAny,
pub splits: ApplicablePlayMatchupSplits,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct ApplicablePlayMatchupSplits {
pub batter: String,
pub pitcher: String,
pub men_on_base: String,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct RunnerData {
pub movement: RunnerMovement,
pub details: RunnerDetails,
#[serde(default)]
pub credits: Vec<RunnerCredit>,
}
#[serde_as]
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct RunnerMovement {
pub origin_base: Option<Base>,
#[serde(rename = "start")]
pub start_base: Option<Base>,
#[serde(rename = "end")]
pub end_base: Option<Base>,
pub out_base: Option<Base>,
#[serde_as(deserialize_as = "DefaultOnNull")]
#[serde(default)]
pub is_out: bool,
pub out_number: Option<usize>,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct RunnerDetails {
pub movement_reason: Option<MovementReason>,
pub runner: NamedPerson,
pub is_scoring_event: bool,
#[serde(rename = "rbi")]
pub is_rbi: bool,
#[serde(rename = "earned")]
pub is_earned: bool,
#[doc(hidden)]
#[serde(rename = "eventType", default)]
pub __event_tyoe: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "event", default)]
pub __event_type: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "responsiblePitcher", default)]
pub __responsible_pitcher: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "teamUnearned", default)]
pub __team_unearned: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "playIndex", default)]
pub __play_index: IgnoredAny,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display)]
pub enum MovementReason {
#[display("Unforced Base Advancement")]
#[serde(rename = "r_adv_play")]
AdvancementUnforced,
#[display("Forced Base Advancement")]
#[serde(rename = "r_adv_force")]
AdancementForced,
#[display("Advancement from Throw")]
#[serde(rename = "r_adv_throw")]
AdvancementThrow,
#[display("Doubled Off")]
#[serde(rename = "r_doubled_off")]
DoubledOff,
#[display("Thrown Out")]
#[serde(rename = "r_thrown_out")]
ThrownOut,
#[display("Called Out Returning")]
#[serde(rename = "r_out_returning")]
CalledOutReturning,
#[display("Forced Out")]
#[serde(rename = "r_force_out")]
ForceOut,
#[display("Runner Called Out")]
#[serde(rename = "r_runner_out")]
RunnerCalledOut,
#[display("Defensive Indifference")]
#[serde(rename = "r_defensive_indiff")]
DefensiveIndifference,
#[display("Rundown")]
#[serde(rename = "r_rundown")]
Rundown,
#[display("Out Stretching")]
#[serde(rename = "r_out_stretching")]
OutStretching,
#[display("Stolen Base (2B)")]
#[serde(rename = "r_stolen_base_2b")]
StolenBase2B,
#[display("Stolen Base (3B)")]
#[serde(rename = "r_stolen_base_3b")]
StolenBase3B,
#[display("Stolen Base (HP)")]
#[serde(rename = "r_stolen_base_home")]
StolenBaseHome,
#[display("Caught Stealing (2B)")]
#[serde(rename = "r_caught_stealing_2b")]
CaughtStealing2B,
#[display("Caught Stealing (3B)")]
#[serde(rename = "r_caught_stealing_3b")]
CaughtStealing3B,
#[display("Caught Stealing (HP)")]
#[serde(rename = "r_caught_stealing_home")]
CaughtStealingHome,
#[display("Pickoff (1B)")]
#[serde(rename = "r_pickoff_1b")]
Pickoff1B,
#[display("Pickoff (2B)")]
#[serde(rename = "r_pickoff_2b")]
Pickoff2B,
#[display("Pickoff (3B)")]
#[serde(rename = "r_pickoff_3b")]
Pickoff3B,
#[display("Pickoff (Error) (1B)")]
#[serde(rename = "r_pickoff_error_1b")]
PickoffError1B,
#[display("Pickoff (Error) (2B)")]
#[serde(rename = "r_pickoff_error_2b")]
PickoffError2B,
#[display("Pickoff (Error) (3B)")]
#[serde(rename = "r_pickoff_error_3b")]
PickoffError3B,
#[display("Pickoff (Caught Stealing) (2B)")]
#[serde(rename = "r_pickoff_caught_stealing_2b")]
PickoffCaughtStealing2B,
#[display("Pickoff (Caught Stealing) (3B)")]
#[serde(rename = "r_pickoff_caught_stealing_3b")]
PickoffCaughtStealing3B,
#[display("Pickoff (Caught Stealing) (HP)")]
#[serde(rename = "r_pickoff_caught_stealing_home")]
PickoffCaughtStealingHome,
#[display("Interference")]
#[serde(rename = "r_interference")]
Interference,
#[display("Hit By Ball")]
#[serde(rename = "r_hbr")]
HitByBall,
}
impl MovementReason {
#[must_use]
pub const fn is_pickoff(self) -> bool {
matches!(self, Self::Pickoff1B | Self::Pickoff2B | Self::Pickoff3B | Self::PickoffError1B | Self::PickoffError2B | Self::PickoffError3B | Self::PickoffCaughtStealing2B | Self::PickoffCaughtStealing3B | Self::PickoffCaughtStealingHome)
}
#[must_use]
pub const fn is_stolen_base_attempt(self) -> bool {
matches!(self, Self::StolenBase2B | Self::StolenBase3B | Self::StolenBaseHome | Self::CaughtStealing2B | Self::CaughtStealing3B | Self::CaughtStealingHome | Self::PickoffCaughtStealing2B | Self::PickoffCaughtStealing3B | Self::PickoffCaughtStealingHome)
}
#[must_use]
pub const fn is_stolen_base(self) -> bool {
matches!(self, Self::StolenBase2B | Self::StolenBase3B | Self::StolenBaseHome)
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct RunnerCredit {
pub player: PersonId,
pub position: NamedPosition,
pub credit: CreditKind,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display)]
pub enum CreditKind {
#[display("Putout")]
#[serde(rename = "f_putout")]
Putout,
#[display("Assist")]
#[serde(rename = "f_assist")]
Assist,
#[display("Outfield Assist")]
#[serde(rename = "f_assist_of")]
OutfieldAssist,
#[display("Fielded Ball")]
#[serde(rename = "f_fielded_ball")]
FieldedBall,
#[display("Fielding Error")]
#[serde(rename = "f_fielding_error")]
FieldingError,
#[display("Throwing Error")]
#[serde(rename = "f_throwing_error")]
ThrowingError,
#[display("Deflection")]
#[serde(rename = "f_deflection")]
Deflection,
#[display("Touch")]
#[serde(rename = "f_touch")]
Touch,
#[display("Dropped Ball Error")]
#[serde(rename = "f_error_dropped_ball")]
DroppedBallError,
#[display("Defensive Shift Violation")]
#[serde(rename = "f_defensive_shift_violation_error")]
DefensiveShiftViolation,
#[display("Interference")]
#[serde(rename = "f_interference")]
Interference,
#[display("Catcher's Interference")]
#[serde(rename = "c_catcher_interf")]
CatchersInterference,
}
pub fn deserialize_review_data<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<ReviewData>, D::Error> {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
struct RawReviewData {
#[serde(flatten)]
base: ReviewData,
#[serde(default)]
additional_reviews: Vec<ReviewData>,
}
let RawReviewData { base, mut additional_reviews } = RawReviewData::deserialize(deserializer)?;
additional_reviews.insert(0, base);
Ok(additional_reviews)
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct ReviewData {
pub is_overturned: bool,
#[serde(rename = "inProgress")]
pub is_in_progress: bool,
pub review_type: ReviewReasonId,
#[serde(alias = "challengeTeamId")]
pub challenging_team: Option<TeamId>,
pub player: Option<NamedPerson>,
}
#[allow(clippy::large_enum_variant, reason = "not a problemo dw")]
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all_fields = "camelCase", tag = "type")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub enum PlayEvent {
#[serde(rename = "action")]
Action {
details: ActionPlayDetails,
#[serde(rename = "actionPlayId")]
play_id: Option<Uuid>,
#[serde(flatten)]
common: PlayEventCommon,
},
#[serde(rename = "pitch")]
Pitch {
details: PitchPlayDetails,
pitch_data: Option<PitchData>,
hit_data: Option<HitData>,
#[serde(rename = "pitchNumber")]
pitch_ordinal: usize,
play_id: Uuid,
#[serde(flatten)]
common: PlayEventCommon,
},
#[serde(rename = "stepoff")]
Stepoff {
details: StepoffPlayDetails,
play_id: Option<Uuid>,
#[serde(flatten)]
common: PlayEventCommon,
},
#[serde(rename = "no_pitch")]
NoPitch {
details: NoPitchPlayDetails,
play_id: Option<Uuid>,
#[serde(rename = "pitchNumber", default)]
pitch_ordinal: usize,
#[serde(flatten)]
common: PlayEventCommon,
},
#[serde(rename = "pickoff")]
Pickoff {
details: PickoffPlayDetails,
#[serde(alias = "actionPlayId", default)]
play_id: Option<Uuid>,
#[serde(flatten)]
common: PlayEventCommon,
}
}
impl Deref for PlayEvent {
type Target = PlayEventCommon;
fn deref(&self) -> &Self::Target {
let (Self::Action { common, .. } | Self::Pitch { common, .. } | Self::Stepoff { common, .. } | Self::NoPitch { common, .. } | Self::Pickoff { common, .. }) = self;
common
}
}
impl DerefMut for PlayEvent {
fn deref_mut(&mut self) -> &mut Self::Target {
let (Self::Action { common, .. } | Self::Pitch { common, .. } | Self::Stepoff { common, .. } | Self::NoPitch { common, .. } | Self::Pickoff { common, .. }) = self;
common
}
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PlayEventCommon {
pub count: AtBatCount,
#[serde(rename = "startTime", deserialize_with = "crate::deserialize_datetime")]
pub start_timestamp: DateTime<Utc>,
#[serde(rename = "endTime", deserialize_with = "crate::deserialize_datetime")]
pub end_timestamp: DateTime<Utc>,
pub is_pitch: bool,
#[serde(rename = "isBaseRunningPlay", default)]
pub is_baserunning_play: bool,
#[serde(default)]
pub is_substitution: bool,
pub player: Option<PersonId>,
pub umpire: Option<PersonId>,
pub position: Option<NamedPosition>,
pub replaced_player: Option<PersonId>,
#[serde(rename = "battingOrder")]
pub batting_order_index: Option<BattingOrderIndex>,
pub base: Option<Base>,
#[serde(rename = "reviewDetails", default, deserialize_with = "deserialize_review_data")]
pub reviews: Vec<ReviewData>,
pub injury_type: Option<String>,
#[doc(hidden)]
#[serde(rename = "index", default)]
pub __index: IgnoredAny,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct ActionPlayDetails {
#[serde(rename = "eventType")]
pub event: EventType,
pub description: String,
pub away_score: usize,
pub home_score: usize,
pub is_out: bool,
pub is_scoring_play: bool,
#[serde(default)]
pub has_review: bool,
#[serde(rename = "disengagementNum", default)]
pub disengagements: Option<NonZeroUsize>,
#[doc(hidden)]
#[serde(rename = "event", default)]
pub __event: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "type", default)]
pub __type: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "violation", default)]
pub __violation: IgnoredAny,
}
#[allow(clippy::struct_excessive_bools, reason = "inapplicable")]
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct PitchPlayDetails {
pub is_in_play: bool,
pub is_strike: bool,
pub is_ball: bool,
pub is_out: bool,
#[serde(default)]
pub has_review: bool,
#[serde(default)]
pub runner_going: bool,
#[serde(rename = "disengagementNum", default)]
pub disengagements: Option<NonZeroUsize>,
#[serde(rename = "type", deserialize_with = "crate::meta::fallback_pitch_type_deserializer", default = "crate::meta::unknown_pitch_type")]
pub pitch_type: PitchType,
pub call: PitchCodeId,
#[doc(hidden)]
#[serde(rename = "ballColor", default)]
pub __ball_color: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "trailColor", default)]
pub __trail_color: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "description", default)]
pub __description: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "code", default)]
pub __code: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "violation", default)]
pub __violation: IgnoredAny,
}
#[allow(clippy::struct_excessive_bools, reason = "inapplicable")]
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct StepoffPlayDetails {
pub description: String,
pub code: PitchCodeId,
pub is_out: bool,
#[serde(default)]
pub has_review: bool,
pub from_catcher: bool,
#[serde(rename = "disengagementNum", default)]
pub disengagements: Option<NonZeroUsize>,
#[doc(hidden)]
#[serde(rename = "violation", default)]
pub __violation: IgnoredAny,
}
#[allow(clippy::struct_excessive_bools, reason = "inapplicable")]
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct NoPitchPlayDetails {
#[serde(default)]
pub is_in_play: bool,
#[serde(default)]
pub is_strike: bool,
#[serde(default)]
pub is_ball: bool,
pub is_out: bool,
#[serde(default)]
pub has_review: bool,
#[serde(default)]
pub runner_going: bool,
#[serde(default = "crate::meta::unknown_pitch_code")]
pub call: PitchCodeId,
#[serde(rename = "disengagementNum", default)]
pub disengagements: Option<NonZeroUsize>,
#[doc(hidden)]
#[serde(rename = "description", default)]
pub __description: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "code", default)]
pub __code: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "violation", default)]
pub __violation: IgnoredAny,
}
#[allow(clippy::struct_excessive_bools, reason = "inapplicable")]
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct PickoffPlayDetails {
pub description: String,
pub code: PitchCodeId,
pub is_out: bool,
#[serde(default)]
pub has_review: bool,
pub from_catcher: bool,
#[serde(rename = "disengagementNum", default)]
pub disengagements: Option<NonZeroUsize>,
#[doc(hidden)]
#[serde(rename = "violation", default)]
pub __violation: IgnoredAny,
}
#[allow(non_snake_case, reason = "spec")]
#[derive(Debug, PartialEq, Copy, Clone)]
pub struct PitchData {
pub release_speed: f64,
pub plate_speed: f64,
pub sz_bot: f64,
pub sz_top: f64,
pub sz_wid: f64,
pub sz_dep: f64,
pub aX: f64,
pub aY: f64,
pub aZ: f64,
pub pfxX: f64,
pub pfxZ: f64,
pub pX: f64,
pub pZ: f64,
pub vX0: f64,
pub vY0: f64,
pub vZ0: f64,
pub x0: f64,
pub y0: f64,
pub z0: f64,
pub x: f64,
pub y: f64,
pub break_angle: f64,
pub break_length: f64,
pub induced_vertical_movement: f64,
pub vertical_drop: f64,
pub horizontal_movement: f64,
pub depth_break: f64,
pub spin_rate: f64,
pub spin_axis: f64,
pub zone: StrikeZoneSection,
pub type_confidence: f64,
pub time_to_plate: f64,
pub extension: f64,
}
impl<'de> Deserialize<'de> for PitchData {
#[allow(clippy::too_many_lines, reason = "deserialization is hard")]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>
{
#[must_use]
const fn default_strike_zone_width() -> f64 {
17.0
}
#[must_use]
const fn default_strike_zone_depth() -> f64 {
17.0
}
#[must_use]
const fn default_nan() -> f64 {
f64::NAN
}
#[must_use]
const fn default_strike_zone_section() -> StrikeZoneSection {
StrikeZoneSection::MiddleMiddle
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
struct Raw {
#[serde(default = "default_nan")]
start_speed: f64,
#[serde(default = "default_nan")]
end_speed: f64,
#[serde(default = "default_nan")]
strike_zone_top: f64,
#[serde(default = "default_nan")]
strike_zone_bottom: f64,
#[serde(default = "default_strike_zone_width")]
strike_zone_width: f64,
#[serde(default = "default_strike_zone_depth")]
strike_zone_depth: f64,
coordinates: RawCoordinates,
breaks: RawBreaks,
#[serde(default = "default_strike_zone_section")]
zone: StrikeZoneSection,
#[serde(default = "default_nan")]
type_confidence: f64,
#[serde(default = "default_nan")]
plate_time: f64,
#[serde(default = "default_nan")]
extension: f64,
}
#[allow(non_snake_case, reason = "spec")]
#[derive(Deserialize)]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
struct RawCoordinates {
#[serde(default = "default_nan")]
aX: f64,
#[serde(default = "default_nan")]
aY: f64,
#[serde(default = "default_nan")]
aZ: f64,
#[serde(default = "default_nan")]
pfxX: f64,
#[serde(default = "default_nan")]
pfxZ: f64,
#[serde(default = "default_nan")]
pX: f64,
#[serde(default = "default_nan")]
pZ: f64,
#[serde(default = "default_nan")]
vX0: f64,
#[serde(default = "default_nan")]
vY0: f64,
#[serde(default = "default_nan")]
vZ0: f64,
#[serde(default = "default_nan")]
x: f64,
#[serde(default = "default_nan")]
y: f64,
#[serde(default = "default_nan")]
x0: f64,
#[serde(default = "default_nan")]
y0: f64,
#[serde(default = "default_nan")]
z0: f64,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
struct RawBreaks {
#[serde(default = "default_nan")]
break_angle: f64,
#[serde(default = "default_nan")]
break_length: f64,
#[serde(default = "default_nan")]
break_y: f64,
#[serde(default = "default_nan")]
break_vertical: f64,
#[serde(default = "default_nan")]
break_vertical_induced: f64,
#[serde(default = "default_nan")]
break_horizontal: f64,
#[serde(default = "default_nan")]
spin_rate: f64,
#[serde(default = "default_nan")]
spin_direction: f64,
}
let Raw {
start_speed,
end_speed,
strike_zone_top,
strike_zone_bottom,
strike_zone_width,
strike_zone_depth,
coordinates: RawCoordinates {
pfxX,
pfxZ,
aX,
aY,
aZ,
pX,
pZ,
vX0,
vY0,
vZ0,
x,
y,
x0,
y0,
z0,
},
breaks: RawBreaks {
break_angle,
break_length,
break_y,
break_vertical,
break_vertical_induced,
break_horizontal,
spin_rate,
spin_direction,
},
zone,
type_confidence,
plate_time,
extension,
} = Raw::deserialize(deserializer)?;
Ok(Self {
release_speed: start_speed,
plate_speed: end_speed,
sz_bot: strike_zone_bottom,
sz_top: strike_zone_top,
sz_wid: strike_zone_width,
sz_dep: strike_zone_depth,
aX,
aY,
aZ,
pfxX,
pfxZ,
pX,
pZ,
vX0,
vY0,
vZ0,
x0,
y0,
z0,
horizontal_movement: break_horizontal,
x,
y,
break_angle,
break_length,
induced_vertical_movement: break_vertical_induced,
vertical_drop: break_vertical,
depth_break: break_y,
spin_rate,
spin_axis: spin_direction,
zone,
type_confidence,
time_to_plate: plate_time,
extension,
})
}
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(from = "__HitDataStruct")]
pub struct HitData {
pub hit_trajectory: Option<HitTrajectory>,
pub contact_hardness: Option<ContactHardness>,
pub statcast: Option<StatcastHitData>,
}
#[serde_as]
#[doc(hidden)]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
struct __HitDataStruct {
#[serde_as(deserialize_as = "DefaultOnError")]
#[serde(rename = "trajectory", default)]
hit_trajectory: Option<HitTrajectory>,
#[serde(rename = "hardness", default)]
contact_hardness: Option<ContactHardness>,
#[serde(flatten, default)]
statcast: Option<StatcastHitData>,
#[doc(hidden)]
#[serde(rename = "location", default)]
__location: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "coordinates")]
__coordinates: IgnoredAny,
}
impl From<__HitDataStruct> for HitData {
fn from(__HitDataStruct { hit_trajectory, contact_hardness, statcast, .. }: __HitDataStruct) -> Self {
Self {
hit_trajectory: hit_trajectory.or_else(|| statcast.as_ref().map(|statcast| statcast.launch_angle).map(HitTrajectory::from_launch_angle)),
contact_hardness,
statcast,
}
}
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct StatcastHitData {
#[serde(rename = "launchSpeed")]
pub exit_velocity: f64,
pub launch_angle: f64,
#[serde(rename = "totalDistance")]
pub distance: f64,
}
#[derive(Builder)]
#[builder(derive(Into))]
pub struct PlayByPlayRequest {
#[builder(into)]
id: GameId,
}
impl<S: play_by_play_request_builder::State + play_by_play_request_builder::IsComplete> crate::request::RequestURLBuilderExt for PlayByPlayRequestBuilder<S> {
type Built = PlayByPlayRequest;
}
impl Display for PlayByPlayRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "http://statsapi.mlb.com/api/v1/game/{}/playByPlay", self.id)
}
}
impl RequestURL for PlayByPlayRequest {
type Response = Plays;
}
#[cfg(test)]
mod tests {
use crate::TEST_YEAR;
use crate::game::PlayByPlayRequest;
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_pbp() {
let _ = PlayByPlayRequest::builder().id(813_024).build_and_get().await.unwrap();
}
#[tokio::test]
async fn postseason_pbp() {
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) = PlayByPlayRequest::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_pbp() {
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) = PlayByPlayRequest::builder().id(game.game_id).build_and_get().await {
dbg!(e);
has_errors = true;
}
}
assert!(!has_errors, "Has errors.");
}
}