use crate::{Copyright, NaiveDateRange};
use chrono::{Datelike, NaiveDate, Utc};
use derive_more::{Deref, Display, From};
use serde::{Deserialize, Deserializer};
use std::fmt::{Display, Formatter};
use bon::Builder;
use serde::de::Error;
use crate::request::RequestURL;
use crate::sport::SportId;
#[derive(Debug, Default, Deref, Display, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash, From)]
#[repr(transparent)]
pub struct SeasonId(u32);
impl<'de> Deserialize<'de> for SeasonId {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(::serde::Deserialize)]
#[serde(untagged)]
enum Repr {
Wrapped { id: u32 },
Inline(u32),
String(String),
}
let id = match Repr::deserialize(deserializer)? {
Repr::Wrapped { id } | Repr::Inline(id) => id,
Repr::String(id) => id.parse::<u32>().map_err(D::Error::custom)?,
};
Ok(Self(id))
}
}
impl SeasonId {
#[must_use]
pub const fn new(id: u32) -> Self {
Self(id)
}
#[allow(clippy::cast_sign_loss, reason = "jesus is not alive")]
#[must_use]
pub fn current_season() -> Self {
Self::new(Utc::now().year() as _)
}
}
#[derive(Deserialize)]
struct SeasonRaw {
#[serde(alias = "season", alias = "seasonId")]
pub id: SeasonId,
#[serde(default)] #[serde(rename = "hasWildcard")]
pub has_wildcard: bool,
#[serde(rename = "preSeasonStartDate")]
pub preseason_start: Option<NaiveDate>,
#[serde(rename = "preSeasonEndDate")]
pub preseason_end: Option<NaiveDate>,
#[serde(rename = "springStartDate")]
pub spring_start: Option<NaiveDate>,
#[serde(rename = "springEndDate")]
pub spring_end: Option<NaiveDate>,
#[serde(rename = "seasonStartDate")]
pub season_start: Option<NaiveDate>,
#[serde(rename = "regularSeasonStartDate")]
pub regular_season_start: Option<NaiveDate>,
#[serde(rename = "lastDate1stHalf")]
pub first_half_end: Option<NaiveDate>,
#[serde(rename = "allStarDate")]
pub all_star: Option<NaiveDate>,
#[serde(rename = "firstDate2ndHalf")]
pub second_half_start: Option<NaiveDate>,
#[serde(rename = "regularSeasonEndDate")]
pub regular_season_end: Option<NaiveDate>,
#[serde(rename = "postSeasonStartDate")]
pub postseason_start: Option<NaiveDate>,
#[serde(rename = "postSeasonEndDate")]
pub postseason_end: Option<NaiveDate>,
#[serde(rename = "seasonEndDate")]
pub season_end: Option<NaiveDate>,
#[serde(rename = "offseasonStartDate")]
pub offseason_start: Option<NaiveDate>,
#[serde(rename = "offSeasonEndDate")]
pub offseason_end: Option<NaiveDate>,
#[serde(flatten)]
pub qualification_multipliers: Option<QualificationMultipliers>,
}
impl From<SeasonRaw> for Season {
fn from(value: SeasonRaw) -> Self {
let SeasonRaw {
id,
has_wildcard,
preseason_start,
preseason_end,
spring_start,
spring_end,
season_start,
regular_season_start,
first_half_end,
all_star,
second_half_start,
regular_season_end,
postseason_start,
postseason_end,
season_end,
offseason_start,
offseason_end,
qualification_multipliers,
} = value;
let season_start = season_start.unwrap_or_else(|| NaiveDate::from_ymd_opt(*id as _, 1, 1).expect("Valid year"));
let offseason_end = offseason_end.unwrap_or_else(|| NaiveDate::from_ymd_opt(*id as _, 12, 31).expect("Valid year"));
Self {
id,
has_wildcard,
preseason: preseason_start.unwrap_or(season_start)..=preseason_end.unwrap_or(season_start),
spring: spring_start.zip(spring_end).map(|(start, end)| start..=end),
season: season_start..=season_end.unwrap_or(offseason_end),
regular_season: regular_season_start.unwrap_or(season_start)..=regular_season_end.or(season_end).unwrap_or(offseason_end),
first_half_end,
all_star,
second_half_start,
postseason: postseason_start.zip(postseason_end).map(|(start, end)| start..=end),
offseason: offseason_start.unwrap_or(offseason_end)..=offseason_end,
qualification_multipliers,
}
}
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(from = "SeasonRaw")]
pub struct Season {
pub id: SeasonId,
pub has_wildcard: bool,
pub preseason: NaiveDateRange,
pub spring: Option<NaiveDateRange>,
pub season: NaiveDateRange,
pub regular_season: NaiveDateRange,
pub first_half_end: Option<NaiveDate>,
pub all_star: Option<NaiveDate>,
pub second_half_start: Option<NaiveDate>,
pub postseason: Option<NaiveDateRange>,
pub offseason: NaiveDateRange,
pub qualification_multipliers: Option<QualificationMultipliers>,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct QualificationMultipliers {
#[serde(rename = "qualifierPlateAppearances")]
pub plate_appearances_per_game: f64,
#[serde(rename = "qualifierOutsPitched")]
pub outs_pitched_per_game: f64,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
pub enum SeasonState {
#[serde(rename = "spring training")]
SpringTraining,
#[serde(rename = "inseason")]
Inseason,
#[serde(rename = "offseason")]
Offseason,
#[serde(rename = "preseason")]
Preseason,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SeasonsResponse {
pub copyright: Copyright,
pub seasons: Vec<Season>,
}
#[derive(Builder)]
#[builder(derive(Into))]
pub struct SeasonsRequest {
#[builder(into)]
#[builder(default)]
sport_id: SportId,
#[builder(into)]
season: Option<SeasonId>,
}
impl<S: seasons_request_builder::State + seasons_request_builder::IsComplete> crate::request::RequestURLBuilderExt for SeasonsRequestBuilder<S> {
type Built = SeasonsRequest;
}
impl Display for SeasonsRequest {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "http://statsapi.mlb.com/api/v1/seasons{}", gen_params! { "sportId": self.sport_id, "season"?: self.season })
}
}
impl RequestURL for SeasonsRequest {
type Response = SeasonsResponse;
}
#[cfg(test)]
mod tests {
use crate::season::SeasonsRequest;
use crate::sport::SportsRequest;
use crate::TEST_YEAR;
use crate::request::RequestURLBuilderExt;
#[tokio::test]
#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
async fn parses_all_seasons() {
let all_sport_ids = SportsRequest::<()>::builder().build_and_get().await.unwrap().sports.into_iter().map(|sport| sport.id).collect::<Vec<_>>();
for season in 1871..=TEST_YEAR {
for id in all_sport_ids.iter().copied() {
let _response = SeasonsRequest::builder().sport_id(id).season(season).build_and_get().await.unwrap();
}
}
}
#[tokio::test]
async fn parse_this_season_mlb() {
let _response = SeasonsRequest::builder().build_and_get().await.unwrap();
}
}