Skip to main content

mlb_api/requests/team/
roster.rs

1//! Returns a list of [`RosterPlayer`]s on a teams roster.
2
3use crate::person::{JerseyNumber, NamedPerson};
4use crate::season::SeasonId;
5use crate::team::TeamId;
6use crate::{Copyright, MLB_API_DATE_FORMAT};
7use bon::Builder;
8use chrono::NaiveDate;
9use serde::Deserialize;
10use std::fmt::{Debug, Display, Formatter};
11use serde::de::DeserializeOwned;
12use serde_with::{serde_as, DefaultOnError};
13use crate::hydrations::Hydrations;
14use crate::meta::NamedPosition;
15use crate::request::RequestURL;
16use crate::meta::RosterType;
17use crate::team::NamedTeam;
18
19/// Returns a [`Vec`] of [`RosterPlayer`]s for a team.
20#[derive(Debug, Deserialize, PartialEq, Clone)]
21#[serde(rename_all = "camelCase", bound = "H: RosterHydrations")]
22pub struct RosterResponse<H: RosterHydrations = ()> {
23    pub copyright: Copyright,
24    #[serde(default)]
25    pub roster: Vec<RosterPlayer<H>>,
26    pub team_id: TeamId,
27    pub roster_type: RosterType,
28}
29
30// A [`NamedPerson`] on a roster, has an assigned position.
31#[serde_as]
32#[derive(Debug, Deserialize, PartialEq, Clone)]
33#[serde(rename_all = "camelCase")]
34pub struct RosterPlayer<H: RosterHydrations = ()> {
35    pub person: H::Person,
36	#[serde(default)]
37	#[serde_as(deserialize_as = "DefaultOnError")]
38    pub jersey_number: Option<JerseyNumber>,
39    pub position: NamedPosition,
40    pub status: RosterStatus,
41    pub parent_team_id: Option<TeamId>,
42}
43
44/// Status on the roster
45#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
46#[serde(try_from = "__RosterStatusStruct")]
47pub enum RosterStatus {
48    Active,
49    FortyMan,
50    Claimed,
51    ReassignedToMinors,
52    Released,
53    MinorLeagueContract,
54    InjuryLeave7Day,
55    InjuryLeave10Day,
56    InjuryLeave15Day,
57    InjuryLeave60Day,
58    Traded,
59    DesignatedForAssignment,
60    FreeAgent,
61    RestrictedList,
62    AssignedToNewTeam,
63    RehabAssignment,
64    NonRosterInvitee,
65    Waived,
66    Deceased,
67    VoluntarilyRetired,
68}
69
70#[derive(Deserialize)]
71#[doc(hidden)]
72struct __RosterStatusStruct {
73    code: String,
74    description: String,
75}
76
77impl TryFrom<__RosterStatusStruct> for RosterStatus {
78    type Error = String;
79
80    fn try_from(value: __RosterStatusStruct) -> Result<Self, Self::Error> {
81        Ok(match &*value.code {
82            "A" => Self::Active,
83            "40M" => Self::FortyMan,
84            "CL" => Self::Claimed,
85            "RM" => Self::ReassignedToMinors,
86            "RL" => Self::Released,
87            "MIN" => Self::MinorLeagueContract,
88            "D7" => Self::InjuryLeave7Day,
89            "D10" => Self::InjuryLeave10Day,
90            "D15" => Self::InjuryLeave15Day,
91            "D60" => Self::InjuryLeave60Day,
92            "TR" => Self::Traded,
93            "DES" => Self::DesignatedForAssignment,
94            "FA" => Self::FreeAgent,
95            "RST" => Self::RestrictedList,
96            "ASG" => Self::AssignedToNewTeam,
97            "RA" => Self::RehabAssignment,
98            "NRI" => Self::NonRosterInvitee,
99            "WA" => Self::Waived,
100            "DEC" => Self::Deceased,
101            "RET" => Self::VoluntarilyRetired,
102            code => return Err(format!("Invalid code '{code}' (desc: {})", value.description)),
103        })
104    }
105}
106
107/// Returns a [`RosterResponse`]
108#[derive(Builder)]
109#[builder(derive(Into))]
110#[allow(unused)]
111pub struct RosterRequest<H: RosterHydrations = ()> {
112    #[builder(into)]
113    team_id: TeamId,
114    #[builder(into)]
115    season: Option<SeasonId>,
116    date: Option<NaiveDate>,
117    #[builder(into, default)]
118    roster_type: RosterType,
119    #[builder(into)]
120    hydrations: H::RequestData,
121}
122
123impl<H: RosterHydrations, S: roster_request_builder::State + roster_request_builder::IsComplete> crate::request::RequestURLBuilderExt for RosterRequestBuilder<H, S> {
124    type Built = RosterRequest<H>;
125}
126
127impl RosterRequest {
128    pub fn for_team(team_id: impl Into<TeamId>) -> RosterRequestBuilder<(), roster_request_builder::SetHydrations<roster_request_builder::SetTeamId>> {
129        Self::builder().team_id(team_id).hydrations(())
130    }
131}
132
133impl<H: RosterHydrations> Display for RosterRequest<H> {
134    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
135        let hydrations = Some(<H as Hydrations>::hydration_text(&self.hydrations)).filter(|s| !s.is_empty());
136        write!(f, "http://statsapi.mlb.com/api/v1/teams/{}/roster{}", self.team_id, gen_params! { "season"?: self.season, "date"?: self.date.as_ref().map(|date| date.format(MLB_API_DATE_FORMAT)), "rosterType": &self.roster_type, "hydrate"?: hydrations })
137    }
138}
139
140impl<H: RosterHydrations> RequestURL for RosterRequest<H> {
141    type Response = RosterResponse<H>;
142}
143
144/// A [`Person`](crate::person::Person)s entry on a roster.
145#[derive(Debug, Deserialize, PartialEq, Clone)]
146#[serde(rename_all = "camelCase")]
147pub struct RosterEntry {
148    pub position: NamedPosition,
149    pub status: RosterStatus,
150    pub team: NamedTeam,
151    pub is_active: bool,
152    pub is_active_forty_man: bool,
153    pub start_date: NaiveDate,
154    pub end_date: Option<NaiveDate>,
155    pub status_date: NaiveDate,
156}
157
158/// A type that is made with [`roster_hydrations!`](crate::roster_hydrations)
159pub trait RosterHydrations: Hydrations {
160    /// [`NamedPerson`] when no hydrations are present and [`Person`](crate::person::Person) when they are.
161    type Person: Debug + DeserializeOwned + PartialEq + Clone;
162}
163
164impl RosterHydrations for () {
165    type Person = NamedPerson;
166}
167
168/// Creates hydrations for a [`RosterRequest`].
169///
170/// ## Roster Hydrations
171/// | Name     | Type                                             |
172/// |----------|--------------------------------------------------|
173/// | `person` | [`person_hydrations!`](crate::person_hydrations) |
174///
175/// ## Examples
176/// ```no_run
177/// person_hydrations! {
178///     pub struct ExamplePersonHydrations {
179///         nicknames
180///     }
181/// }
182///
183/// roster_hydrations! {
184///     pub struct ExampleRosterHydrations {
185///         person: ExamplePersonHydrations
186///     }
187/// }
188///
189/// // alternatively you can inline these hydrations
190/// roster_hydrations! {
191///     pub struct ExampleRosterHydrations {
192///         person: { nicknames }
193///     }
194/// }
195///
196/// let request = RosterRequest::<ExamplePersonHydrations>::builder()
197///     .team_id(141)
198///     .hydrations(ExampleRosterHydrations::builder()
199///         .person(ExamplePersonHydrations::builder()
200///             .build())
201///         .build())
202///     .build();
203/// let response = request.get();
204///
205/// // note that assuming there isn't anything required to be specified, Default can be used on these builders
206/// let request = RosterRequest::<ExamplePersonHydrations>::builder()
207///     .team_id(141)
208///     .hydrations(ExampleRosterHydrationsRequestData::default())
209///     .build();
210/// ```
211#[macro_export]
212macro_rules! roster_hydrations {
213    (@ inline_structs [person: { $($contents:tt)* } $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
214        ::pastey::paste! {
215            $crate::person_hydrations! {
216                $vis struct [<$name InlinePersonHydrations>] {
217                    $($contents)*
218                }
219            }
220
221            $crate::roster_hydrations! { @ inline_structs [$($($rest)*)?]
222                $vis struct $name {
223                    $($field_tt)*
224                    person: [<$name InlinePersonHydrations>],
225                }
226            }
227        }
228    };
229    (@ inline_structs [$marker:ident : { $($contents:tt)* } $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
230        ::core::compile_error!("Found unknown inline struct");
231    };
232    (@ inline_structs [$marker:ident $(: $value:ty)? $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
233        ::pastey::paste! {
234            $crate::roster_hydrations! { @ inline_structs [$($($rest)*)?]
235                $vis struct $name {
236                    $($field_tt)*
237                    $marker $(: $value)?,
238                }
239            }
240        }
241    };
242    (@ inline_structs [$(,)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
243        ::pastey::paste! {
244            $crate::roster_hydrations! { @ actual
245                $vis struct $name {
246                    $($field_tt)*
247                }
248            }
249        }
250    };
251    ($vis:vis struct $name:ident {
252        $($contents:tt)*
253    }) => {
254        $crate::roster_hydrations! { @ inline_structs [$($contents)*] $vis struct $name {} }
255    };
256    (@ person_type $hydrations:path) => {
257        $crate::person::Person<$hydrations>
258    };
259    (@ person_type) => {
260        $crate::person::NamedPerson
261    };
262    (@ actual $vis:vis struct $name:ident {
263        $(person: $person:ty ,)?
264    }) => {
265        ::pastey::paste! {
266            #[derive(::core::fmt::Debug, ::serde::Deserialize, ::core::cmp::PartialEq, ::core::clone::Clone)]
267            #[serde(rename_all = "camelCase")]
268            $vis struct $name {}
269
270            impl $crate::team::roster::RosterHydrations for $name {
271                type Person = $crate::roster_hydrations!(@ person_type $($person)?);
272            }
273
274            impl $crate::hydrations::Hydrations for $name {
275                type RequestData = [<$name RequestData>];
276
277                #[allow(unused_variables, reason = "branches")]
278                fn hydration_text(_data: &Self::RequestData) -> ::std::borrow::Cow<'static, str> {
279                    let text = ::std::borrow::Cow::Borrowed("");
280
281                    $(
282                    let text = ::std::borrow::Cow::Owned(::std::format!("person({})", <$person as $crate::hydrations::Hydrations>::hydration_text(&_data.person)));
283                    )?
284
285                    text
286                }
287            }
288
289            #[derive(::bon::Builder)]
290            #[builder(derive(Into))]
291            $vis struct [<$name RequestData>] {
292                $(#[builder(into)] person: <$person as $crate::hydrations::Hydrations>::RequestData,)?
293            }
294
295            impl $name {
296				#[allow(unused)]
297				pub fn builder() -> [<$name RequestDataBuilder>] {
298					[<$name RequestData>]::builder()
299				}
300			}
301
302            impl ::core::default::Default for [<$name RequestData>]
303			where
304				$(for<'no_rfc_2056> <$person as $crate::hydrations::Hydrations>::RequestData: ::core::default::Default,)?
305			{
306				fn default() -> Self {
307					Self {
308						$(person: <<$person as $crate::hydrations::Hydrations>::RequestData as ::core::default::Default>::default(),)?
309					}
310				}
311			}
312        }
313    };
314}
315
316#[cfg(test)]
317mod tests {
318	use crate::meta::MetaRequest;
319    use crate::request::{RequestURL, RequestURLBuilderExt};
320    use crate::meta::RosterType;
321    use crate::team::roster::RosterRequest;
322	use crate::team::TeamsRequest;
323    use crate::TEST_YEAR;
324
325    #[tokio::test]
326    #[cfg_attr(not(feature = "_heavy_tests"), ignore)]
327    async fn test_this_year_all_mlb_teams_all_roster_types() {
328        let season = TEST_YEAR;
329        let teams = TeamsRequest::mlb_teams().season(season).build_and_get().await.unwrap().teams;
330        let roster_types = MetaRequest::<RosterType>::new().get().await.unwrap().entries;
331        for team in teams {
332            for roster_type in &roster_types {
333                let _ = RosterRequest::<()>::for_team(team.id).season(season).roster_type(*roster_type).build_and_get().await.unwrap();
334            }
335        }
336    }
337
338    #[tokio::test]
339    async fn hydrations_test() {
340        roster_hydrations! {
341            pub struct TestHydrations {
342                person: {
343                    nicknames
344                },
345            }
346        }
347
348        let request = RosterRequest::<TestHydrations>::builder().hydrations(TestHydrationsRequestData::default()).team_id(141).season(TEST_YEAR).roster_type(RosterType::default()).build();
349        // println!("Request: {request}");
350        let _response = request.get().await.unwrap();
351        /*for entry in _response.roster {
352            if let Person::Ballplayer(ballplayer) = entry.person {
353                println!("Name: {}, Nicknames: {:?}", ballplayer.full_name, ballplayer.extras.nicknames);
354            }
355        }*/
356    }
357}
358