Skip to main content

mlb_api/requests/game/
linescore.rs

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/// An inning by inning record of the game's scoring.
16/// 
17/// This is pretty much a 1:1 correlation of the:
18/// ```
19///     | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11|| R | H | E |
20/// LAD | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 || 5 | 11| 0 |
21/// TOR | 0 | 0 | 3 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 || 4 | 14| 0 |
22/// ````
23/// You're used to seeing.
24#[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/// A record of [`RHE`] from both teams in a single inning.
59#[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/// Current offense in the linescore
74#[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    /// Index of the current player in the batting order
84    #[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/// Current defense in the linescore, note that it also contains their offense too.
117#[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/// Returns a [`Linescore`]
143#[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}