Skip to main content

mlb_api/requests/person/
mod.rs

1//! A description of a person in baseball.
2//!
3//! Person works through multi-level definition.
4//!
5//! 1. [`PersonId`], which deserializes a:
6//! ```json
7//! "person": {
8//!     "id": 660271,
9//!     "link": "/api/v1/people/660271"
10//! }
11//! ```
12//! 2. [`NamedPerson`], which deserializes a:
13//! ```json
14//! "person": {
15//!     "id": 660271,
16//!     "link": "/api/v1/people/660271",
17//!     "fullName": "Shohei Ohtani"
18//! }
19//! ```
20//! 3. [`Person`], which deserializes a lot of extra fields, see <http://statsapi.mlb.com/api/v1/people/660271>.
21//! Technically, [`Person`] is actually an enum which separates fields supplied for [`Ballplayer`]s (handedness, draft year, etc.), and fields available to people like coaches and umpires (such as last name, age, etc.) [`RegularPerson`].
22//!
23//! This module also contains [`person_hydrations`](crate::person_hydrations), which are used to get additional data about people when making requests.
24
25pub mod free_agents;
26pub mod stats;
27pub mod players;
28
29use crate::cache::Requestable;
30use crate::draft::School;
31use crate::hydrations::Hydrations;
32use crate::{Copyright, Gender, Handedness, HeightMeasurement};
33use crate::request::RequestURL;
34use bon::Builder;
35use chrono::{Local, NaiveDate};
36use derive_more::{Deref, DerefMut, Display, From};
37use serde::de::Error;
38use serde::{Deserialize, Deserializer};
39use serde_with::{serde_as, DefaultOnError};
40use std::fmt::{Debug, Display, Formatter};
41use std::hash::{Hash, Hasher};
42use std::ops::{Deref, DerefMut};
43use crate::meta::NamedPosition;
44use crate::team::NamedTeam;
45
46#[cfg(feature = "cache")]
47use crate::{rwlock_const_new, RwLock, cache::CacheTable};
48
49/// Response containing a list of people
50#[derive(Debug, Deserialize, PartialEq, Clone)]
51#[serde(rename_all = "camelCase")]
52#[serde(bound = "H: PersonHydrations")]
53pub struct PeopleResponse<H: PersonHydrations> {
54	pub copyright: Copyright,
55	#[serde(default)]
56	pub people: Vec<Person<H>>,
57}
58
59/// A baseball player.
60///
61/// [`Deref`]s to [`RegularPerson`]
62#[derive(Debug, Deref, DerefMut, Deserialize, Clone)]
63#[serde(rename_all = "camelCase")]
64#[serde(bound = "H: PersonHydrations")]
65pub struct Ballplayer<H: PersonHydrations> {
66	#[serde(deserialize_with = "crate::try_from_str")]
67	#[serde(default)]
68	pub primary_number: Option<u8>,
69	#[serde(flatten)]
70	pub birth_data: BirthData,
71	#[serde(flatten)]
72	pub body_measurements: BodyMeasurements,
73	pub gender: Gender,
74	pub draft_year: Option<u16>,
75	#[serde(rename = "mlbDebutDate")]
76	pub mlb_debut: Option<NaiveDate>,
77	pub bat_side: Handedness,
78	pub pitch_hand: Handedness,
79	#[serde(flatten)]
80	pub strike_zone: StrikeZoneMeasurements,
81	#[serde(rename = "nickName")]
82	pub nickname: Option<String>,
83
84	#[deref]
85	#[deref_mut]
86	#[serde(flatten)]
87	inner: Box<RegularPerson<H>>,
88}
89
90/// A regular person; detailed-name stuff.
91///
92/// [`Deref`]s to [`NamedPerson`]
93#[derive(Debug, Deserialize, Deref, DerefMut, Clone)]
94#[serde(rename_all = "camelCase")]
95#[serde(bound = "H: PersonHydrations")]
96pub struct RegularPerson<H: PersonHydrations> {
97	pub primary_position: NamedPosition,
98	// '? ? Brown' in 1920 does not have a first name or a middle name, rather than dealing with Option and making everyone hate this API, the better approach is an empty String.
99	#[serde(default)]
100	pub first_name: String,
101	#[serde(rename = "nameSuffix")]
102	pub suffix: Option<String>,
103	#[serde(default)] // this is how their API does it, so I'll copy that.
104	pub middle_name: String,
105	#[serde(default)]
106	pub last_name: String,
107	#[serde(default)]
108	#[serde(rename = "useName")]
109	pub use_first_name: String,
110	#[serde(default)]
111	pub use_last_name: String,
112	#[serde(default)]
113	pub boxscore_name: String,
114
115	pub is_player: bool,
116	#[serde(default)]
117	pub is_verified: bool,
118	pub active: bool,
119
120	#[deref]
121	#[deref_mut]
122	#[serde(flatten)]
123	inner: NamedPerson,
124
125	#[serde(flatten)]
126	pub extras: H,
127}
128
129impl<H: PersonHydrations> RegularPerson<H> {
130	#[must_use]
131	pub fn name_first_last(&self) -> String {
132		format!("{0} {1}", self.use_first_name, self.use_last_name)
133	}
134
135	#[must_use]
136	pub fn name_last_first(&self) -> String {
137		format!("{1}, {0}", self.use_first_name, self.use_last_name)
138	}
139
140	#[must_use]
141	pub fn name_last_first_initial(&self) -> String {
142		self.use_first_name.chars().next().map_or_else(|| self.use_last_name.clone(), |char| format!("{1}, {0}", char, self.use_last_name))
143	}
144
145	#[must_use]
146	pub fn name_first_initial_last(&self) -> String {
147		self.use_first_name.chars().next().map_or_else(|| self.use_last_name.clone(), |char| format!("{0} {1}", char, self.use_last_name))
148	}
149
150	#[must_use]
151	pub fn name_fml(&self) -> String {
152		format!("{0} {1} {2}", self.use_first_name, self.middle_name, self.use_last_name)
153	}
154
155	#[must_use]
156	pub fn name_lfm(&self) -> String {
157		format!("{2}, {0} {1}", self.use_first_name, self.middle_name, self.use_last_name)
158	}
159}
160
161/// A person with a name.
162///
163/// [`Deref`]s to [`PersonId`]
164#[derive(Debug, Deserialize, Clone, Eq)]
165#[serde(rename_all = "camelCase")]
166pub struct NamedPerson {
167	pub full_name: String,
168
169	#[serde(flatten)]
170	pub id: PersonId,
171}
172
173impl Hash for NamedPerson {
174	fn hash<H: Hasher>(&self, state: &mut H) {
175		self.id.hash(state);
176	}
177}
178
179impl NamedPerson {
180	#[must_use]
181	pub(crate) fn unknown_person() -> Self {
182		Self {
183			full_name: "null".to_owned(),
184			id: PersonId::new(0),
185		}
186	}
187
188	#[must_use]
189	pub fn is_unknown(&self) -> bool {
190		*self.id == 0
191	}
192}
193
194id!(#[doc = "A [`u32`] that represents a person."] PersonId { id: u32 });
195
196/// A complete person response for a hydrated request. Ballplayers have more fields.
197#[derive(Debug, Deserialize, Clone, From)]
198#[serde(untagged)]
199#[serde(bound = "H: PersonHydrations")]
200pub enum Person<H: PersonHydrations = ()> {
201	Ballplayer(Ballplayer<H>),
202	Regular(RegularPerson<H>),
203}
204
205impl<H: PersonHydrations> Person<H> {
206	#[must_use]
207	pub const fn as_ballplayer(&self) -> Option<&Ballplayer<H>> {
208		match self {
209			Self::Ballplayer(x) => Some(x),
210			Self::Regular(_) => None,
211		}
212	}
213}
214
215impl<H: PersonHydrations> Person<H> {
216	#[must_use]
217	pub const fn as_ballplayer_mut(&mut self) -> Option<&mut Ballplayer<H>> {
218		match self {
219			Self::Ballplayer(x) => Some(x),
220			Self::Regular(_) => None,
221		}
222	}
223}
224
225impl<H: PersonHydrations> Person<H> {
226	#[must_use]
227	pub fn into_ballplayer(self) -> Option<Ballplayer<H>> {
228		match self {
229			Self::Ballplayer(x) => Some(x),
230			Self::Regular(_) => None,
231		}
232	}
233}
234
235impl<H: PersonHydrations> Deref for Person<H> {
236	type Target = RegularPerson<H>;
237
238	fn deref(&self) -> &Self::Target {
239		match self {
240			Self::Ballplayer(x) => x,
241			Self::Regular(x) => x,
242		}
243	}
244}
245
246impl<H: PersonHydrations> DerefMut for Person<H> {
247	fn deref_mut(&mut self) -> &mut Self::Target {
248		match self {
249			Self::Ballplayer(x) => x,
250			Self::Regular(x) => x,
251		}
252	}
253}
254
255impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<Person<H2>> for Person<H1> {
256	fn eq(&self, other: &Person<H2>) -> bool {
257		self.id == other.id
258	}
259}
260
261impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<Ballplayer<H2>> for Ballplayer<H1> {
262	fn eq(&self, other: &Ballplayer<H2>) -> bool {
263		self.id == other.id
264	}
265}
266
267impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<RegularPerson<H2>> for RegularPerson<H1> {
268	fn eq(&self, other: &RegularPerson<H2>) -> bool {
269		self.id == other.id
270	}
271}
272
273id_only_eq_impl!(NamedPerson, id);
274
275/// Returns a [`PeopleResponse`].
276#[derive(Builder)]
277#[builder(derive(Into))]
278pub struct PersonRequest<H: PersonHydrations> {
279	#[builder(into)]
280	id: PersonId,
281
282	#[builder(into)]
283	hydrations: H::RequestData,
284}
285
286impl PersonRequest<()> {
287	pub fn for_id(id: impl Into<PersonId>) -> PersonRequestBuilder<(), person_request_builder::SetHydrations<person_request_builder::SetId>> {
288		Self::builder().id(id).hydrations(())
289	}
290}
291
292impl<H: PersonHydrations, S: person_request_builder::State + person_request_builder::IsComplete> crate::request::RequestURLBuilderExt for PersonRequestBuilder<H, S> {
293	type Built = PersonRequest<H>;
294}
295
296impl<H: PersonHydrations> Display for PersonRequest<H> {
297	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
298		let hydration_text = H::hydration_text(&self.hydrations);
299		if hydration_text.is_empty() {
300			write!(f, "http://statsapi.mlb.com/api/v1/people/{}", self.id)
301		} else {
302			write!(f, "http://statsapi.mlb.com/api/v1/people/{}?hydrate={hydration_text}", self.id)
303		}
304	}
305}
306
307impl<H: PersonHydrations> RequestURL for PersonRequest<H> {
308	type Response = PeopleResponse<H>;
309}
310
311/// The number on the back of a jersey, useful for radix sorts maybe??
312#[repr(transparent)]
313#[derive(Debug, Deref, Display, PartialEq, Eq, Copy, Clone, Hash, From)]
314pub struct JerseyNumber(u8);
315
316impl<'de> Deserialize<'de> for JerseyNumber {
317	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
318	where
319		D: Deserializer<'de>,
320	{
321		String::deserialize(deserializer)?.parse::<u8>().map(JerseyNumber).map_err(D::Error::custom)
322	}
323}
324
325/// Data regarding birthplace.
326#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
327#[serde(rename_all = "camelCase")]
328pub struct BirthData {
329	pub birth_date: NaiveDate,
330	pub birth_city: String,
331	#[serde(rename = "birthStateProvince")]
332	pub birth_state_or_province: Option<String>,
333	pub birth_country: String,
334}
335
336impl BirthData {
337	#[must_use]
338	pub fn current_age(&self) -> u16 {
339		Local::now().naive_local().date().years_since(self.birth_date).and_then(|x| u16::try_from(x).ok()).unwrap_or(0)
340	}
341}
342
343/// Height and weight
344#[derive(Debug, Deserialize, PartialEq, Clone)]
345#[serde(rename_all = "camelCase")]
346pub struct BodyMeasurements {
347	pub height: HeightMeasurement,
348	pub weight: u16,
349}
350
351/// Strike zone dimensions, measured in feet from the ground.
352#[derive(Debug, Deserialize, PartialEq, Clone)]
353#[serde(rename_all = "camelCase")]
354pub struct StrikeZoneMeasurements {
355	pub strike_zone_top: f64,
356	pub strike_zone_bottom: f64,
357}
358
359/// Data regarding preferred team, likely for showcasing the player with a certain look regardless of the time.
360#[serde_as]
361#[derive(Debug, Deserialize, PartialEq, Clone)]
362#[serde(rename_all = "camelCase")]
363pub struct PreferredTeamData {
364	#[serde(default)]
365	#[serde_as(deserialize_as = "DefaultOnError")]
366	pub jersey_number: Option<JerseyNumber>,
367	pub position: NamedPosition,
368	pub team: NamedTeam,
369}
370
371/// Relative to the ballplayer, father, son, etc.
372#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
373#[serde(rename_all = "camelCase")]
374pub struct Relative {
375	pub has_stats: bool,
376	pub relation: String,
377	#[serde(flatten)]
378	pub person: NamedPerson,
379}
380
381/// Schools the ballplayer went to.
382#[derive(Debug, Deserialize, PartialEq, Clone, Default)]
383pub struct Education {
384	#[serde(default)]
385	pub highschools: Vec<School>,
386	#[serde(default)]
387	pub colleges: Vec<School>,
388}
389
390/// A type that is made with [`person_hydrations!`](crate::person_hydrations)
391pub trait PersonHydrations: Hydrations {}
392
393impl PersonHydrations for () {}
394
395/// Creates hydrations for a person
396/// 
397/// ## Examples
398///```no_run
399/// person_hydrations! {
400///     pub struct TestHydrations {  ->  pub struct TestHydrations {
401///         awards,                     ->      awards: Vec<Award>,
402///         social,                     ->      social: HashMap<String, Vec<String>>,
403///         stats: MyStats,             ->      stats: MyStats,
404///     }                               ->  }
405/// }
406///
407/// person_hydrations! {
408///     pub struct TestHydrations {        ->  pub struct TestHydrations {
409///         stats: { [Season] + [Hitting] },  ->      stats: TestHydrationsInlineStats,
410///     }                                     ->  }
411/// }
412///
413/// let request = PersonRequest::<TestHydrations>::builder()
414///     .id(660_271)
415///     .hydrations(TestHydrations::builder())
416///     .build();
417///
418/// let response = request.get().await.unwrap();
419///```
420///
421/// ## Person Hydrations
422/// <u>Note: Fields must appear in exactly this order (or be omitted)</u>
423///
424/// | Name             | Type                             |
425/// |------------------|----------------------------------|
426/// | `awards`         | [`Vec<Award>`]                   |
427/// | `current_team`   | [`Team`]                         |
428/// | `depth_charts`   | [`Vec<RosterEntry>`]             |
429/// | `draft`          | [`Vec<DraftPick>`]               |
430/// | `education`      | [`Education`]                    |
431/// | `jobs`           | [`Vec<EmployedPerson>`]          |
432/// | `nicknames`      | [`Vec<String>`]                  |
433/// | `preferred_team` | [`Team`]                         |
434/// | `relatives`      | [`Vec<Relative>`]                |
435/// | `roster_entries` | [`Vec<RosterEntry>`]             |
436/// | `transactions`   | [`Vec<Transaction>`]             |
437/// | `social`         | [`HashMap<String, Vec<String>>`] |
438/// | `stats`          | [`stats_hydrations!`]            |
439/// | `external_references` | [`Vec<ExternalReference>`]  |
440///
441/// [`Vec<Award>`]: crate::awards::Award
442/// [`Team`]: crate::team::Team
443/// [`Vec<RosterEntry>`]: crate::team::roster::RosterEntry
444/// [`Vec<DraftPick>`]: crate::draft::DraftPick
445/// [`Education`]: Education
446/// [`Vec<EmployedPerson>`]: crate::jobs::EmployedPerson
447/// [`Vec<String>`]: String
448/// [`Team`]: crate::team::Team
449/// [`Vec<Relative>`]: Relative
450/// [`Vec<RosterEntry>`]: crate::team::rosterRosterEntry
451/// [`Vec<Transaction>`]: crate::transactions::Transaction
452/// [`HashMap<String, Vec<String>>`]: std::collections::HashMap
453/// [`stats_hydrations!`]: crate::stats_hydrations
454/// [`Vec<ExternalReference>`]: crate::types::ExternalReference
455#[macro_export]
456macro_rules! person_hydrations {
457	(@ inline_structs [stats: { $($contents:tt)* } $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
458        ::pastey::paste! {
459            $crate::stats_hydrations! {
460                $vis struct [<$name InlineStats>] {
461                    $($contents)*
462                }
463            }
464
465            $crate::person_hydrations! { @ inline_structs [$($($rest)*)?]
466                $vis struct $name {
467                    $($field_tt)*
468                    stats: [<$name InlineStats>],
469                }
470            }
471        }
472    };
473    (@ inline_structs [$marker:ident : { $($contents:tt)* } $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
474        ::core::compile_error!("Found unknown inline struct");
475    };
476    (@ inline_structs [$marker:ident $(: $value:ty)? $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
477        ::pastey::paste! {
478            $crate::person_hydrations! { @ inline_structs [$($($rest)*)?]
479                $vis struct $name {
480                    $($field_tt)*
481                    $marker $(: $value)?,
482                }
483            }
484        }
485    };
486    (@ inline_structs [$(,)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
487        ::pastey::paste! {
488            $crate::person_hydrations! { @ actual
489                $vis struct $name {
490                    $($field_tt)*
491                }
492            }
493        }
494    };
495    (@ actual
496		$vis:vis struct $name:ident {
497			$(awards $awards_comma:tt)?
498			$(current_team $current_team_comma:tt)?
499			$(depth_charts $depth_charts_comma:tt)?
500			$(draft $draft_comma:tt)?
501			$(education $education_comma:tt)?
502			$(jobs $jobs_comma:tt)?
503			$(nicknames $nicknames_comma:tt)?
504			$(preferred_team $preferred_team_comma:tt)?
505			$(relatives $relatives_comma:tt)?
506			$(roster_entries $roster_entries_comma:tt)?
507			$(transactions $transactions_comma:tt)?
508			$(social $social_comma:tt)?
509			$(stats: $stats:ty ,)?
510			$(external_references $external_references_comma:tt)?
511		}
512    ) => {
513		::pastey::paste! {
514			#[derive(::core::fmt::Debug, ::serde::Deserialize, ::core::cmp::PartialEq, ::core::clone::Clone)]
515			#[serde(rename_all = "camelCase")]
516			$vis struct $name {
517				$(#[serde(default)] pub awards: ::std::vec::Vec<$crate::awards::Award> $awards_comma)?
518				$(pub current_team: ::core::option::Option<$crate::team::NamedTeam> $current_team_comma)?
519				$(#[serde(default)] pub depth_charts: ::std::vec::Vec<$crate::team::roster::RosterEntry> $depth_charts_comma)?
520				$(#[serde(default, rename = "drafts")] pub draft: ::std::vec::Vec<$crate::draft::DraftPick> $draft_comma)?
521				$(#[serde(default)] pub education: $crate::person::Education $education_comma)?
522				$(#[serde(default, rename = "jobEntries")] pub jobs: ::std::vec::Vec<$crate::jobs::EmployedPerson> $jobs_comma)?
523				$(#[serde(default)] pub nicknames: ::std::vec::Vec<String> $nicknames_comma)?
524				$(pub preferred_team: ::core::option::Option<$crate::person::PreferredTeamData> $preferred_team_comma)?
525				$(#[serde(default)] pub relatives: ::std::vec::Vec<$crate::person::Relative> $relatives_comma)?
526				$(#[serde(default)] pub roster_entries: ::std::vec::Vec<$crate::team::roster::RosterEntry> $roster_entries_comma)?
527				$(#[serde(default)] pub transactions: ::std::vec::Vec<$crate::transactions::Transaction> $transactions_comma)?
528				$(#[serde(flatten)] pub stats: $stats ,)?
529				$(#[serde(default, rename = "social")] pub socials: ::std::collections::HashMap<String, Vec<String>> $social_comma)?
530				$(#[serde(default, rename = "xrefIds")] pub external_references: ::std::vec::Vec<$crate::types::ExternalReference> $external_references_comma)?
531			}
532
533			impl $crate::person::PersonHydrations for $name {}
534
535			impl $crate::hydrations::Hydrations for $name {
536				type RequestData = [<$name RequestData>];
537
538				fn hydration_text(_data: &Self::RequestData) -> ::std::borrow::Cow<'static, str> {
539					let text = ::std::borrow::Cow::Borrowed(::std::concat!(
540						$("awards," $awards_comma)?
541						$("currentTeam," $current_team_comma)?
542						$("depthCharts," $depth_charts_comma)?
543						$("draft," $draft_comma)?
544						$("education," $education_comma)?
545						$("jobs," $jobs_comma)?
546						$("nicknames," $nicknames_comma)?
547						$("preferredTeam," $preferred_team_comma)?
548						$("relatives," $relatives_comma)?
549						$("rosterEntries," $roster_entries_comma)?
550						$("transactions," $transactions_comma)?
551						$("social," $social_comma)?
552						$("xrefId," $external_references_comma)?
553					));
554
555					$(
556					let text = ::std::borrow::Cow::Owned(::std::format!("{text}stats({}),", <$stats as $crate::hydrations::Hydrations>::hydration_text(&_data.stats)));
557					)?
558
559					text
560				}
561			}
562
563			#[derive(::bon::Builder)]
564			#[builder(derive(Into))]
565			$vis struct [<$name RequestData>] {
566				$(#[builder(into)] stats: <$stats as $crate::hydrations::Hydrations>::RequestData,)?
567			}
568
569			impl $name {
570				#[allow(unused, reason = "potentially unused if the builder is Default")]
571				pub fn builder() -> [<$name RequestDataBuilder>] {
572					[<$name RequestData>]::builder()
573				}
574			}
575
576			impl ::core::default::Default for [<$name RequestData>]
577			where
578				$(for<'no_rfc_2056> <$stats as $crate::hydrations::Hydrations>::RequestData: ::core::default::Default,)?
579			{
580				fn default() -> Self {
581					Self {
582						$(stats: <<$stats as $crate::hydrations::Hydrations>::RequestData as ::core::default::Default>::default(),)?
583					}
584				}
585			}
586		}
587    };
588	($vis:vis struct $name:ident {
589		$($tt:tt)*
590	}) => {
591		$crate::person_hydrations! { @ inline_structs [$($tt)*] $vis struct $name {} }
592	};
593}
594
595#[cfg(feature = "cache")]
596static CACHE: RwLock<CacheTable<Person<()>>> = rwlock_const_new(CacheTable::new());
597
598impl Requestable for Person<()> {
599	type Identifier = PersonId;
600	type URL = PersonRequest<()>;
601
602	fn id(&self) -> &Self::Identifier {
603		&self.id
604	}
605
606	fn url_for_id(id: &Self::Identifier) -> Self::URL {
607		PersonRequest::for_id(*id).build()
608	}
609
610	fn get_entries(response: <Self::URL as RequestURL>::Response) -> impl IntoIterator<Item = Self>
611	where
612		Self: Sized,
613	{
614		response.people
615	}
616
617	#[cfg(feature = "cache")]
618	fn get_cache_table() -> &'static RwLock<CacheTable<Self>>
619	where
620		Self: Sized,
621	{
622		&CACHE
623	}
624}
625
626entrypoint!(PersonId => Person);
627entrypoint!(NamedPerson.id => Person);
628entrypoint!(for < H > RegularPerson < H > . id => Person < > where H: PersonHydrations);
629entrypoint!(for < H > Ballplayer < H > . id => Person < > where H: PersonHydrations);
630
631#[cfg(test)]
632mod tests {
633	use crate::person::players::PlayersRequest;
634	use crate::request::RequestURLBuilderExt;
635	use crate::sport::SportId;
636	use super::*;
637	use crate::TEST_YEAR;
638
639	#[tokio::test]
640	async fn no_hydrations() {
641		person_hydrations! {
642			pub struct EmptyHydrations {}
643		}
644
645		let _ = PersonRequest::<()>::for_id(665_489).build_and_get().await.unwrap();
646		let _ = PersonRequest::<EmptyHydrations>::builder().id(665_489).hydrations(EmptyHydrationsRequestData::default()).build_and_get().await.unwrap();
647	}
648
649	#[tokio::test]
650	async fn all_but_stats_hydrations() {
651		person_hydrations! {
652			pub struct AllButStatHydrations {
653				awards,
654				current_team,
655				depth_charts,
656				draft,
657				education,
658				jobs,
659				nicknames,
660				preferred_team,
661				relatives,
662				roster_entries,
663				transactions,
664				social,
665				external_references
666			}
667		}
668
669		let _person = PersonRequest::<AllButStatHydrations>::builder().hydrations(AllButStatHydrationsRequestData::default()).id(665_489).build_and_get().await.unwrap().people.into_iter().next().unwrap();
670	}
671
672	#[rustfmt::skip]
673	#[tokio::test]
674	async fn only_stats_hydrations() {
675		person_hydrations! {
676			pub struct StatOnlyHydrations {
677				stats: { [Sabermetrics] + [Pitching] },
678			}
679		}
680
681		let player = PlayersRequest::<()>::for_sport(SportId::MLB)
682			.season(TEST_YEAR)
683			.build_and_get()
684			.await
685			.unwrap()
686			.people
687			.into_iter()
688			.find(|player| player.full_name == "Kevin Gausman")
689			.unwrap();
690
691		let request = PersonRequest::<StatOnlyHydrations>::builder()
692			.id(player.id)
693			.hydrations(StatOnlyHydrations::builder()
694				.stats(StatOnlyHydrationsInlineStats::builder()
695					.season(2023)
696					// .situation(SituationCodeId::new("h"))
697				)
698			).build();
699		println!("{request}");
700		let player = request.get()
701			.await
702			.unwrap()
703			.people
704			.into_iter()
705			.next()
706			.unwrap();
707
708		dbg!(&player.extras.stats);
709	}
710}