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