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