1use 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#[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#[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#[derive(Debug, Deserialize, PartialEq, 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#[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(<() as Hydrations>::RequestData::default())
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#[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
158pub trait RosterHydrations: Hydrations {
160 type Person: Debug + DeserializeOwned + PartialEq + Clone;
162}
163
164impl RosterHydrations for () {
165 type Person = NamedPerson;
166}
167
168#[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 let _response = request.get().await.unwrap();
351 }
357}
358