1use std::fmt::{Display, Formatter};
2
3use bon::Builder;
4use derive_more::{Deref, DerefMut};
5use serde::Deserialize;
6use serde::de::IgnoredAny;
7use serde_with::{serde_as, DefaultOnError};
8
9use crate::request::RequestURL;
10use crate::{Copyright, HomeAway};
11use crate::game::{AtBatCount, GameId, Inning, InningHalf, RHE};
12use crate::person::NamedPerson;
13use crate::team::NamedTeam;
14
15#[serde_as]
25#[derive(Debug, Deserialize, PartialEq, Clone)]
26#[serde(rename_all = "camelCase")]
27#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
28pub struct Linescore {
29 #[serde(default)]
30 pub copyright: Copyright,
31 #[serde(default = "Inning::starting")]
32 pub current_inning: Inning,
33 #[serde(default = "InningHalf::starting")]
34 pub inning_half: InningHalf,
35 pub scheduled_innings: usize,
36 pub innings: Vec<LinescoreInningRecord>,
37 #[serde(rename = "teams")]
38 pub rhe_totals: HomeAway<RHE>,
39 #[serde_as(deserialize_as = "DefaultOnError")]
40 pub offense: Option<LinescoreOffense>,
41 #[serde_as(deserialize_as = "DefaultOnError")]
42 pub defense: Option<LinescoreDefense>,
43 pub note: Option<String>,
44 #[serde(flatten)]
45 pub count: AtBatCount,
46
47 #[doc(hidden)]
48 #[serde(rename = "currentInningOrdinal", default)]
49 pub __current_inning_ordinal: IgnoredAny,
50 #[doc(hidden)]
51 #[serde(rename = "inningState", default)]
52 pub __inning_state: IgnoredAny,
53 #[doc(hidden)]
54 #[serde(rename = "isTopInning", default)]
55 pub __is_top_inning: IgnoredAny,
56}
57
58#[derive(Debug, Deserialize, PartialEq, Clone)]
60#[serde(rename_all = "camelCase")]
61#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
62pub struct LinescoreInningRecord {
63 #[serde(rename = "num")]
64 pub inning: Inning,
65 #[serde(flatten)]
66 pub inning_record: HomeAway<RHE>,
67
68 #[doc(hidden)]
69 #[serde(rename = "ordinalNum", default)]
70 pub __ordinal_num: IgnoredAny,
71}
72
73#[derive(Debug, Deserialize, PartialEq, Clone)]
75#[serde(rename_all = "camelCase")]
76#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
77pub struct LinescoreOffense {
78 pub batter: NamedPerson,
79 pub on_deck: NamedPerson,
80 #[serde(rename = "inHole")]
81 pub in_the_hole: NamedPerson,
82 pub team: NamedTeam,
83 #[serde(rename = "battingOrder")]
85 pub batting_order_index: usize,
86
87 #[doc(hidden)]
88 #[serde(rename = "pitcher", default)]
89 pub __pitcher: IgnoredAny,
90 #[doc(hidden)]
91 #[serde(rename = "catcher", default)]
92 pub __catcher: IgnoredAny,
93 #[doc(hidden)]
94 #[serde(rename = "first", default)]
95 pub __first_baseman: IgnoredAny,
96 #[doc(hidden)]
97 #[serde(rename = "second", default)]
98 pub __second_baseman: IgnoredAny,
99 #[doc(hidden)]
100 #[serde(rename = "third", default)]
101 pub __third_baseman: IgnoredAny,
102 #[doc(hidden)]
103 #[serde(rename = "shortstop", default)]
104 pub __shortstop: IgnoredAny,
105 #[doc(hidden)]
106 #[serde(rename = "left", default)]
107 pub __leftfielder: IgnoredAny,
108 #[doc(hidden)]
109 #[serde(rename = "center", default)]
110 pub __centerfielder: IgnoredAny,
111 #[doc(hidden)]
112 #[serde(rename = "right", default)]
113 pub __rightfielder: IgnoredAny,
114}
115
116#[derive(Debug, Deserialize, PartialEq, Clone, Deref, DerefMut)]
118#[cfg_attr(feature = "_debug", serde(deny_unknown_fields))]
119pub struct LinescoreDefense {
120 pub pitcher: NamedPerson,
121 pub catcher: NamedPerson,
122 #[serde(rename = "first")]
123 pub first_baseman: NamedPerson,
124 #[serde(rename = "second")]
125 pub second_baseman: NamedPerson,
126 #[serde(rename = "third")]
127 pub third_baseman: NamedPerson,
128 pub shortstop: NamedPerson,
129 #[serde(rename = "left")]
130 pub leftfielder: NamedPerson,
131 #[serde(rename = "center")]
132 pub centerfielder: NamedPerson,
133 #[serde(rename = "right")]
134 pub rightfielder: NamedPerson,
135
136 #[deref]
137 #[deref_mut]
138 #[serde(flatten)]
139 pub offense: LinescoreOffense,
140}
141
142#[derive(Builder)]
144#[builder(derive(Into))]
145pub struct LinescoreRequest {
146 #[builder(into)]
147 id: GameId
148}
149
150impl<S: linescore_request_builder::State + linescore_request_builder::IsComplete> crate::request::RequestURLBuilderExt for LinescoreRequestBuilder<S> {
151 type Built = LinescoreRequest;
152}
153
154impl Display for LinescoreRequest {
155 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
156 write!(f, "http://statsapi.mlb.com/api/v1/game/{}/linescore", self.id)
157 }
158}
159
160impl RequestURL for LinescoreRequest {
161 type Response = Linescore;
162}
163
164#[cfg(test)]
165mod tests {
166 use crate::TEST_YEAR;
167 use crate::game::LinescoreRequest;
168 use crate::meta::GameType;
169 use crate::request::RequestURLBuilderExt;
170 use crate::schedule::ScheduleRequest;
171 use crate::season::{Season, SeasonsRequest};
172 use crate::sport::SportId;
173
174 #[tokio::test]
175 async fn ws_gm7_2025_linescore() {
176 let _ = LinescoreRequest::builder().id(813_024).build_and_get().await.unwrap();
177 }
178
179 #[tokio::test]
180 async fn postseason_linescore() {
181 let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
182 let postseason = season.postseason.expect("Expected the MLB to have a postseason");
183 let games = ScheduleRequest::<()>::builder().date_range(postseason).sport_id(SportId::MLB).build_and_get().await.unwrap();
184 let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type.is_postseason()).map(|game| game.game_id).collect::<Vec<_>>();
185 let mut has_errors = false;
186 for game in games {
187 if let Err(e) = LinescoreRequest::builder().id(game).build_and_get().await {
188 dbg!(e);
189 has_errors = true;
190 }
191 }
192 assert!(!has_errors, "Has errors.");
193 }
194
195 #[cfg_attr(not(feature = "_heavy_tests"), ignore)]
196 #[tokio::test]
197 async fn regular_season_linescore() {
198 let [season]: [Season; 1] = SeasonsRequest::builder().season(TEST_YEAR).sport_id(SportId::MLB).build_and_get().await.unwrap().seasons.try_into().unwrap();
199 let regular_season = season.regular_season;
200 let games = ScheduleRequest::<()>::builder().date_range(regular_season).sport_id(SportId::MLB).build_and_get().await.unwrap();
201 let games = games.dates.into_iter().flat_map(|date| date.games).filter(|game| game.game_type == GameType::RegularSeason).collect::<Vec<_>>();
202 let mut has_errors = false;
203 for game in games {
204 if let Err(e) = LinescoreRequest::builder().id(game.game_id).build_and_get().await {
205 dbg!(e);
206 has_errors = true;
207 }
208 }
209 assert!(!has_errors, "Has errors.");
210 }
211}