Skip to main content

mlb_api/requests/
season.rs

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)] // will be overwriten if not present because of bad league schedule schema
51	#[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	// opt<(season_level_gameday_type, game_level_gameday_type)>
144}
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}