1#![allow(non_snake_case)]
2
3use crate::game::{DoubleHeaderKind, GameId};
4use crate::league::LeagueId;
5use crate::season::SeasonId;
6use crate::team::TeamId;
7use crate::types::{Copyright, HomeAwaySplits, NaiveDateRange, MLB_API_DATE_FORMAT};
8use crate::venue::{NamedVenue, VenueId};
9use crate::game_status::GameStatus;
10use crate::game_types::GameType;
11use crate::request::RequestURL;
12use crate::sky::Sky;
13use crate::sport::SportId;
14use bon::Builder;
15use chrono::{NaiveDate, NaiveDateTime, Utc};
16use either::Either;
17use itertools::Itertools;
18use serde::Deserialize;
19use serde_with::serde_as;
20use serde_with::DefaultOnError;
21use std::fmt::{Display, Formatter};
22use uuid::Uuid;
23use crate::team::NamedTeam;
24
25pub mod postseason;
26pub mod tied;
27
28#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
29#[serde(rename_all = "camelCase")]
30pub struct ScheduleResponse {
31 pub copyright: Copyright,
32 pub dates: Vec<ScheduleDate>,
33}
34
35#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
36#[serde(rename_all = "camelCase")]
37pub struct ScheduleDate {
38 pub date: NaiveDate,
39 pub games: Vec<ScheduleGame>,
40}
41
42#[allow(clippy::struct_excessive_bools, reason = "false positive")]
43#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
44#[serde(from = "__ScheduleGameStruct")]
45pub struct ScheduleGame {
46 pub game_id: GameId,
47 pub game_guid: Uuid,
48 pub game_type: GameType,
49 pub season: SeasonId,
50 pub game_date: NaiveDateTime,
51 pub official_date: NaiveDate,
53 pub status: GameStatus,
54 pub teams: HomeAwaySplits<TeamWithStandings>,
55 pub venue: NamedVenue,
56 pub is_tie: bool,
57
58 pub game_ordinal: u32,
61 pub is_public_facing: bool,
62 pub double_header: DoubleHeaderKind,
63 pub is_tiebreaker: bool,
66 pub displayed_season: SeasonId,
68 pub day_night: Sky,
69 pub description: Option<String>,
70 pub scheduled_innings: u32,
71 pub reverse_home_away_status: bool,
72 pub inning_break_length: uom::si::i32::Time,
73 pub series_data: Option<SeriesData>,
75}
76
77#[serde_as]
78#[derive(Deserialize)]
79#[serde(rename_all = "camelCase")]
80struct __ScheduleGameStruct {
81 #[serde(rename = "gamePk")]
82 game_id: GameId,
83 game_guid: Uuid,
84 game_type: GameType,
85 season: SeasonId,
86 #[serde(deserialize_with = "crate::types::deserialize_datetime")]
87 game_date: NaiveDateTime,
88 official_date: NaiveDate,
89 status: GameStatus,
90 teams: HomeAwaySplits<TeamWithStandings>,
91 #[serde_as(deserialize_as = "DefaultOnError")]
92 venue: Option<NamedVenue>,
93 is_tie: Option<bool>,
94 #[serde(rename = "gameNumber")]
95 game_ordinal: u32,
96 #[serde(rename = "publicFacing")]
97 is_public_facing: bool,
98 double_header: DoubleHeaderKind,
99 #[serde(rename = "tiebreaker", deserialize_with = "crate::types::from_yes_no")]
102 is_tiebreaker: bool,
103 #[serde(rename = "seasonDisplay")]
105 displayed_season: SeasonId,
106 day_night: Sky,
107 description: Option<String>,
108 scheduled_innings: u32,
109 reverse_home_away_status: bool,
110 inning_break_length: Option<u32>,
111 #[serde(flatten)]
112 series_data: Option<SeriesData>,
113}
114
115impl From<__ScheduleGameStruct> for ScheduleGame {
116 #[allow(clippy::cast_possible_wrap, reason = "not gonna happen")]
117 fn from(
118 __ScheduleGameStruct {
119 game_id,
120 game_guid,
121 game_type,
122 season,
123 game_date,
124 official_date,
125 status,
126 teams,
127 venue,
128 is_tie,
129 game_ordinal,
130 is_public_facing,
131 double_header,
132 is_tiebreaker,
133 displayed_season,
134 day_night,
135 description,
136 scheduled_innings,
137 reverse_home_away_status,
138 inning_break_length,
139 series_data,
140 }: __ScheduleGameStruct,
141 ) -> Self {
142 Self {
143 game_id,
144 game_guid,
145 game_type,
146 season,
147 game_date,
148 official_date,
149 status,
150 teams,
151 venue: venue.unwrap_or_else(NamedVenue::unknown_venue),
152 is_tie: is_tie.unwrap_or(false),
153 game_ordinal,
154 is_public_facing,
155 double_header,
156 is_tiebreaker,
157 displayed_season,
158 day_night,
159 description,
160 scheduled_innings,
161 reverse_home_away_status,
162 inning_break_length: uom::si::i32::Time::new::<uom::si::time::second>(inning_break_length.unwrap_or(120) as i32),
163 series_data,
164 }
165 }
166}
167
168#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
169#[serde(rename_all = "camelCase")]
170pub struct SeriesData {
171 pub games_in_series: u32,
172 #[serde(rename = "seriesGameNumber")]
173 pub game_in_series_ordinal: u32,
174}
175
176#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
177#[serde(rename_all = "camelCase")]
178pub struct TeamWithStandings {
179 pub team: NamedTeam,
180 #[serde(rename = "leagueRecord")]
181 pub standings: Standings,
182 #[serde(flatten)]
183 pub score: Option<TeamWithStandingsGameScore>,
184 #[serde(rename = "splitSquad")]
185 pub is_split_squad_game: bool,
186
187 #[serde(rename = "seriesNumber")]
191 pub series_ordinal: Option<u32>,
192}
193
194#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
195#[serde(rename_all = "camelCase")]
196pub struct TeamWithStandingsGameScore {
197 #[serde(rename = "score")]
198 pub runs_scored: u32,
199 pub is_winner: bool,
200}
201
202#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
203#[serde(rename_all = "camelCase")]
204pub struct Standings {
205 pub wins: u32,
206 pub losses: u32,
207}
208
209impl Standings {
210 #[must_use]
211 pub const fn games_played(self) -> u32 {
212 self.wins + self.losses
213 }
214
215 #[must_use]
216 pub fn pct(self) -> f64 {
217 f64::from(self.wins) / f64::from(self.games_played())
218 }
219}
220
221#[allow(dead_code, reason = "rust analyzer says that opponent_id and season are dead, while being used in Display")]
222#[derive(Builder)]
223#[builder(derive(Into))]
224pub struct ScheduleRequest {
225 #[builder(into)]
226 #[builder(default)]
227 sport_id: SportId,
228 #[builder(setters(vis = "", name = __game_ids_internal))]
229 game_ids: Option<Vec<GameId>>,
230 #[builder(into)]
231 team_id: Option<TeamId>,
232 #[builder(into)]
233 league_id: Option<LeagueId>,
234 #[builder(setters(vis = "", name = __venue_ids_internal))]
235 venue_ids: Option<Vec<VenueId>>,
236 #[builder(default = Either::Left(Utc::now().date_naive()))]
237 #[builder(setters(vis = "", name = __date_internal))]
238 date: Either<NaiveDate, NaiveDateRange>,
239 #[builder(into)]
240 opponent_id: Option<TeamId>,
241 #[builder(into)]
242 season: Option<SeasonId>,
243}
244
245
246impl<S: schedule_request_builder::State + schedule_request_builder::IsComplete> crate::request::RequestURLBuilderExt for ScheduleRequestBuilder<S> {
247 type Built = ScheduleRequest;
248}
249
250impl<S: schedule_request_builder::State> ScheduleRequestBuilder<S> {
251 pub fn game_ids(self, game_ids: Vec<impl Into<GameId>>) -> ScheduleRequestBuilder<schedule_request_builder::SetGameIds<S>> where S::GameIds: schedule_request_builder::IsUnset {
252 self.__game_ids_internal(game_ids.into_iter().map(Into::into).collect())
253 }
254
255 pub fn venue_ids(self, venue_ids: Vec<impl Into<VenueId>>) -> ScheduleRequestBuilder<schedule_request_builder::SetVenueIds<S>> where S::VenueIds: schedule_request_builder::IsUnset {
256 self.__venue_ids_internal(venue_ids.into_iter().map(Into::into).collect())
257 }
258
259 pub fn date(self, date: NaiveDate) -> ScheduleRequestBuilder<schedule_request_builder::SetDate<S>> where S::Date: schedule_request_builder::IsUnset {
260 self.__date_internal(Either::Left(date))
261 }
262
263 pub fn date_range(self, range: NaiveDateRange) -> ScheduleRequestBuilder<schedule_request_builder::SetDate<S>> where S::Date: schedule_request_builder::IsUnset {
264 self.__date_internal(Either::Right(range))
265 }
266}
267
268impl Display for ScheduleRequest {
269 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
270 write!(
271 f,
272 "http://statsapi.mlb.com/api/v1/schedule{params}",
273 params = gen_params! {
274 "sportId": self.sport_id,
275 "gamePks"?: self.game_ids.as_ref().map(|ids| ids.iter().map(ToString::to_string).join(",")),
276 "teamId"?: self.team_id,
277 "leagueId"?: self.league_id,
278 "venueIds"?: self.venue_ids.as_ref().map(|ids| ids.iter().map(ToString::to_string).join(",")),
279 "date"?: self.date.as_ref().left().map(|x| x.format(MLB_API_DATE_FORMAT)),
280 "startDate"?: self.date.as_ref().right().map(|range| range.start().format(MLB_API_DATE_FORMAT)),
281 "endDate"?: self.date.as_ref().right().map(|range| range.end().format(MLB_API_DATE_FORMAT)),
282 "opponentId"?; self.opponent_id,
283 "season"?: self.season,
284 }
285 )
286 }
287}
288
289impl RequestURL for ScheduleRequest {
290 type Response = ScheduleResponse;
291}
292
293#[cfg(test)]
294mod tests {
295 use crate::schedule::ScheduleRequest;
296 use crate::TEST_YEAR;
297 use chrono::NaiveDate;
298 use crate::request::RequestURLBuilderExt;
299
300 #[tokio::test]
301 async fn test_one_date() {
302 let date = NaiveDate::from_ymd_opt(2020, 8, 2).expect("Valid date");
303 let _ = ScheduleRequest::builder().date(date).build_and_get().await.unwrap();
304 }
305
306 #[tokio::test]
307 async fn test_all_dates_current_year() {
308 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();
309 }
310
311 #[tokio::test]
312 #[cfg_attr(not(feature = "_heavy_tests"), ignore)]
313 async fn test_all_dates_all_years() {
314 for year in 1876..=TEST_YEAR {
315 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())
316 .build_and_get()
317 .await
318 .unwrap();
319 }
320 }
321}