Skip to main content

mlb_api/requests/game/
plays.rs

1use std::{fmt::Display, num::NonZeroUsize, ops::{Deref, DerefMut}};
2
3use bon::Builder;
4use chrono::NaiveDateTime;
5use derive_more::{Deref, DerefMut, Display};
6use serde::{Deserialize, Deserializer, de::IgnoredAny};
7use serde_with::{serde_as, DefaultOnNull, DefaultOnError};
8use uuid::Uuid;
9
10use 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};
11
12/// A collection of plays, often a whole game's worth.
13#[allow(clippy::struct_field_names, clippy::unsafe_derive_deserialize, reason = "not relevant here")]
14#[derive(Debug, Deserialize, PartialEq, Clone, Deref)]
15#[serde(rename_all = "camelCase")]
16#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
17pub struct Plays {
18    #[serde(default)]
19    pub copyright: Copyright,
20    
21    #[deref]
22    #[serde(rename = "allPlays")]
23    plays: Vec<Play>,
24
25    /// Unlinked from the `plays` list
26    pub current_play: Play,
27
28    #[serde(rename = "scoringPlays")]
29    pub(super) scoring_play_indices: Vec<usize>,
30
31    #[serde(rename = "playsByInning")]
32    pub(super) play_indices_by_inning: Vec<InningPlaysIndices>,
33}
34
35impl Plays {
36    /// Gives a mutable refernece to the underlying plays.
37    ///
38    /// # Safety
39    /// [`Self::scroing_plays`], [`Self::by_inning`] and [`Self::by_inning_halves`] use caches for these plays, if mutated, these caches will be outdated.
40    #[must_use]
41    pub const unsafe fn plays_mut(&mut self) -> &mut Vec<Play> {
42        &mut self.plays
43    }
44
45    /// Reduces this type into the underlying [`Vec<Play>`]
46    #[must_use]
47    pub fn into_plays(self) -> Vec<Play> {
48        self.plays
49    }
50
51    /// Iterator over a list of scoring plays.
52    ///
53    /// ## Examples
54    /// ```no_run
55    /// let plays: Plays = ...;
56    ///
57    /// for play in plays.scoring_plays() {
58    ///     dbg!(play);
59    /// }
60    /// ```
61    pub fn scoring_plays(&self) -> impl Iterator<Item=&Play> {
62        self.scoring_play_indices.iter()
63            .filter_map(|&idx| self.plays.get(idx))
64    }
65
66    /// Iterator of plays by inning
67    ///
68    /// ## Examples
69    /// ```no_run
70    /// let plays: Plays = ...;
71    ///
72    /// for plays in plays.by_inning() {
73    ///     for play in plays {
74    ///         dbg!(play);
75    ///     }
76    /// }
77    /// ```
78    pub fn by_inning(&self) -> impl Iterator<Item=impl Iterator<Item=&Play>> {
79        self.play_indices_by_inning.iter()
80            .map(|inning| (inning.start..=inning.end)
81                .filter_map(|idx| self.plays.get(idx)))
82    }
83
84    /// Iterator of plays by inning halves. (top then bottom)
85    ///
86    /// ## Examples
87    /// ```no_run
88    /// let plays: Plays = ...;
89    ///
90    /// for (top, bottom) in plays.by_inning_halves() {
91    ///     for play in top {
92    ///         dbg!(play);
93    ///     }
94    ///     for play in bottom {
95    ///         dbg!(play);
96    ///     }
97    /// }
98    /// ```
99    pub fn by_inning_halves(&self) -> impl Iterator<Item=(impl Iterator<Item=&Play>, impl Iterator<Item=&Play>)> {
100        self.play_indices_by_inning.iter()
101            .map(|inning| (
102                inning.top_indices.iter().filter_map(|&idx| self.plays.get(idx)),
103                inning.bottom_indices.iter().filter_map(|&idx| self.plays.get(idx))
104            ))
105    }
106}
107
108#[derive(Debug, Deserialize, PartialEq, Clone)]
109#[serde(rename_all = "camelCase")]
110#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
111pub(super) struct InningPlaysIndices {
112    #[serde(rename = "startIndex")]
113    pub(super) start: usize,
114    #[serde(rename = "endIndex")]
115    pub(super) end: usize,
116    #[serde(rename = "top")]
117    pub(super) top_indices: Vec<usize>,
118    #[serde(rename = "bottom")]
119    pub(super) bottom_indices: Vec<usize>,
120    #[doc(hidden)]
121    #[serde(rename = "hits", default)]
122    pub(super) __balls_in_play: IgnoredAny,
123}
124
125/// The play(s) within an "At-Bat"
126///
127/// For individual "plays" and actions, look to [`PlayEvent`]s
128#[derive(Debug, Deserialize, PartialEq, Clone)]
129#[serde(rename_all = "camelCase")]
130#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
131pub struct Play {
132    /// See [`PlayDetails`].
133    pub result: PlayDetails,
134    /// See [`PlayAbout`].
135    pub about: PlayAbout,
136    /// Active count in the at-bat.
137    pub count: AtBatCount,
138    /// See [`PlayMatchup`].
139    pub matchup: PlayMatchup,
140    /// See [`PlayEvent`].
141    pub play_events: Vec<PlayEvent>,
142    pub runners: Vec<RunnerData>,
143    #[serde(rename = "reviewDetails", default, deserialize_with = "deserialize_review_data")]
144    pub reviews: Vec<ReviewData>,
145    
146    /// Timestamp at which the [`Play`] is called complete.
147    #[serde(rename = "playEndTime", deserialize_with = "crate::deserialize_datetime")]
148    pub play_end_timestamp: NaiveDateTime,
149
150    #[doc(hidden)]
151    #[serde(rename = "pitchIndex", default)]
152    pub __pitch_indices: IgnoredAny,
153    #[doc(hidden)]
154    #[serde(rename = "actionIndex", default)]
155    pub __action_indices: IgnoredAny,
156    #[doc(hidden)]
157    #[serde(rename = "runnerIndex", default)]
158    pub __runner_indices: IgnoredAny,
159    #[doc(hidden)]
160    #[serde(rename = "atBatIndex", default)]
161    pub __at_bat_index: IgnoredAny,
162}
163
164/// The result of a play, such as a Strikeout, Home Run, etc.
165#[derive(Debug, Deserialize, PartialEq, Clone)]
166#[serde(rename_all = "camelCase")]
167#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
168pub struct PlayDetails {
169    #[serde(flatten, default)]
170    pub completed_play_details: Option<CompletedPlayDetails>,
171    
172    /// Score as of the end of the play
173    pub away_score: usize,
174    /// Score as of the end of the play
175    pub home_score: usize,
176
177    #[doc(hidden)]
178    #[serde(rename = "event", default)]
179    pub __event: IgnoredAny,
180
181    #[doc(hidden)]
182    #[serde(rename = "type", default)]
183    pub __type: IgnoredAny,
184}
185
186/// Information supplied to [`PlayDetails`] when the play is complete
187#[derive(Debug, Deserialize, PartialEq, Clone)]
188#[serde(rename_all = "camelCase")]
189#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
190pub struct CompletedPlayDetails {
191    #[serde(rename = "eventType")]
192    pub event: EventType,
193    /// Shohei Ohtani strikes out swinging.
194    pub description: String,
195    /// Runs batted in
196    pub rbi: usize,
197    /// Whether the batter in the play is out
198    pub is_out: bool,
199}
200
201/// Miscallaneous data regarding a play
202#[allow(clippy::struct_excessive_bools, reason = "inapplicable")]
203#[derive(Debug, Deserialize, PartialEq, Clone)]
204#[serde(rename_all = "camelCase")]
205#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
206pub struct PlayAbout {
207    /// Ordinal at bat of the game (starts at 0)
208    #[serde(rename = "atBatIndex")]
209    pub at_bat_idx: usize,
210
211    /// The inning half this play is in
212    #[serde(rename = "halfInning")]
213    pub inning_half: InningHalf,
214
215    /// The inning this play is in
216    pub inning: Inning,
217
218    /// The timestamp that this play begins; includes milliseconds
219    #[serde(rename = "startTime", deserialize_with = "crate::deserialize_datetime")]
220    pub start_timestamp: NaiveDateTime,
221
222    /// The timestamp that this play ends at; includes milliseconds
223    #[serde(rename = "endTime", deserialize_with = "crate::deserialize_datetime")]
224    pub end_timestamp: NaiveDateTime,
225
226    /// Whether the play is "complete" or not, i.e. opposite of ongoing.
227    ///
228    /// Once a play is complete it cannot be edited
229    pub is_complete: bool,
230
231    /// Whether the play itself is scoring, such as a Home Run.
232    ///
233    /// Note that [`Play`]s that include [`PlayEvent`]s that score runs that are not part of the [`Play`] (such as stealing home) do not indicate this as true.
234    ///
235    /// This is the predicate for [`Plays::scoring_plays`]
236    pub is_scoring_play: Option<bool>,
237
238    /// Whether the play has a replay review occur. Note that At-Bats can have multiple challenges occur.
239    #[serde(default)]
240    pub has_review: bool,
241
242    /// Whether the play has counted towards an out so far.
243    ///
244    /// todo: check if includes play events like pickoffs.
245    #[serde(default)]
246    pub has_out: bool,
247
248    /// Ordinal ranking for +/- WPA effect.
249    ///
250    /// `1` means largest effect on WPA,
251    /// `2` means second most, etc.
252    #[serde(default)]
253    pub captivating_index: usize,
254
255    #[doc(hidden)]
256    #[serde(rename = "isTopInning")]
257    pub __is_top_inning: IgnoredAny,
258}
259
260/// Hitter & Pitcher matchup information
261#[derive(Debug, Deserialize, PartialEq, Clone)]
262#[serde(rename_all = "camelCase")]
263#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
264pub struct PlayMatchup {
265    pub batter: NamedPerson,
266    pub pitcher: NamedPerson,
267    /// Cannot change during the play
268    pub bat_side: Handedness,
269    /// Cannot change during the play
270    pub pitch_hand: Handedness,
271    pub post_on_first: Option<NamedPerson>,
272    pub post_on_second: Option<NamedPerson>,
273    pub post_on_third: Option<NamedPerson>,
274
275    #[doc(hidden)]
276    #[serde(rename = "batterHotColdZones", default)]
277    pub __batter_hot_cold_zones: IgnoredAny,
278    #[doc(hidden)]
279    #[serde(rename = "pitcherHotColdZones", default)]
280    pub __pitcher_hot_cold_zones: IgnoredAny,
281    #[doc(hidden)]
282    #[serde(rename = "batterHotColdZoneStats", default)]
283    pub __batter_hot_cold_zone_stats: IgnoredAny,
284    #[doc(hidden)]
285    #[serde(rename = "pitcherHotColdZoneStats", default)]
286    pub __pitcher_hot_cold_zone_stats: IgnoredAny,
287    
288    // pub batter_hot_cold_zones: HittingHotColdZones,
289    // pub pitcher_hot_cold_zones: PitchingHotColdZones,
290
291    pub splits: ApplicablePlayMatchupSplits,
292}
293
294/// Batter, Pitcher, and Men-On-Base splits; unknown type.
295#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
296#[serde(rename_all = "camelCase")]
297#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
298pub struct ApplicablePlayMatchupSplits {
299    pub batter: String,
300    pub pitcher: String,
301    pub men_on_base: String,
302}
303
304/// Data regarding a baserunner.
305#[derive(Debug, Deserialize, PartialEq, Clone)]
306#[serde(rename_all = "camelCase")]
307#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
308pub struct RunnerData {
309    pub movement: RunnerMovement,
310    pub details: RunnerDetails,
311    #[serde(default)]
312    pub credits: Vec<RunnerCredit>,
313}
314
315/// Data regarding the basepath of a runner
316#[serde_as]
317#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
318#[serde(rename_all = "camelCase")]
319#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
320pub struct RunnerMovement {
321    /// The base the runner begins the play at. `None` if they do not start on-base at the beginning of the play.
322    pub origin_base: Option<Base>,
323
324    /// Unsure how it is different from ``origin_base``
325    #[serde(rename = "start")]
326    pub start_base: Option<Base>,
327
328    /// The latest base the runner is called "safe" at. `None` if the runner was never safe at any base.
329    #[serde(rename = "end")]
330    pub end_base: Option<Base>,
331
332    /// The base the runner was called out at. `None` if the runner was never called out.
333    pub out_base: Option<Base>,
334
335    /// Identical to `out_base.is_some()`
336    #[serde_as(deserialize_as = "DefaultOnNull")]
337    #[serde(default)]
338    pub is_out: bool,
339    
340    /// Ordinal of out in the game. `None` if the runner was not called out. Otherwise 1, 2, or 3.
341    pub out_number: Option<usize>,
342}
343
344/// Details about the runner's movement
345#[derive(Debug, Deserialize, PartialEq, Clone)]
346#[serde(rename_all = "camelCase")]
347#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
348pub struct RunnerDetails {
349    /// `None` represents completely unforced movement, such as hitting a single with no-one on.
350    pub movement_reason: Option<MovementReason>,
351    pub runner: NamedPerson,
352    pub is_scoring_event: bool,
353    #[serde(rename = "rbi")]
354    pub is_rbi: bool,
355    #[serde(rename = "earned")]
356    pub is_earned: bool,
357
358    // Same as [`PlayDetails`].event
359    #[doc(hidden)]
360    #[serde(rename = "eventType", default)]
361    pub __event_tyoe: IgnoredAny,
362
363    #[doc(hidden)]
364    #[serde(rename = "event", default)]
365    pub __event_type: IgnoredAny,
366
367    #[doc(hidden)]
368    #[serde(rename = "responsiblePitcher", default)]
369    pub __responsible_pitcher: IgnoredAny,
370
371    #[doc(hidden)]
372    #[serde(rename = "teamUnearned", default)]
373    pub __team_unearned: IgnoredAny,
374    
375    #[doc(hidden)]
376    #[serde(rename = "playIndex", default)]
377    pub __play_index: IgnoredAny,
378}
379
380/// Reasons for baserunner movement
381#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display)]
382pub enum MovementReason {
383    //// Unforced base advancement, such as going first to third on a single.
384    #[display("Unforced Base Advancement")]
385    #[serde(rename = "r_adv_play")]
386    AdvancementUnforced,
387    
388    /// Forced base advancement, such as moving up one base on a single.
389    #[display("Forced Base Advancement")]
390    #[serde(rename = "r_adv_force")]
391    AdancementForced,
392
393    /// Advancement from a choice in throwing, such as throwing home and allowing this runner to move up a base instead.
394    #[display("Advancement from Throw")]
395    #[serde(rename = "r_adv_throw")]
396    AdvancementThrow,
397
398    /// Runner fails to tag up and is forced out.
399    #[display("Doubled Off")]
400    #[serde(rename = "r_doubled_off")]
401    DoubledOff,
402
403    #[display("Thrown Out")]
404    #[serde(rename = "r_thrown_out")]
405    ThrownOut,
406
407    #[display("Called Out Returning")]
408    #[serde(rename = "r_out_returning")]
409    CalledOutReturning,
410        
411    /// Standard force-out.
412    #[display("Forced Out")]
413    #[serde(rename = "r_force_out")]
414    ForceOut,
415
416    /// Deviation from the basepath, etc.
417    #[display("Runner Called Out")]
418    #[serde(rename = "r_runner_out")]
419    RunnerCalledOut,
420
421    /// A Stolen Base with no throw.
422    #[display("Defensive Indifference")]
423    #[serde(rename = "r_defensive_indiff")]
424    DefensiveIndifference,
425
426    #[display("Rundown")]
427    #[serde(rename = "r_rundown")]
428    Rundown,
429
430    /// Stretching a single into a double.
431    #[display("Out Stretching")]
432    #[serde(rename = "r_out_stretching")]
433    OutStretching,
434
435    #[display("Stolen Base (2B)")]
436    #[serde(rename = "r_stolen_base_2b")]
437    StolenBase2B,
438
439    #[display("Stolen Base (3B)")]
440    #[serde(rename = "r_stolen_base_3b")]
441    StolenBase3B,
442
443    #[display("Stolen Base (HP)")]
444    #[serde(rename = "r_stolen_base_home")]
445    StolenBaseHome,
446
447    #[display("Caught Stealing (2B)")]
448    #[serde(rename = "r_caught_stealing_2b")]
449    CaughtStealing2B,
450
451    #[display("Caught Stealing (3B)")]
452    #[serde(rename = "r_caught_stealing_3b")]
453    CaughtStealing3B,
454
455    #[display("Caught Stealing (HP)")]
456    #[serde(rename = "r_caught_stealing_home")]
457    CaughtStealingHome,
458    
459    /// Successful pickoff
460    #[display("Pickoff (1B)")]
461    #[serde(rename = "r_pickoff_1b")]
462    Pickoff1B,
463    
464    /// Successful pickoff
465    #[display("Pickoff (2B)")]
466    #[serde(rename = "r_pickoff_2b")]
467    Pickoff2B,
468    
469    /// Successful pickoff
470    #[display("Pickoff (3B)")]
471    #[serde(rename = "r_pickoff_3b")]
472    Pickoff3B,
473    
474    #[display("Pickoff (Error) (1B)")]
475    #[serde(rename = "r_pickoff_error_1b")]
476    PickoffError1B,
477    
478    #[display("Pickoff (Error) (2B)")]
479    #[serde(rename = "r_pickoff_error_2b")]
480    PickoffError2B,
481    
482    #[display("Pickoff (Error) (3B)")]
483    #[serde(rename = "r_pickoff_error_3b")]
484    PickoffError3B,
485
486    #[display("Pickoff (Caught Stealing) (2B)")]
487    #[serde(rename = "r_pickoff_caught_stealing_2b")]
488    PickoffCaughtStealing2B,
489    
490    #[display("Pickoff (Caught Stealing) (3B)")]
491    #[serde(rename = "r_pickoff_caught_stealing_3b")]
492    PickoffCaughtStealing3B,
493    
494    #[display("Pickoff (Caught Stealing) (HP)")]
495    #[serde(rename = "r_pickoff_caught_stealing_home")]
496    PickoffCaughtStealingHome,
497
498    /// General interference
499    #[display("Interference")]
500    #[serde(rename = "r_interference")]
501    Interference,
502
503    #[display("Hit By Ball")]
504    #[serde(rename = "r_hbr")]
505    HitByBall,
506}
507
508impl MovementReason {
509    /// If the movement reason is a pickoff
510    #[must_use]
511    pub const fn is_pickoff(self) -> bool {
512        matches!(self, Self::Pickoff1B | Self::Pickoff2B | Self::Pickoff3B | Self::PickoffError1B | Self::PickoffError2B | Self::PickoffError3B | Self::PickoffCaughtStealing2B | Self::PickoffCaughtStealing3B | Self::PickoffCaughtStealingHome)
513    }
514
515    /// If the movement reason is a stolen base attempt
516    #[must_use]
517    pub const fn is_stolen_base_attempt(self) -> bool {
518        matches!(self, Self::StolenBase2B | Self::StolenBase3B | Self::StolenBaseHome | Self::CaughtStealing2B | Self::CaughtStealing3B | Self::CaughtStealingHome | Self::PickoffCaughtStealing2B | Self::PickoffCaughtStealing3B | Self::PickoffCaughtStealingHome)
519    }
520
521    /// If the movement reason is a stolen base
522    #[must_use]
523    pub const fn is_stolen_base(self) -> bool {
524        matches!(self, Self::StolenBase2B | Self::StolenBase3B | Self::StolenBaseHome)
525    }
526}
527
528/// Fielder credits to outs
529#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
530#[serde(rename_all = "camelCase")]
531#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
532pub struct RunnerCredit {
533    pub player: PersonId,
534    pub position: NamedPosition,
535    pub credit: CreditKind,
536}
537
538/// Statistical credits to fielders; putouts, assists, etc.
539#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display)]
540pub enum CreditKind {
541    #[display("Putout")]
542    #[serde(rename = "f_putout")]
543    Putout,
544    
545    #[display("Assist")]
546    #[serde(rename = "f_assist")]
547    Assist,
548    
549    #[display("Outfield Assist")]
550    #[serde(rename = "f_assist_of")]
551    OutfieldAssist,
552    
553    /// The fielder just, fielded the ball, no outs or anything.
554    #[display("Fielded Ball")]
555    #[serde(rename = "f_fielded_ball")]
556    FieldedBall,
557    
558    #[display("Fielding Error")]
559    #[serde(rename = "f_fielding_error")]
560    FieldingError,
561    
562    #[display("Throwing Error")]
563    #[serde(rename = "f_throwing_error")]
564    ThrowingError,
565    
566    #[display("Deflection")]
567    #[serde(rename = "f_deflection")]
568    Deflection,
569
570    /// They literally touched it, no deflection, just a tap.
571    #[display("Touch")]
572    #[serde(rename = "f_touch")]
573    Touch,
574
575    #[display("Dropped Ball Error")]
576    #[serde(rename = "f_error_dropped_ball")]
577    DroppedBallError,
578
579    #[display("Defensive Shift Violation")]
580    #[serde(rename = "f_defensive_shift_violation_error")]
581    DefensiveShiftViolation,
582
583    #[display("Interference")]
584    #[serde(rename = "f_interference")]
585    Interference,
586
587    #[display("Catcher's Interference")]
588    #[serde(rename = "c_catcher_interf")]
589    CatchersInterference,
590}
591
592/// # Errors
593/// See D::Error, likely [`serde_json::Error`]
594pub fn deserialize_review_data<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<ReviewData>, D::Error> {
595    #[derive(Deserialize)]
596    #[serde(rename_all = "camelCase")]
597    #[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
598    struct RawReviewData {
599        #[serde(flatten)]
600        base: ReviewData,
601        #[serde(default)]
602        additional_reviews: Vec<ReviewData>,
603    }
604
605    let RawReviewData { base, mut additional_reviews } = RawReviewData::deserialize(deserializer)?;
606    additional_reviews.insert(0, base);
607    Ok(additional_reviews)
608}
609
610/// Data regarding replay reviews; present on [`Play`], not [`PlayEvent`].
611#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
612#[serde(rename_all = "camelCase")]
613#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
614pub struct ReviewData {
615    pub is_overturned: bool,
616    #[serde(rename = "inProgress")]
617    pub is_in_progress: bool,
618    pub review_type: ReviewReasonId,
619    /// If `None`, then a crew-chief review.
620    #[serde(alias = "challengeTeamId")]
621    pub challenging_team: Option<TeamId>,
622    /// For ABS challenges
623    pub player: Option<NamedPerson>,
624}
625
626/// An "indivisible" play, such as pickoff, pitch, stolen base, etc.
627#[allow(clippy::large_enum_variant, reason = "not a problemo dw")]
628#[derive(Debug, Deserialize, PartialEq, Clone)]
629#[serde(rename_all_fields = "camelCase", tag = "type")]
630#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
631pub enum PlayEvent {
632    #[serde(rename = "action")]
633    Action {
634        details: ActionPlayDetails,
635        #[serde(rename = "actionPlayId")]
636        play_id: Option<Uuid>,
637        
638        #[serde(flatten)]
639        common: PlayEventCommon,
640    },
641    #[serde(rename = "pitch")]
642    Pitch {
643        details: PitchPlayDetails,
644        pitch_data: Option<PitchData>,
645        hit_data: Option<HitData>,
646        /// Starts at 1
647        #[serde(rename = "pitchNumber")]
648        pitch_ordinal: usize,
649        play_id: Uuid,
650
651        #[serde(flatten)]
652        common: PlayEventCommon,
653    },
654    #[serde(rename = "stepoff")]
655    Stepoff {
656        details: StepoffPlayDetails,
657        play_id: Option<Uuid>,
658        
659        #[serde(flatten)]
660        common: PlayEventCommon,
661    },
662    #[serde(rename = "no_pitch")]
663    NoPitch {
664        details: NoPitchPlayDetails,
665        play_id: Option<Uuid>,
666        /// Starts at 1
667        #[serde(rename = "pitchNumber", default)]
668        pitch_ordinal: usize,
669
670        #[serde(flatten)]
671        common: PlayEventCommon,
672    },
673    #[serde(rename = "pickoff")]
674    Pickoff {
675        details: PickoffPlayDetails,
676        #[serde(alias = "actionPlayId", default)]
677        play_id: Option<Uuid>,
678
679        #[serde(flatten)]
680        common: PlayEventCommon,
681    }
682}
683
684impl Deref for PlayEvent {
685    type Target = PlayEventCommon;
686
687    fn deref(&self) -> &Self::Target {
688        let (Self::Action { common, .. } | Self::Pitch { common, .. } | Self::Stepoff { common, .. } | Self::NoPitch { common, .. } | Self::Pickoff { common, .. }) = self;
689        common
690    }
691}
692
693impl DerefMut for PlayEvent {
694    fn deref_mut(&mut self) -> &mut Self::Target {
695        let (Self::Action { common, .. } | Self::Pitch { common, .. } | Self::Stepoff { common, .. } | Self::NoPitch { common, .. } | Self::Pickoff { common, .. }) = self;
696        common
697    }
698}
699
700#[derive(Debug, Deserialize, PartialEq, Clone)]
701#[serde(rename_all = "camelCase")]
702pub struct PlayEventCommon {
703    /// At the end of the play event
704    pub count: AtBatCount,
705    #[serde(rename = "startTime", deserialize_with = "crate::deserialize_datetime")]
706    pub start_timestamp: NaiveDateTime,
707    #[serde(rename = "endTime", deserialize_with = "crate::deserialize_datetime")]
708    pub end_timestamp: NaiveDateTime,
709    pub is_pitch: bool,
710    #[serde(rename = "isBaseRunningPlay", default)]
711    pub is_baserunning_play: bool,
712    /// Pitching Subsitution, Defensive Switches, Pinch Hitting, etc.
713    #[serde(default)]
714    pub is_substitution: bool,
715
716    /// A player involved in the play.
717    pub player: Option<PersonId>,
718    /// An umpire involved in the play, ex: Ejection
719    pub umpire: Option<PersonId>,
720    /// Position (typically a complement of ``player``)
721    pub position: Option<NamedPosition>,
722    /// Also not always present, check by the [`EventType`]; [`PitchingSubsitution`](EventType::PitchingSubstitution)s don't have it.
723    pub replaced_player: Option<PersonId>,
724    /// Batting Order Index, typically supplied with a [`DefensiveSwitch`](EventType::DefensiveSwitch) or [`OffensiveSubstitution`](EventType::OffensiveSubstitution)
725    #[serde(rename = "battingOrder")]
726    pub batting_order_index: Option<BattingOrderIndex>,
727    /// Base correlated with play, such as a stolen base
728    pub base: Option<Base>,
729    #[serde(rename = "reviewDetails", default, deserialize_with = "deserialize_review_data")]
730    pub reviews: Vec<ReviewData>,
731    pub injury_type: Option<String>,
732    
733    #[doc(hidden)]
734    #[serde(rename = "index", default)]
735    pub __index: IgnoredAny,
736}
737
738#[derive(Debug, Deserialize, PartialEq, Clone)]
739#[serde(rename_all = "camelCase")]
740#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
741pub struct ActionPlayDetails {
742    #[serde(rename = "eventType")]
743    pub event: EventType,
744    /// Shohei Ohtani strikes out swinging.
745    pub description: String,
746
747    /// Score as of the end of the play
748    pub away_score: usize,
749    /// Score as of the end of the play
750    pub home_score: usize,
751
752    /// Whether the batter in the play is out
753    pub is_out: bool,
754    pub is_scoring_play: bool,
755    #[serde(default)]
756    pub has_review: bool,
757
758    #[serde(rename = "disengagementNum", default)]
759    pub disengagements: Option<NonZeroUsize>,
760    
761    #[doc(hidden)]
762    #[serde(rename = "event", default)]
763    pub __event: IgnoredAny,
764
765    #[doc(hidden)]
766    #[serde(rename = "type", default)]
767    pub __type: IgnoredAny,
768
769    // redundant
770    #[doc(hidden)]
771    #[serde(rename = "violation", default)]
772    pub __violation: IgnoredAny,
773}
774
775#[allow(clippy::struct_excessive_bools, reason = "inapplicable")]
776#[derive(Debug, Deserialize, PartialEq, Clone)]
777#[serde(rename_all = "camelCase")]
778#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
779pub struct PitchPlayDetails {
780    pub is_in_play: bool,
781    pub is_strike: bool,
782    pub is_ball: bool,
783    pub is_out: bool,
784    #[serde(default)]
785    pub has_review: bool,
786    #[serde(default)]
787    pub runner_going: bool,
788    #[serde(rename = "disengagementNum", default)]
789    pub disengagements: Option<NonZeroUsize>,
790    
791    #[serde(rename = "type", deserialize_with = "crate::meta::fallback_pitch_type_deserializer", default = "crate::meta::unknown_pitch_type")]
792    pub pitch_type: PitchType,
793
794    pub call: PitchCodeId,
795
796    #[doc(hidden)]
797    #[serde(rename = "ballColor", default)]
798    pub __ball_color: IgnoredAny,
799
800    #[doc(hidden)]
801    #[serde(rename = "trailColor", default)]
802    pub __trail_color: IgnoredAny,
803
804    #[doc(hidden)]
805    #[serde(rename = "description", default)]
806    pub __description: IgnoredAny,
807
808    #[doc(hidden)]
809    #[serde(rename = "code", default)]
810    pub __code: IgnoredAny,
811
812    // redundant
813    #[doc(hidden)]
814    #[serde(rename = "violation", default)]
815    pub __violation: IgnoredAny,
816}
817
818#[allow(clippy::struct_excessive_bools, reason = "inapplicable")]
819#[derive(Debug, Deserialize, PartialEq, Clone)]
820#[serde(rename_all = "camelCase")]
821#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
822pub struct StepoffPlayDetails {
823    pub description: String,
824    /// Typically "PSO" - "Pitcher Step Off"
825    pub code: PitchCodeId,
826    pub is_out: bool,
827    #[serde(default)]
828    pub has_review: bool,
829    /// Catcher-called mound disengagement.
830    pub from_catcher: bool,
831    #[serde(rename = "disengagementNum", default)]
832    pub disengagements: Option<NonZeroUsize>,
833
834    // redundant
835    #[doc(hidden)]
836    #[serde(rename = "violation", default)]
837    pub __violation: IgnoredAny,
838}
839
840#[allow(clippy::struct_excessive_bools, reason = "inapplicable")]
841#[derive(Debug, Deserialize, PartialEq, Clone)]
842#[serde(rename_all = "camelCase")]
843#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
844pub struct NoPitchPlayDetails {
845    #[serde(default)]
846    pub is_in_play: bool,
847    #[serde(default)]
848    pub is_strike: bool,
849    #[serde(default)]
850    pub is_ball: bool,
851    pub is_out: bool,
852    #[serde(default)]
853    pub has_review: bool,
854    #[serde(default)]
855    pub runner_going: bool,
856
857    #[serde(default = "crate::meta::unknown_pitch_code")]
858    pub call: PitchCodeId,
859
860    #[serde(rename = "disengagementNum", default)]
861    pub disengagements: Option<NonZeroUsize>,
862    
863    #[doc(hidden)]
864    #[serde(rename = "description", default)]
865    pub __description: IgnoredAny,
866
867    #[doc(hidden)]
868    #[serde(rename = "code", default)]
869    pub __code: IgnoredAny,
870
871    // redundant
872    #[doc(hidden)]
873    #[serde(rename = "violation", default)]
874    pub __violation: IgnoredAny,
875}
876
877#[allow(clippy::struct_excessive_bools, reason = "inapplicable")]
878#[derive(Debug, Deserialize, PartialEq, Clone)]
879#[serde(rename_all = "camelCase")]
880#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
881pub struct PickoffPlayDetails {
882    pub description: String,
883    /// Typically "1" - "Pickoff Attempt 1B", "2" - "Pickoff Attempt 2B", "3" - "Pickoff Attempt 3B"
884    pub code: PitchCodeId,
885    pub is_out: bool,
886    #[serde(default)]
887    pub has_review: bool,
888    /// Catcher-called pickoff.
889    pub from_catcher: bool,
890
891    #[serde(rename = "disengagementNum", default)]
892    pub disengagements: Option<NonZeroUsize>,
893
894    // redundant
895    #[doc(hidden)]
896    #[serde(rename = "violation", default)]
897    pub __violation: IgnoredAny,
898}
899
900/// Statistical data regarding a pitch.
901///
902/// Some acronyms are an existing spec, best to keep with that.
903#[allow(non_snake_case, reason = "spec")]
904#[derive(Debug, PartialEq, Copy, Clone)]
905pub struct PitchData {
906    /// Velocity measured at release, measured in mph
907    pub release_speed: f64,
908    /// Velocity measured crossing home plate, measured in mph
909    pub plate_speed: f64,
910    
911    /// Height above home plate for the bottom of the hitter's strike zone
912    ///
913    /// Measured in feet.
914    pub sz_bot: f64,
915    /// Height above home plate for the top of the hitter's strike zone
916    ///
917    /// Measured in feet.
918    pub sz_top: f64,
919    /// Width of the strike zone
920    ///
921    /// Measured in inches.
922    pub sz_wid: f64,
923    /// Depth of the strike zone
924    ///
925    /// Measured in inches.
926    pub sz_dep: f64,
927    
928    /// Acceleration of the pitch near release, horizontal movement axis,
929    /// catchers perspective (positive means RHP sweep)
930    /// 
931    /// Measured in feet/s^2.
932    pub aX: f64,
933    /// Acceleration of the pitch near release, depth axis,
934    /// catchers perspective (positive means deceleration)
935    /// 
936    /// Measured in feet/s^2.
937    pub aY: f64,
938    /// Acceleration of the pitch near release, vertical movement axis,
939    /// catchers perspective (positive means literal carry)
940    ///
941    /// Measured in feet/s^2.
942    pub aZ: f64,
943    
944    /// Might be broken, use ``horizontal_movement`` instead
945    ///
946    /// Horizontal movement of the pitch between the release point and home plate,
947    /// catchers perspective (positive means RHP sweep)
948    /// as compared to a theoretical pitch thrown at with the same velocity vector
949    /// and no spin-induced movement.
950    /// This parameter is measured at y=40 feet regardless of the y0 value.
951    /// 
952    /// Measured in inches.
953    ///
954    /// Does not account for seam-shifted wake!
955    pub pfxX: f64,
956    /// Might be broken, use ``vertical_drop`` instead
957    ///
958    /// Vertical movement of the pitch between the release point and home plate,
959    /// catchers perspective (positive means literal rise),
960    /// as compared to a theoretical pitch thrown at with the same velocity vector
961    /// and no spin-induced movement.
962    /// This parameter is measured at y=40 feet regardless of the y0 value.
963    /// 
964    /// Measured in inches.
965    ///
966    /// Does not account for seam-shifted wake!
967    pub pfxZ: f64,
968
969    /// Horizontal coordinate of the pitch as it crosses home plate, 0 is the middle of the plate.
970    /// Catchers perspective, positive means arm-side for a RHP, negative means glove-side for a RHP.
971    /// 
972    /// Measured in feet.
973    pub pX: f64,
974    /// Vertical coordinate of the pitch as it crosses home plate, 0 is the plate itself
975    /// 
976    /// Measured in feet.
977    pub pZ: f64,
978
979    /// Horizontal component of velocity out of the hand, catchers perspective, positive means RHP glove-side.
980    ///
981    /// Measured in feet per second.
982    pub vX0: f64,
983    /// Depth component of velocity out of the hand, catchers perspective, positive means the ball isn't going into centerfield.
984    ///
985    /// Measured in feet per second.
986    pub vY0: f64,
987    /// Vertical component of velocity out of the hand, measured in feet per second.
988    ///
989    /// Measured in feet per second.
990    pub vZ0: f64,
991
992    /// X coordinate of pitch at release
993    ///
994    /// Measured in feet
995    pub x0: f64,
996    /// Y coordinate of pitch at release, typically as close to 50 as possible.
997    ///
998    /// Measured in feet
999    pub y0: f64,
1000    /// Z coordinate of pitch at release
1001    ///
1002    /// Measured in feet
1003    pub z0: f64,
1004
1005    /// No clue.
1006    pub x: f64,
1007    /// No clue.
1008    pub y: f64,
1009
1010    /// No clue. Does not match theta angle of induced break vector. Consistently 36.0. Strange.
1011    pub break_angle: f64,
1012    /// No clue. Does not match length of induced break vector.
1013    pub break_length: f64,
1014
1015    /// Standard metric, amount of vertical movement induced.
1016    ///
1017    /// Measured in inches
1018    pub induced_vertical_movement: f64,
1019    /// Standard metric, amount of vertical movement the pitch has (including gravity).
1020    ///
1021    /// Measured in inches
1022    pub vertical_drop: f64,
1023    /// Standard metric, amount of horizontal movement the pitch has.
1024    ///
1025    /// Measured in inches
1026    pub horizontal_movement: f64,
1027    /// No clue. Thought to be the amount of depth-based movement (acceleration), but it's consistently 24.0. Strange.
1028    pub depth_break: f64,
1029    
1030    /// RPMs out of the hand
1031    pub spin_rate: f64,
1032
1033    /// Measured in degrees.
1034    ///
1035    /// 0 means complete topspin.
1036    /// 180 means complete backspin.
1037    /// 90 means complete sidespin (RHP sweeper).
1038    /// 270 means complete sidespin (elite RHP changeup).
1039    /// 
1040    /// ~225 is your average RHP fastball.
1041    pub spin_axis: f64,
1042
1043    pub zone: StrikeZoneSection,
1044
1045    /// AI model confidence about pitch type designation.
1046    ///
1047    /// Sometimes greater than 1.0
1048    pub type_confidence: f64,
1049
1050    /// Time from out of the hand to crossing the plate.
1051    ///
1052    /// Measured in seconds
1053    pub time_to_plate: f64,
1054
1055    /// Extension
1056    ///
1057    /// Measured in feet
1058    pub extension: f64,
1059}
1060
1061impl<'de> Deserialize<'de> for PitchData {
1062    #[allow(clippy::too_many_lines, reason = "deserialization is hard")]
1063    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1064    where
1065        D: serde::Deserializer<'de>
1066    {
1067        #[must_use]
1068        const fn default_strike_zone_width() -> f64 {
1069            17.0
1070        }
1071
1072        #[must_use]
1073        const fn default_strike_zone_depth() -> f64 {
1074            17.0
1075        }
1076
1077        #[must_use]
1078        const fn default_nan() -> f64 {
1079            f64::NAN
1080        }
1081
1082        #[must_use]
1083        const fn default_strike_zone_section() -> StrikeZoneSection {
1084            StrikeZoneSection::MiddleMiddle
1085        }
1086        
1087        #[derive(Deserialize)]
1088        #[serde(rename_all = "camelCase")]
1089        #[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
1090        struct Raw {
1091            #[serde(default = "default_nan")]
1092            start_speed: f64,
1093            #[serde(default = "default_nan")]
1094            end_speed: f64,
1095            #[serde(default = "default_nan")]
1096            strike_zone_top: f64,
1097            #[serde(default = "default_nan")]
1098            strike_zone_bottom: f64,
1099            #[serde(default = "default_strike_zone_width")]
1100            strike_zone_width: f64,
1101            #[serde(default = "default_strike_zone_depth")]
1102            strike_zone_depth: f64,
1103            coordinates: RawCoordinates,
1104            breaks: RawBreaks,
1105            #[serde(default = "default_strike_zone_section")]
1106            zone: StrikeZoneSection,
1107            #[serde(default = "default_nan")]
1108            type_confidence: f64,
1109            #[serde(default = "default_nan")]
1110            plate_time: f64,
1111            #[serde(default = "default_nan")]
1112            extension: f64,
1113        }
1114
1115        #[allow(non_snake_case, reason = "spec")]
1116        #[derive(Deserialize)]
1117        #[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
1118        struct RawCoordinates {
1119            #[serde(default = "default_nan")]
1120            aX: f64,
1121            #[serde(default = "default_nan")]
1122            aY: f64,
1123            #[serde(default = "default_nan")]
1124            aZ: f64,
1125            #[serde(default = "default_nan")]
1126            pfxX: f64,
1127            #[serde(default = "default_nan")]
1128            pfxZ: f64,
1129            #[serde(default = "default_nan")]
1130            pX: f64,
1131            #[serde(default = "default_nan")]
1132            pZ: f64,
1133            #[serde(default = "default_nan")]
1134            vX0: f64,
1135            #[serde(default = "default_nan")]
1136            vY0: f64,
1137            #[serde(default = "default_nan")]
1138            vZ0: f64,
1139            #[serde(default = "default_nan")]
1140            x: f64,
1141            #[serde(default = "default_nan")]
1142            y: f64,
1143            #[serde(default = "default_nan")]
1144            x0: f64,
1145            #[serde(default = "default_nan")]
1146            y0: f64,
1147            #[serde(default = "default_nan")]
1148            z0: f64,
1149        }
1150
1151        #[derive(Deserialize)]
1152        #[serde(rename_all = "camelCase")]
1153        #[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
1154        struct RawBreaks {
1155            #[serde(default = "default_nan")]
1156            break_angle: f64,
1157            #[serde(default = "default_nan")]
1158            break_length: f64,
1159            #[serde(default = "default_nan")]
1160            break_y: f64,
1161            #[serde(default = "default_nan")]
1162            break_vertical: f64,
1163            #[serde(default = "default_nan")]
1164            break_vertical_induced: f64,
1165            #[serde(default = "default_nan")]
1166            break_horizontal: f64,
1167            #[serde(default = "default_nan")]
1168            spin_rate: f64,
1169            #[serde(default = "default_nan")]
1170            spin_direction: f64,
1171        }
1172
1173        let Raw {
1174            start_speed,
1175            end_speed,
1176            strike_zone_top,
1177            strike_zone_bottom,
1178            strike_zone_width,
1179            strike_zone_depth,
1180            coordinates: RawCoordinates {
1181                pfxX,
1182                pfxZ,
1183                aX,
1184                aY,
1185                aZ,
1186                pX,
1187                pZ,
1188                vX0,
1189                vY0,
1190                vZ0,
1191                x,
1192                y,
1193                x0,
1194                y0,
1195                z0,
1196            },
1197            breaks: RawBreaks {
1198                break_angle,
1199                break_length,
1200                break_y,
1201                break_vertical,
1202                break_vertical_induced,
1203                break_horizontal,
1204                spin_rate,
1205                spin_direction,
1206            },
1207            zone,
1208            type_confidence,
1209            plate_time,
1210            extension,
1211        } =  Raw::deserialize(deserializer)?;
1212
1213        Ok(Self {
1214            release_speed: start_speed,
1215            plate_speed: end_speed,
1216            sz_bot: strike_zone_bottom,
1217            sz_top: strike_zone_top,
1218            sz_wid: strike_zone_width,
1219            sz_dep: strike_zone_depth,
1220            aX,
1221            aY,
1222            aZ,
1223            pfxX,
1224            pfxZ,
1225            pX,
1226            pZ,
1227            vX0,
1228            vY0,
1229            vZ0,
1230            x0,
1231            y0,
1232            z0,
1233            horizontal_movement: break_horizontal,
1234            x,
1235            y,
1236            break_angle,
1237            break_length,
1238            induced_vertical_movement: break_vertical_induced,
1239            vertical_drop: break_vertical,
1240            depth_break: break_y,
1241            spin_rate,
1242            spin_axis: spin_direction,
1243            zone,
1244            type_confidence,
1245            time_to_plate: plate_time,
1246            extension,
1247        })
1248    }
1249}
1250
1251/// Data regarding batted-balls
1252#[derive(Debug, Deserialize, PartialEq, Clone)]
1253#[serde(from = "__HitDataStruct")]
1254pub struct HitData {
1255    /// sometimes just takes a second to be present
1256    pub hit_trajectory: Option<HitTrajectory>,
1257    /// sometimes just takes a second to be present
1258    pub contact_hardness: Option<ContactHardness>,
1259    pub statcast: Option<StatcastHitData>,
1260}
1261
1262#[serde_as]
1263#[doc(hidden)]
1264#[derive(Deserialize)]
1265#[serde(rename_all = "camelCase")]
1266#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
1267struct __HitDataStruct {
1268    #[serde_as(deserialize_as = "DefaultOnError")]
1269    #[serde(rename = "trajectory", default)]
1270    hit_trajectory: Option<HitTrajectory>,
1271    #[serde(rename = "hardness", default)]
1272    contact_hardness: Option<ContactHardness>,
1273
1274    #[serde(flatten, default)]
1275    statcast: Option<StatcastHitData>,
1276
1277    #[doc(hidden)]
1278    #[serde(rename = "location", default)]
1279    __location: IgnoredAny,
1280
1281    #[doc(hidden)]
1282    #[serde(rename = "coordinates")]
1283    __coordinates: IgnoredAny,
1284}
1285
1286impl From<__HitDataStruct> for HitData {
1287    fn from(__HitDataStruct { hit_trajectory, contact_hardness, statcast, .. }: __HitDataStruct) -> Self {
1288        Self {
1289            hit_trajectory: hit_trajectory.or_else(|| statcast.as_ref().map(|statcast| statcast.launch_angle).map(HitTrajectory::from_launch_angle)),
1290            contact_hardness,
1291            statcast,
1292        }
1293    }
1294}
1295
1296/// Statcast data regarding batted balls, only sometimes present.
1297#[derive(Debug, Deserialize, PartialEq, Clone)]
1298#[serde(rename_all = "camelCase")]
1299#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
1300pub struct StatcastHitData {
1301    /// Speed of the ball as it leaves the bat
1302    ///
1303    /// Measured in mph
1304    #[serde(rename = "launchSpeed")]
1305    pub exit_velocity: f64,
1306    /// Vertical Angle in degrees at which the ball leaves the bat.
1307    ///
1308    /// Measured in degrees
1309    pub launch_angle: f64,
1310
1311    /// Distance the ball travels before being caught or rolling.
1312    ///
1313    /// Measured in feet
1314    #[serde(rename = "totalDistance")]
1315    pub distance: f64,
1316}
1317
1318#[derive(Builder)]
1319#[builder(derive(Into))]
1320pub struct PlayByPlayRequest {
1321    #[builder(into)]
1322    id: GameId,
1323}
1324
1325impl<S: play_by_play_request_builder::State + play_by_play_request_builder::IsComplete> crate::request::RequestURLBuilderExt for PlayByPlayRequestBuilder<S> {
1326    type Built = PlayByPlayRequest;
1327}
1328
1329impl Display for PlayByPlayRequest {
1330    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1331        write!(f, "http://statsapi.mlb.com/api/v1/game/{}/playByPlay", self.id)
1332    }
1333}
1334
1335impl RequestURL for PlayByPlayRequest {
1336    type Response = Plays;
1337}
1338
1339#[cfg(test)]
1340mod tests {
1341    use crate::TEST_YEAR;
1342    use crate::game::PlayByPlayRequest;
1343    use crate::meta::GameType;
1344    use crate::request::RequestURLBuilderExt;
1345    use crate::schedule::ScheduleRequest;
1346    use crate::season::{Season, SeasonsRequest};
1347    use crate::sport::SportId;
1348
1349    #[tokio::test]
1350    async fn ws_gm7_2025_pbp() {
1351        let _ = PlayByPlayRequest::builder().id(813_024).build_and_get().await.unwrap();
1352    }
1353
1354    #[tokio::test]
1355    async fn postseason_pbp() {
1356        let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
1357		let postseason = season.postseason.expect("Expected the MLB to have a postseason");
1358		let games = ScheduleRequest::<()>::builder().date_range(postseason).sport_id(SportId::MLB).build_and_get().await.unwrap();
1359		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<_>>();
1360		let mut has_errors = false;
1361		for game in games {
1362			if let Err(e) = PlayByPlayRequest::builder().id(game).build_and_get().await {
1363			    dbg!(e);
1364			    has_errors = true;
1365			}
1366		}
1367		assert!(!has_errors, "Has errors.");
1368    }
1369
1370    #[cfg_attr(not(feature = "_heavy_tests"), ignore)]
1371    #[tokio::test]
1372    async fn regular_season_pbp() {
1373        let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
1374        let regular_season = season.regular_season;
1375        let games = ScheduleRequest::<()>::builder().date_range(regular_season).sport_id(SportId::MLB).build_and_get().await.unwrap();
1376        let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type == GameType::RegularSeason).collect::<Vec<_>>();
1377        let mut has_errors = false;
1378        for game in games {
1379            if let Err(e) = PlayByPlayRequest::builder().id(game.game_id).build_and_get().await {
1380                dbg!(e);
1381                has_errors = true;
1382            }
1383        }
1384        assert!(!has_errors, "Has errors.");
1385    }
1386}