1use crate::{Copyright, NaiveDateRange};
6use chrono::{Datelike, NaiveDate, Utc};
7use derive_more::{Deref, Display, From};
8use serde::{Deserialize, Deserializer};
9use std::fmt::{Display, Formatter};
10use bon::Builder;
11use serde::de::Error;
12use crate::request::RequestURL;
13use crate::sport::SportId;
14
15#[derive(Debug, Default, Deref, Display, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash, From)]
16#[repr(transparent)]
17pub struct SeasonId(u32);
18
19impl<'de> Deserialize<'de> for SeasonId {
20 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
21 #[derive(::serde::Deserialize)]
22 #[serde(untagged)]
23 enum Repr {
24 Wrapped { id: u32 },
25 Inline(u32),
26 String(String),
27 }
28
29 let id = match Repr::deserialize(deserializer)? {
30 Repr::Wrapped { id } | Repr::Inline(id) => id,
31 Repr::String(id) => id.parse::<u32>().map_err(D::Error::custom)?,
32 };
33 Ok(Self(id))
34 }
35}
36
37impl SeasonId {
38 #[must_use]
39 pub const fn new(id: u32) -> Self {
40 Self(id)
41 }
42
43 #[allow(clippy::cast_sign_loss, reason = "jesus is not alive")]
44 #[must_use]
45 pub fn current_season() -> Self {
46 Self::new(Utc::now().year() as _)
47 }
48}
49
50#[derive(Deserialize)]
51struct SeasonRaw {
52 #[serde(alias = "season", alias = "seasonId")]
53 pub id: SeasonId,
54
55 #[serde(default)] #[serde(rename = "hasWildcard")]
57 pub has_wildcard: bool,
58
59 #[serde(rename = "preSeasonStartDate")]
60 pub preseason_start: Option<NaiveDate>,
61 #[serde(rename = "preSeasonEndDate")]
62 pub preseason_end: Option<NaiveDate>,
63 #[serde(rename = "springStartDate")]
64 pub spring_start: Option<NaiveDate>,
65 #[serde(rename = "springEndDate")]
66 pub spring_end: Option<NaiveDate>,
67 #[serde(rename = "seasonStartDate")]
68 pub season_start: Option<NaiveDate>,
69 #[serde(rename = "regularSeasonStartDate")]
70 pub regular_season_start: Option<NaiveDate>,
71 #[serde(rename = "lastDate1stHalf")]
72 pub first_half_end: Option<NaiveDate>,
73 #[serde(rename = "allStarDate")]
74 pub all_star: Option<NaiveDate>,
75 #[serde(rename = "firstDate2ndHalf")]
76 pub second_half_start: Option<NaiveDate>,
77 #[serde(rename = "regularSeasonEndDate")]
78 pub regular_season_end: Option<NaiveDate>,
79 #[serde(rename = "postSeasonStartDate")]
80 pub postseason_start: Option<NaiveDate>,
81 #[serde(rename = "postSeasonEndDate")]
82 pub postseason_end: Option<NaiveDate>,
83 #[serde(rename = "seasonEndDate")]
84 pub season_end: Option<NaiveDate>,
85 #[serde(rename = "offseasonStartDate")]
86 pub offseason_start: Option<NaiveDate>,
87 #[serde(rename = "offSeasonEndDate")]
88 pub offseason_end: Option<NaiveDate>,
89 #[serde(flatten)]
90 pub qualification_multipliers: Option<QualificationMultipliers>,
91}
92
93impl From<SeasonRaw> for Season {
94 fn from(value: SeasonRaw) -> Self {
95 let SeasonRaw {
96 id,
97 has_wildcard,
98 preseason_start,
99 preseason_end,
100 spring_start,
101 spring_end,
102 season_start,
103 regular_season_start,
104 first_half_end,
105 all_star,
106 second_half_start,
107 regular_season_end,
108 postseason_start,
109 postseason_end,
110 season_end,
111 offseason_start,
112 offseason_end,
113 qualification_multipliers,
114 } = value;
115
116 let season_start = season_start.unwrap_or_else(|| NaiveDate::from_ymd_opt(*id as _, 1, 1).expect("Valid year"));
118 let offseason_end = offseason_end.unwrap_or_else(|| NaiveDate::from_ymd_opt(*id as _, 12, 31).expect("Valid year"));
119
120 Self {
121 id,
122 has_wildcard,
123 preseason: preseason_start.unwrap_or(season_start)..=preseason_end.unwrap_or(season_start),
124 spring: spring_start.zip(spring_end).map(|(start, end)| start..=end),
125 season: season_start..=season_end.unwrap_or(offseason_end),
126 regular_season: regular_season_start.unwrap_or(season_start)..=regular_season_end.or(season_end).unwrap_or(offseason_end),
127 first_half_end,
128 all_star,
129 second_half_start,
130 postseason: postseason_start.zip(postseason_end).map(|(start, end)| start..=end),
131 offseason: offseason_start.unwrap_or(offseason_end)..=offseason_end,
132 qualification_multipliers,
133 }
134 }
135}
136
137#[derive(Debug, Deserialize, PartialEq, Clone)]
142#[serde(from = "SeasonRaw")]
143pub struct Season {
144 pub id: SeasonId,
145 pub has_wildcard: bool,
147 pub preseason: NaiveDateRange,
149 pub spring: Option<NaiveDateRange>,
151 pub season: NaiveDateRange,
153 pub regular_season: NaiveDateRange,
155 pub first_half_end: Option<NaiveDate>,
157 pub all_star: Option<NaiveDate>,
159 pub second_half_start: Option<NaiveDate>,
161 pub postseason: Option<NaiveDateRange>,
163 pub offseason: NaiveDateRange,
165 pub qualification_multipliers: Option<QualificationMultipliers>,
167 }
169
170#[derive(Debug, Deserialize, PartialEq, Clone)]
172#[serde(rename_all = "camelCase")]
173pub struct QualificationMultipliers {
174 #[serde(rename = "qualifierPlateAppearances")]
176 pub plate_appearances_per_game: f64,
177 #[serde(rename = "qualifierOutsPitched")]
179 pub outs_pitched_per_game: f64,
180}
181
182#[derive(Debug, Deserialize, PartialEq, Copy, Clone)]
184pub enum SeasonState {
185 #[serde(rename = "spring training")]
186 SpringTraining,
187 #[serde(rename = "inseason")]
188 Inseason,
189 #[serde(rename = "offseason")]
190 Offseason,
191 #[serde(rename = "preseason")]
192 Preseason,
193}
194
195#[derive(Debug, Deserialize, PartialEq, Clone)]
197#[serde(rename_all = "camelCase")]
198pub struct SeasonsResponse {
199 pub copyright: Copyright,
200 pub seasons: Vec<Season>,
201}
202
203#[derive(Builder)]
205#[builder(derive(Into))]
206pub struct SeasonsRequest {
207 #[builder(into)]
208 #[builder(default)]
209 sport_id: SportId,
210 #[builder(into)]
211 season: Option<SeasonId>,
212}
213
214impl<S: seasons_request_builder::State + seasons_request_builder::IsComplete> crate::request::RequestURLBuilderExt for SeasonsRequestBuilder<S> {
215 type Built = SeasonsRequest;
216}
217
218impl Display for SeasonsRequest {
219 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
220 write!(f, "http://statsapi.mlb.com/api/v1/seasons{}", gen_params! { "sportId": self.sport_id, "season"?: self.season })
221 }
222}
223
224impl RequestURL for SeasonsRequest {
225 type Response = SeasonsResponse;
226}
227
228#[cfg(test)]
229mod tests {
230 use crate::season::SeasonsRequest;
231 use crate::sport::SportsRequest;
232 use crate::TEST_YEAR;
233 use crate::request::RequestURLBuilderExt;
234
235 #[tokio::test]
236 #[cfg_attr(not(feature = "_heavy_tests"), ignore)]
237 async fn parses_all_seasons() {
238 let all_sport_ids = SportsRequest::<()>::builder().build_and_get().await.unwrap().sports.into_iter().map(|sport| sport.id).collect::<Vec<_>>();
239
240 for season in 1871..=TEST_YEAR {
241 for id in all_sport_ids.iter().copied() {
242 let _response = SeasonsRequest::builder().sport_id(id).season(season).build_and_get().await.unwrap();
243 }
244 }
245 }
246
247 #[tokio::test]
248 async fn parse_this_season_mlb() {
249 let _response = SeasonsRequest::builder().build_and_get().await.unwrap();
250 }
251}