#![allow(unused_imports, reason = "usage of children modules")]
use std::fmt::{Display, Formatter};
use std::marker::PhantomData;
use std::ops::{ControlFlow, Sub};
use std::time::{Duration, Instant};
use bon::Builder;
use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Utc};
use derive_more::{Deref, DerefMut, Display, From, Not};
use fxhash::FxHashMap;
use serde::{Deserialize, Deserializer};
use serde::de::{DeserializeOwned, Error, IgnoredAny, MapAccess, Visitor};
use serde_with::{serde_as, DisplayFromStr};
use crate::person::{Ballplayer, JerseyNumber, NamedPerson, PersonId};
use crate::meta::{DayNight, NamedPosition};
use crate::request::RequestURLBuilderExt;
use crate::team::TeamId;
use crate::team::roster::RosterStatus;
use crate::{DayHalf, HomeAway, ResourceUsage, ResultHoldingResourceUsage, TeamSide};
use crate::meta::WindDirectionId;
use crate::request;
mod boxscore; mod changes;
mod content;
mod context_metrics;
mod diff;
mod linescore; mod pace; mod plays; mod timestamps; mod uniforms;
mod win_probability;
mod live_feed;
pub use boxscore::*;
pub use changes::*;
pub use content::*;
pub use context_metrics::*;
pub use diff::*;
pub use linescore::*;
pub use pace::*;
pub use plays::*;
pub use timestamps::*;
pub use uniforms::*;
pub use win_probability::*;
pub use live_feed::*;
id!(#[doc = "A [`u32`] representing a baseball game. [Sport](crate::sport)-independent"] GameId { gamePk: u32 });
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct GameDateTime {
#[serde(rename = "dateTime", deserialize_with = "crate::deserialize_datetime")]
pub datetime: DateTime<Utc>,
pub original_date: NaiveDate,
pub official_date: NaiveDate,
#[serde(flatten, default)]
pub resumed: Option<GameResumedDateTime>,
#[serde(rename = "dayNight")]
pub sky: DayNight,
pub time: NaiveTime,
pub ampm: DayHalf,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct GameResumedDateTime {
#[serde(rename = "resumeDateTime", deserialize_with = "crate::deserialize_datetime")]
pub resumed_datetime: DateTime<Utc>,
#[serde(rename = "resumedFromDateTime", deserialize_with = "crate::deserialize_datetime")]
pub resumed_from_datetime: DateTime<Utc>,
#[serde(rename = "resumeDate", default)]
pub __resume_date: IgnoredAny,
#[serde(rename = "resumeFromDate", default)]
pub __resume_from_date: IgnoredAny,
}
#[derive(Debug, Deserialize, PartialEq, Clone, Default)]
#[serde(try_from = "__WeatherConditionsStruct")]
pub struct WeatherConditions {
pub condition: Option<String>,
pub temp: Option<uom::si::f64::ThermodynamicTemperature>,
pub wind: Option<(uom::si::f64::Velocity, WindDirectionId)>,
}
#[derive(Deserialize)]
#[doc(hidden)]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
struct __WeatherConditionsStruct {
condition: Option<String>,
temp: Option<String>,
wind: Option<String>,
}
impl TryFrom<__WeatherConditionsStruct> for WeatherConditions {
type Error = &'static str;
fn try_from(value: __WeatherConditionsStruct) -> Result<Self, Self::Error> {
Ok(Self {
condition: value.condition,
temp: value.temp.and_then(|temp| temp.parse::<i32>().ok()).map(|temp| uom::si::f64::ThermodynamicTemperature::new::<uom::si::thermodynamic_temperature::degree_fahrenheit>(temp as f64)),
wind: if let Some(wind) = value.wind {
let (speed, direction) = wind.split_once(" mph, ").ok_or("invalid wind format")?;
let speed = speed.parse::<i32>().map_err(|_| "invalid wind speed")?;
Some((uom::si::f64::Velocity::new::<uom::si::velocity::mile_per_hour>(speed as f64), WindDirectionId::new(direction)))
} else { None },
})
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct GameInfo {
pub attendance: Option<u32>,
#[serde(deserialize_with = "crate::try_deserialize_datetime", default)]
pub first_pitch: Option<DateTime<Utc>>,
#[serde(rename = "gameDurationMinutes")]
pub game_duration: Option<u32>,
#[serde(rename = "delayDurationMinutes")]
pub delay_duration: Option<u32>,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct TeamReviewData {
pub has_challenges: bool,
#[serde(flatten)]
pub teams: HomeAway<ResourceUsage>,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct TeamChallengeData {
pub has_challenges: bool,
#[serde(flatten)]
pub teams: HomeAway<ResultHoldingResourceUsage>,
}
#[allow(clippy::struct_excessive_bools, reason = "no")]
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct GameTags {
pub no_hitter: bool,
pub perfect_game: bool,
pub away_team_no_hitter: bool,
pub away_team_perfect_game: bool,
pub home_team_no_hitter: bool,
pub home_team_perfect_game: bool,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
pub enum DoubleHeaderKind {
#[serde(rename = "N")]
Not,
#[serde(rename = "Y")]
FirstGame,
#[serde(rename = "S")]
SecondGame,
}
impl DoubleHeaderKind {
#[must_use]
pub const fn is_double_header(self) -> bool {
matches!(self, Self::FirstGame | Self::SecondGame)
}
}
#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq, Deref, DerefMut, From)]
pub struct Inning(usize);
impl Inning {
#[must_use]
pub const fn starting() -> Self {
Self(1)
}
}
impl Display for Inning {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
crate::write_nth(self.0, f)
}
}
#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq, Not)]
pub enum InningHalf {
#[serde(rename = "Top", alias = "top")]
Top,
#[serde(rename = "Bottom", alias = "bottom")]
Bottom,
}
impl InningHalf {
#[must_use]
pub const fn starting() -> Self {
Self::Top
}
pub(crate) fn deserialize_from_is_top_inning<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(match bool::deserialize(deserializer)? {
true => Self::Top,
false => Self::Bottom,
})
}
}
impl InningHalf {
#[must_use]
pub const fn unicode_char_filled(self) -> char {
match self {
Self::Top => 'â–²',
Self::Bottom => 'â–¼',
}
}
#[must_use]
pub const fn unicode_char_empty(self) -> char {
match self {
Self::Top => 'â–³',
Self::Bottom => 'â–½',
}
}
#[must_use]
pub const fn three_char(self) -> &'static str {
match self {
Self::Top => "Top",
Self::Bottom => "Bot",
}
}
#[must_use]
pub const fn bats(self) -> TeamSide {
match self {
Self::Top => TeamSide::Away,
Self::Bottom => TeamSide::Home,
}
}
#[must_use]
pub const fn pitches(self) -> TeamSide {
match self {
Self::Top => TeamSide::Home,
Self::Bottom => TeamSide::Away,
}
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display, Default)]
#[display("{balls}-{strikes} ({outs} out)")]
pub struct AtBatCount {
#[serde(default)]
pub balls: u8,
#[serde(default)]
pub strikes: u8,
#[serde(default)]
pub outs: u8,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
pub struct SituationCount {
pub balls: u8,
pub strikes: u8,
pub outs: u8,
pub inning: Inning,
#[serde(rename = "isTopInning", deserialize_with = "InningHalf::deserialize_from_is_top_inning")]
pub inning_half: InningHalf,
#[serde(rename = "runnerOn1b")]
pub runner_on_first: bool,
#[serde(rename = "runnerOn2b")]
pub runner_on_second: bool,
#[serde(rename = "runnerOn3b")]
pub runner_on_third: bool,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
#[serde(from = "__RHEStruct")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct RHE {
pub runs: usize,
pub hits: usize,
pub errors: usize,
pub left_on_base: usize,
pub was_inning_half_played: bool,
}
#[doc(hidden)]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct __RHEStruct {
pub runs: Option<usize>,
#[serde(default)]
pub hits: usize,
#[serde(default)]
pub errors: usize,
#[serde(default)]
pub left_on_base: usize,
#[doc(hidden)]
#[serde(rename = "isWinner", default)]
pub __is_winner: IgnoredAny,
}
impl From<__RHEStruct> for RHE {
fn from(__RHEStruct { runs, hits, errors, left_on_base, .. }: __RHEStruct) -> Self {
Self {
runs: runs.unwrap_or(0),
hits,
errors,
left_on_base,
was_inning_half_played: runs.is_some(),
}
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct LabelledValue {
pub label: String,
#[serde(default)]
pub value: String,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct SectionedLabelledValues {
#[serde(rename = "title")]
pub section: String,
#[serde(rename = "fieldList")]
pub values: Vec<LabelledValue>,
}
#[allow(clippy::struct_excessive_bools, reason = "not what's happening here")]
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct PlayerGameStatusFlags {
pub is_current_batter: bool,
pub is_current_pitcher: bool,
pub is_on_bench: bool,
pub is_substitute: bool,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct Official {
pub official: NamedPerson,
pub official_type: OfficialType,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
pub enum OfficialType {
#[serde(rename = "Home Plate")]
HomePlate,
#[serde(rename = "First Base")]
FirstBase,
#[serde(rename = "Second Base")]
SecondBase,
#[serde(rename = "Third Base")]
ThirdBase,
#[serde(rename = "Left Field")]
LeftField,
#[serde(rename = "Right Field")]
RightField,
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct BattingOrderIndex {
pub major: usize,
pub minor: usize,
}
impl<'de> Deserialize<'de> for BattingOrderIndex {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>
{
let v: usize = String::deserialize(deserializer)?.parse().map_err(D::Error::custom)?;
Ok(Self {
major: v / 100,
minor: v % 100,
})
}
}
impl Display for BattingOrderIndex {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
crate::write_nth(self.major, f)?;
if self.minor > 0 {
write!(f, " ({})", self.minor + 1)?;
}
Ok(())
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct Decisions {
pub winner: Option<NamedPerson>,
pub loser: Option<NamedPerson>,
pub save: Option<NamedPerson>,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
pub struct GameStatLeaders {
#[doc(hidden)]
#[serde(rename = "hitDistance", default)]
pub __distance: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "hitSpeed", default)]
pub __exit_velocity: IgnoredAny,
#[doc(hidden)]
#[serde(rename = "pitchSpeed", default)]
pub __velocity: IgnoredAny,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Display)]
pub enum Base {
#[display("1B")]
First,
#[display("2B")]
Second,
#[display("3B")]
Third,
#[display("HP")]
Home,
}
impl<'de> Deserialize<'de> for Base {
#[allow(clippy::too_many_lines, reason = "Visitor impl takes up the bulk, is properly scoped")]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>
{
struct BaseVisitor;
impl Visitor<'_> for BaseVisitor {
type Value = Base;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "a string or integer representing the base")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(match v {
"1B" | "1" => Base::First,
"2B" | "2" => Base::Second,
"3B" | "3" => Base::Third,
"score" | "HP" | "4B" | "4" => Base::Home,
_ => return Err(E::unknown_variant(v, &["1B", "1", "2B" , "2", "3B", "3", "score", "HP", "4B", "4"]))
})
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error,
{
Ok(match v {
1 => Base::First,
2 => Base::Second,
3 => Base::Third,
4 => Base::Home,
_ => return Err(E::unknown_variant("[a number]", &["1", "2", "3", "4"]))
})
}
}
deserializer.deserialize_any(BaseVisitor)
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
pub enum ContactHardness {
#[serde(rename = "soft")]
Soft,
#[serde(rename = "medium")]
Medium,
#[serde(rename = "hard")]
Hard,
}
pub(crate) fn deserialize_players_cache<'de, T: DeserializeOwned, D: Deserializer<'de>>(deserializer: D) -> Result<FxHashMap<PersonId, T>, D::Error> {
struct PlayersCacheVisitor<T2: DeserializeOwned>(PhantomData<T2>);
impl<'de2, T2: DeserializeOwned> serde::de::Visitor<'de2> for PlayersCacheVisitor<T2> {
type Value = FxHashMap<PersonId, T2>;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("a map")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de2>,
{
let mut values = FxHashMap::default();
while let Some((key, value)) = map.next_entry()? {
let key: String = key;
let key = PersonId::new(key.strip_prefix("ID").ok_or_else(|| A::Error::custom("invalid id format"))?.parse::<u32>().map_err(A::Error::custom)?);
values.insert(key, value);
}
Ok(values)
}
}
deserializer.deserialize_map(PlayersCacheVisitor::<T>(PhantomData))
}
#[derive(Debug)]
pub struct PlayStream {
game_id: GameId,
current_play_idx: usize,
in_progress_current_play: bool,
current_play_review_idx: usize,
in_progress_current_play_review: bool,
current_play_event_idx: usize,
current_play_event_review_idx: usize,
in_progress_current_play_event_review: bool,
}
impl PlayStream {
#[must_use]
pub fn new(game_id: impl Into<GameId>) -> Self {
Self {
game_id: game_id.into(),
current_play_idx: 0,
in_progress_current_play: false,
current_play_review_idx: 0,
in_progress_current_play_review: false,
current_play_event_idx: 0,
current_play_event_review_idx: 0,
in_progress_current_play_event_review: false,
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum PlayStreamEvent<'a> {
Start,
StartPlay(&'a Play),
PlayReviewStart(&'a ReviewData, &'a Play),
PlayReviewEnd(&'a ReviewData, &'a Play),
EndPlay(&'a Play),
PlayEvent(&'a PlayEvent, &'a Play),
PlayEventReviewStart(&'a ReviewData, &'a PlayEvent, &'a Play),
PlayEventReviewEnd(&'a ReviewData, &'a PlayEvent, &'a Play),
GameEnd(&'a Decisions, &'a GameStatLeaders),
}
impl PlayStream {
pub async fn run<F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, request::Error>>(self, f: F) -> Result<(), request::Error> {
self.run_with_custom_error::<request::Error, F>(f).await
}
async fn run_current_play<E, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(&self, mut f: F, current_play: &Play, meta: &LiveFeedMetadata, data: &LiveFeedData, linescore: &Linescore, boxscore: &Boxscore) -> Result<ControlFlow<()>, E> {
macro_rules! flow_try {
($($t:tt)*) => {
match ($($t)*).await? {
ControlFlow::Continue(()) => {},
ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
}
};
}
if !self.in_progress_current_play {
flow_try!(f(PlayStreamEvent::StartPlay(current_play), meta, data, linescore, boxscore));
}
let mut play_events = current_play.play_events.iter().skip(self.current_play_event_idx);
if let Some(current_play_event) = play_events.next() {
flow_try!(f(PlayStreamEvent::PlayEvent(current_play_event, current_play), meta, data, linescore, boxscore));
let mut reviews = current_play_event.reviews.iter().skip(self.current_play_event_review_idx);
if let Some(current_review) = reviews.next() {
if !self.in_progress_current_play_event_review {
flow_try!(f(PlayStreamEvent::PlayEventReviewStart(current_review, current_play_event, current_play), meta, data, linescore, boxscore));
}
if !current_review.is_in_progress {
flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(current_review, current_play_event, current_play), meta, data, linescore, boxscore));
}
}
for review in reviews {
flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, current_play_event, current_play), meta, data, linescore, boxscore));
if !review.is_in_progress {
flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, current_play_event, current_play), meta, data, linescore, boxscore));
}
}
}
for play_event in play_events {
flow_try!(f(PlayStreamEvent::PlayEvent(play_event, current_play), meta, data, linescore, boxscore));
for review in &play_event.reviews {
flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, current_play), meta, data, linescore, boxscore));
if !review.is_in_progress {
flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, current_play), meta, data, linescore, boxscore));
}
}
}
let mut reviews = current_play.reviews.iter().skip(self.current_play_review_idx);
if let Some(current_review) = reviews.next() {
if !self.in_progress_current_play_review {
flow_try!(f(PlayStreamEvent::PlayReviewStart(current_review, current_play), meta, data, linescore, boxscore));
}
if !current_review.is_in_progress {
flow_try!(f(PlayStreamEvent::PlayReviewEnd(current_review, current_play), meta, data, linescore, boxscore));
}
}
for review in reviews {
flow_try!(f(PlayStreamEvent::PlayReviewStart(review, current_play), meta, data, linescore, boxscore));
if !review.is_in_progress {
flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, current_play), meta, data, linescore, boxscore));
}
}
if current_play.about.is_complete {
flow_try!(f(PlayStreamEvent::EndPlay(current_play), meta, data, linescore, boxscore));
}
Ok(ControlFlow::Continue(()))
}
async fn run_next_plays<E, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(&self, mut f: F, plays: impl Iterator<Item=&Play>, meta: &LiveFeedMetadata, data: &LiveFeedData, linescore: &Linescore, boxscore: &Boxscore) -> Result<ControlFlow<()>, E> {
macro_rules! flow_try {
($($t:tt)*) => {
match ($($t)*).await? {
ControlFlow::Continue(()) => {},
ControlFlow::Break(()) => return Ok(ControlFlow::Break(())),
}
};
}
for play in plays {
flow_try!(f(PlayStreamEvent::StartPlay(play), meta, data, linescore, boxscore));
for play_event in &play.play_events {
flow_try!(f(PlayStreamEvent::PlayEvent(play_event, play), meta, data, linescore, boxscore));
for review in &play_event.reviews {
flow_try!(f(PlayStreamEvent::PlayEventReviewStart(review, play_event, play), meta, data, linescore, boxscore));
if !review.is_in_progress {
flow_try!(f(PlayStreamEvent::PlayEventReviewEnd(review, play_event, play), meta, data, linescore, boxscore));
}
}
}
for review in &play.reviews {
flow_try!(f(PlayStreamEvent::PlayReviewStart(review, play), meta, data, linescore, boxscore));
if !review.is_in_progress {
flow_try!(f(PlayStreamEvent::PlayReviewEnd(review, play), meta, data, linescore, boxscore));
}
}
if play.about.is_complete {
flow_try!(f(PlayStreamEvent::EndPlay(play), meta, data, linescore, boxscore));
}
}
Ok(ControlFlow::Continue(()))
}
fn update_indices(&mut self, plays: &[Play]) {
let latest_play = plays.last();
self.in_progress_current_play = latest_play.is_some_and(|play| !play.about.is_complete);
self.current_play_idx = if self.in_progress_current_play { plays.len() - 1 } else { plays.len() };
let current_play = plays.get(self.current_play_idx);
let current_play_event = current_play.and_then(|play| play.play_events.last());
let current_play_review = current_play.and_then(|play| play.reviews.last());
let current_play_event_review = current_play_event.and_then(|play_event| play_event.reviews.last());
self.in_progress_current_play_review = current_play_review.is_some_and(|review| review.is_in_progress);
self.current_play_review_idx = current_play.map_or(0, |play| if self.in_progress_current_play_review { play.reviews.len() - 1 } else { play.reviews.len() });
self.current_play_event_idx = current_play.map_or(0, |play| play.play_events.len());
self.in_progress_current_play_event_review = current_play_event_review.is_some_and(|review| review.is_in_progress);
self.current_play_event_review_idx = current_play_event.map_or(0, |play_event| if self.in_progress_current_play_event_review { play_event.reviews.len() - 1 } else { play_event.reviews.len() });
}
pub async fn run_with_custom_error<E: From<request::Error>, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(self, f: F) -> Result<(), E> {
let feed = LiveFeedRequest::builder().id(self.game_id).build_and_get().await?;
Self::with_presupplied_feed(feed, f).await
}
pub async fn with_presupplied_feed<E: From<request::Error>, F: AsyncFnMut(PlayStreamEvent, &LiveFeedMetadata, &LiveFeedData, &Linescore, &Boxscore) -> Result<ControlFlow<()>, E>>(mut feed: LiveFeedResponse, mut f: F) -> Result<(), E> {
macro_rules! flow_try {
($($t:tt)*) => {
match ($($t)*).await? {
ControlFlow::Continue(()) => {},
ControlFlow::Break(()) => return Ok(()),
}
};
}
let mut this = Self::new(feed.id);
flow_try!(f(PlayStreamEvent::Start, &feed.meta, &feed.data, &feed.live.linescore, &feed.live.boxscore));
loop {
let since_last_request = Instant::now();
let LiveFeedResponse { meta, data, live, .. } = &feed;
let LiveFeedLiveData { linescore, boxscore, decisions, leaders, plays } = live;
let mut plays = plays.iter().skip(this.current_play_idx);
if let Some(current_play) = plays.next() {
flow_try!(this.run_current_play(&mut f, current_play, meta, data, linescore, boxscore));
}
flow_try!(this.run_next_plays(&mut f, plays, meta, data, linescore, boxscore));
if data.status.abstract_game_code.is_finished() && let Some(decisions) = decisions {
let _ = f(PlayStreamEvent::GameEnd(decisions, leaders), meta, data, linescore, boxscore).await?;
return Ok(())
}
this.update_indices(&live.plays);
let total_sleep_time = Duration::from_secs(meta.recommended_poll_rate as _);
drop(feed);
tokio::time::sleep(total_sleep_time.saturating_sub(since_last_request.elapsed())).await;
feed = LiveFeedRequest::builder().id(this.game_id).build_and_get().await?;
}
}
}
#[cfg(test)]
mod tests {
use std::ops::ControlFlow;
use crate::{cache::RequestableEntrypoint, game::{PlayEvent, PlayStream, PlayStreamEvent}};
#[tokio::test]
async fn test_play_stream() {
Box::pin(PlayStream::new(822_834).run(async |event, _meta, _data, _linescore, _boxscore| {
match event {
PlayStreamEvent::Start => println!("GameStart"),
PlayStreamEvent::StartPlay(play) => println!("PlayStart; {} vs. {}", play.matchup.batter.full_name, play.matchup.pitcher.full_name),
PlayStreamEvent::PlayEvent(play_event, _play) => {
print!("PlayEvent; ");
match play_event {
PlayEvent::Action { details, .. } => println!("{}", details.description),
PlayEvent::Pitch { details, common, .. } => println!("{} -> {}", details.call, common.count),
PlayEvent::Stepoff { .. } => println!("Stepoff"),
PlayEvent::NoPitch { .. } => println!("No Pitch"),
PlayEvent::Pickoff { .. } => println!("Pickoff"),
}
},
PlayStreamEvent::PlayEventReviewStart(review, _, _) => println!("PlayEventReviewStart; {}", review.review_type),
PlayStreamEvent::PlayEventReviewEnd(review, _, _) => println!("PlayEventReviewEnd; {}", review.review_type),
PlayStreamEvent::PlayReviewStart(review, _) => println!("PlayReviewStart; {}", review.review_type),
PlayStreamEvent::PlayReviewEnd(review, _) => println!("PlayReviewEnd; {}", review.review_type),
PlayStreamEvent::EndPlay(play) => println!("PlayEnd; {}", play.result.completed_play_details.as_ref().expect("Completed play").description),
PlayStreamEvent::GameEnd(_, _) => println!("GameEnd"),
}
Ok(ControlFlow::Continue(()))
})).await.unwrap();
}
}