use crate::game::{DoubleHeaderKind, GameId};
use crate::hydrations::Hydrations;
use crate::league::LeagueId;
use crate::meta::DayNight;
use crate::meta::GameStatus;
use crate::meta::GameType;
use crate::request::RequestURL;
use crate::season::SeasonId;
use crate::sport::SportId;
use crate::team::NamedTeam;
use crate::team::TeamId;
use crate::venue::{NamedVenue, VenueId};
use crate::{Copyright, HomeAway, MLB_API_DATE_FORMAT, NaiveDateRange};
use bon::Builder;
use chrono::{DateTime, NaiveDate, Utc};
use either::Either;
use itertools::Itertools;
use serde::Deserialize;
use serde::de::DeserializeOwned;
use serde_with::DefaultOnError;
use serde_with::serde_as;
use std::fmt::{Debug, Display, Formatter};
use std::marker::PhantomData;
use uuid::Uuid;
pub mod postseason;
pub mod tied;
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase", bound = "H: ScheduleHydrations")]
pub struct ScheduleResponse<H: ScheduleHydrations> {
#[serde(default)] pub copyright: Copyright,
pub dates: Vec<ScheduleDate<H>>,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase", bound = "H: ScheduleHydrations")]
pub struct ScheduleDate<H: ScheduleHydrations> {
pub date: NaiveDate,
pub games: Vec<ScheduleGame<H>>,
}
#[allow(clippy::struct_excessive_bools, reason = "false positive")]
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(from = "__ScheduleGameStruct<H>", bound = "H: ScheduleHydrations")]
pub struct ScheduleGame<H: ScheduleHydrations> {
pub game_id: GameId,
pub game_guid: Uuid,
pub game_type: GameType,
pub season: SeasonId,
pub game_date: DateTime<Utc>,
pub official_date: NaiveDate,
pub status: GameStatus,
pub teams: HomeAway<TeamWithStandings<H>>,
pub venue: NamedVenue,
pub is_tie: bool,
pub game_ordinal: u32,
pub is_public_facing: bool,
pub double_header: DoubleHeaderKind,
pub is_tiebreaker: bool,
pub displayed_season: SeasonId,
pub day_night: DayNight,
pub description: Option<String>,
pub scheduled_innings: u32,
pub reverse_home_away_status: bool,
pub inning_break_length: uom::si::i32::Time,
pub series_data: Option<SeriesData>,
}
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", bound = "H: ScheduleHydrations")]
struct __ScheduleGameStruct<H: ScheduleHydrations> {
#[serde(rename = "gamePk")]
game_id: GameId,
game_guid: Uuid,
game_type: GameType,
season: SeasonId,
#[serde(deserialize_with = "crate::deserialize_datetime")]
game_date: DateTime<Utc>,
official_date: NaiveDate,
status: GameStatus,
teams: HomeAway<TeamWithStandings<H>>,
#[serde_as(deserialize_as = "DefaultOnError")]
venue: Option<NamedVenue>,
is_tie: Option<bool>,
#[serde(rename = "gameNumber")]
game_ordinal: u32,
#[serde(rename = "publicFacing")]
is_public_facing: bool,
double_header: DoubleHeaderKind,
#[serde(rename = "tiebreaker", deserialize_with = "crate::from_yes_no")]
is_tiebreaker: bool,
#[serde(rename = "seasonDisplay")]
displayed_season: SeasonId,
day_night: DayNight,
description: Option<String>,
scheduled_innings: u32,
reverse_home_away_status: bool,
inning_break_length: Option<u32>,
#[serde(flatten)]
series_data: Option<SeriesData>,
}
impl<H: ScheduleHydrations> From<__ScheduleGameStruct<H>> for ScheduleGame<H> {
#[allow(clippy::cast_possible_wrap, reason = "not gonna happen")]
fn from(
__ScheduleGameStruct {
game_id,
game_guid,
game_type,
season,
game_date,
official_date,
status,
teams,
venue,
is_tie,
game_ordinal,
is_public_facing,
double_header,
is_tiebreaker,
displayed_season,
day_night,
description,
scheduled_innings,
reverse_home_away_status,
inning_break_length,
series_data,
}: __ScheduleGameStruct<H>,
) -> Self {
Self {
game_id,
game_guid,
game_type,
season,
game_date,
official_date,
status,
teams,
venue: venue.unwrap_or_else(NamedVenue::unknown_venue),
is_tie: is_tie.unwrap_or(false),
game_ordinal,
is_public_facing,
double_header,
is_tiebreaker,
displayed_season,
day_night,
description,
scheduled_innings,
reverse_home_away_status,
inning_break_length: uom::si::i32::Time::new::<uom::si::time::second>(inning_break_length.unwrap_or(120) as i32),
series_data,
}
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SeriesData {
pub games_in_series: u32,
#[serde(rename = "seriesGameNumber")]
pub game_in_series_ordinal: u32,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase", bound = "H: ScheduleHydrations")]
pub struct TeamWithStandings<H: ScheduleHydrations> {
pub team: H::Team,
#[serde(rename = "leagueRecord")]
pub standings: Standings,
#[serde(flatten)]
pub score: Option<TeamWithStandingsGameScore>,
#[serde(rename = "splitSquad")]
pub is_split_squad_game: bool,
#[serde(rename = "seriesNumber")]
pub series_ordinal: Option<u32>,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct TeamWithStandingsGameScore {
#[serde(rename = "score")]
pub runs_scored: u32,
pub is_winner: bool,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Standings {
pub wins: u32,
pub losses: u32,
}
impl Standings {
#[must_use]
pub const fn games_played(self) -> u32 {
self.wins + self.losses
}
#[must_use]
pub fn pct(self) -> f64 {
f64::from(self.wins) / f64::from(self.games_played())
}
}
pub trait ScheduleHydrations: Hydrations<RequestData = ()> {
type Team: Debug + DeserializeOwned + Clone + PartialEq;
type Venue: Debug + DeserializeOwned + Clone + PartialEq;
}
impl ScheduleHydrations for () {
type Team = NamedTeam;
type Venue = NamedVenue;
}
#[macro_export]
macro_rules! schedule_hydrations {
(@ inline_structs [team: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::macro_use::pastey::paste! {
$crate::team_hydrations! {
$vis struct [<$name InlineTeam>] {
$($inline_tt)*
}
}
$crate::schedule_hydrations! { @ inline_structs [$($($tt)*)?]
$vis struct $name {
$($field_tt)*
team: [<$name InlineTeam>],
}
}
}
};
(@ inline_structs [venue: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::macro_use::pastey::paste! {
$crate::venue_hydrations! {
$vis struct [<$name InlineVenue>] {
$($inline_tt)*
}
}
$crate::schedule_hydrations! { @ inline_structs [$($($tt)*)?]
$vis struct $name {
$($field_tt)*
venue: [<$name InlineVenue>],
}
}
}
};
(@ inline_structs [$_01:ident : { $($_02:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
::core::compile_error!("Found unknown inline struct");
};
(@ inline_structs [$field:ident $(: $value:ty)? $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::schedule_hydrations! { @ inline_structs [$($($tt)*)?]
$vis struct $name {
$($field_tt)*
$field $(: $value)?,
}
}
};
(@ inline_structs [] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::schedule_hydrations! { @ actual
$vis struct $name {
$($field_tt)*
}
}
};
(@ team) => { $crate::team::NamedTeam };
(@ team $hydrations:ty) => { $crate::team::Team<$hydrations> };
(@ venue) => { $crate::venue::NamedVenue };
(@ venue $hydrations:ty) => { $crate::venue::Venue<$hydrations> };
(@ actual $vis:vis struct $name:ident {
$(team: $team:ty ,)?
$(venue: $venue:ty)?
}) => {
#[derive(::core::fmt::Debug, $crate::macro_use::serde::Deserialize, ::core::cmp::PartialEq, ::core::clone::Clone)]
#[serde(rename_all = "camelCase")]
$vis struct $name {
}
impl $crate::schedule::ScheduleHydrations for $name {
type Team = $crate::schedule_hydrations!(@ team $($team)?);
type Venue = $crate::schedule_hydrations!(@ venue $($venue)?);
}
impl $crate::hydrations::Hydrations for $name {
type RequestData = ();
fn hydration_text(&(): Self::RequestData) -> ::std::borrow::Cow<'static, str> {
let text = ::std::borrow::Cow::Borrowed(::core::concat!(
));
$(let text = ::std::borrow::Cow::Owned(::std::format!("{text}team({}),", <$team as $crate::hydrations::Hydrations>::hydration_text(&()))))?
$(let text = ::std::borrow::Cow::Owned(::std::format!("{text}venue({}),", <$venue as $crate::hydrations::Hydrations>::hydration_text(&()))))?
text
}
}
};
}
#[allow(dead_code, reason = "rust analyzer says that opponent_id and season are dead, while being used in Display")]
#[derive(Builder)]
#[builder(derive(Into))]
pub struct ScheduleRequest<H: ScheduleHydrations> {
#[builder(into)]
#[builder(default)]
sport_id: SportId,
#[builder(setters(vis = "", name = game_ids_internal))]
game_ids: Option<Vec<GameId>>,
#[builder(into)]
team_id: Option<TeamId>,
#[builder(into)]
league_id: Option<LeagueId>,
#[builder(setters(vis = "", name = venue_ids_internal))]
venue_ids: Option<Vec<VenueId>>,
#[builder(default = Either::Left(Utc::now().date_naive()))]
#[builder(setters(vis = "", name = date_internal))]
date: Either<NaiveDate, NaiveDateRange>,
#[builder(into)]
opponent_id: Option<TeamId>,
#[builder(into)]
season: Option<SeasonId>,
game_type: Option<GameType>,
#[builder(skip)]
_marker: PhantomData<H>,
}
impl<H: ScheduleHydrations, S: schedule_request_builder::State + schedule_request_builder::IsComplete> crate::request::RequestURLBuilderExt for ScheduleRequestBuilder<H, S> {
type Built = ScheduleRequest<H>;
}
impl<H: ScheduleHydrations, S: schedule_request_builder::State> ScheduleRequestBuilder<H, S> {
pub fn game_ids(self, game_ids: Vec<impl Into<GameId>>) -> ScheduleRequestBuilder<H, schedule_request_builder::SetGameIds<S>>
where
S::GameIds: schedule_request_builder::IsUnset,
{
self.game_ids_internal(game_ids.into_iter().map(Into::into).collect())
}
pub fn venue_ids(self, venue_ids: Vec<impl Into<VenueId>>) -> ScheduleRequestBuilder<H, schedule_request_builder::SetVenueIds<S>>
where
S::VenueIds: schedule_request_builder::IsUnset,
{
self.venue_ids_internal(venue_ids.into_iter().map(Into::into).collect())
}
pub fn date(self, date: NaiveDate) -> ScheduleRequestBuilder<H, schedule_request_builder::SetDate<S>>
where
S::Date: schedule_request_builder::IsUnset,
{
self.date_internal(Either::Left(date))
}
pub fn date_range(self, range: NaiveDateRange) -> ScheduleRequestBuilder<H, schedule_request_builder::SetDate<S>>
where
S::Date: schedule_request_builder::IsUnset,
{
self.date_internal(Either::Right(range))
}
}
impl<H: ScheduleHydrations> Display for ScheduleRequest<H> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let hydrations = Some(H::hydration_text(&())).filter(|s| !s.is_empty());
write!(
f,
"http://statsapi.mlb.com/api/v1/schedule{params}",
params = gen_params! {
"hydrate"?: hydrations,
"sportId": self.sport_id,
"gamePks"?: self.game_ids.as_ref().map(|ids| ids.iter().map(ToString::to_string).join(",")),
"teamId"?: self.team_id,
"leagueId"?: self.league_id,
"date"?: self.date.as_ref().left().map(|x| x.format(MLB_API_DATE_FORMAT)),
"startDate"?: self.date.as_ref().right().map(|range| range.start().format(MLB_API_DATE_FORMAT)),
"endDate"?: self.date.as_ref().right().map(|range| range.end().format(MLB_API_DATE_FORMAT)),
"opponentId"?; self.opponent_id,
"season"?: self.season,
"venueIds"?: self.venue_ids.as_ref().map(|ids| ids.iter().map(ToString::to_string).join(",")),
"gameType"?: self.game_type,
}
)
}
}
impl<H: ScheduleHydrations> RequestURL for ScheduleRequest<H> {
type Response = ScheduleResponse<H>;
}
#[cfg(test)]
mod tests {
use crate::TEST_YEAR;
use crate::request::RequestURLBuilderExt;
use crate::schedule::ScheduleRequest;
use chrono::NaiveDate;
#[tokio::test]
async fn test_one_date() {
let date = NaiveDate::from_ymd_opt(2020, 8, 2).expect("Valid date");
let _ = ScheduleRequest::<()>::builder().date(date).build_and_get().await.unwrap();
}
#[tokio::test]
async fn test_all_dates_current_year() {
let _ = ScheduleRequest::<()>::builder()
.date_range(NaiveDate::from_ymd_opt(TEST_YEAR.try_into().unwrap(), 1, 1).expect("Valid date")..=NaiveDate::from_ymd_opt(TEST_YEAR.try_into().unwrap(), 12, 31).expect("Valid date"))
.build_and_get()
.await
.unwrap();
}
#[tokio::test]
#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
async fn test_all_dates_all_years() {
for year in 1876..=TEST_YEAR {
let _ = ScheduleRequest::<()>::builder()
.date_range(NaiveDate::from_ymd_opt(year.try_into().unwrap(), 1, 1).unwrap()..=NaiveDate::from_ymd_opt(year.try_into().unwrap(), 12, 31).unwrap())
.build_and_get()
.await
.unwrap();
}
}
}