1pub 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::{Deserialize, Deserializer};
38use serde::de::Error;
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#[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#[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 pub inner: Box<RegularPerson<H>>,
88}
89
90#[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 #[serde(default)]
100 pub first_name: String,
101 #[serde(rename = "nameSuffix")]
102 pub suffix: Option<String>,
103 #[serde(default)] 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 #[serde(default)]
116 pub is_player: bool,
117 #[serde(default)]
118 pub is_verified: bool,
119 #[serde(default)]
120 pub active: bool,
121
122 #[deref]
123 #[deref_mut]
124 #[serde(flatten)]
125 pub inner: NamedPerson,
126
127 #[serde(flatten)]
128 pub extras: H,
129}
130
131impl<H: PersonHydrations> RegularPerson<H> {
132 #[must_use]
133 pub fn name_first_last(&self) -> String {
134 format!("{0} {1}", self.use_first_name, self.use_last_name)
135 }
136
137 #[must_use]
138 pub fn name_last_first(&self) -> String {
139 format!("{1}, {0}", self.use_first_name, self.use_last_name)
140 }
141
142 #[must_use]
143 pub fn name_last_first_initial(&self) -> String {
144 self.use_first_name.chars().next().map_or_else(|| self.use_last_name.clone(), |char| format!("{1}, {0}", char, self.use_last_name))
145 }
146
147 #[must_use]
148 pub fn name_first_initial_last(&self) -> String {
149 self.use_first_name.chars().next().map_or_else(|| self.use_last_name.clone(), |char| format!("{0} {1}", char, self.use_last_name))
150 }
151
152 #[must_use]
153 pub fn name_fml(&self) -> String {
154 format!("{0} {1} {2}", self.use_first_name, self.middle_name, self.use_last_name)
155 }
156
157 #[must_use]
158 pub fn name_lfm(&self) -> String {
159 format!("{2}, {0} {1}", self.use_first_name, self.middle_name, self.use_last_name)
160 }
161}
162
163#[derive(Debug, Deserialize, Clone, Eq)]
167#[serde(rename_all = "camelCase")]
168pub struct NamedPerson {
169 pub full_name: String,
170
171 #[serde(flatten)]
172 pub id: PersonId,
173}
174
175impl Hash for NamedPerson {
176 fn hash<H: Hasher>(&self, state: &mut H) {
177 self.id.hash(state);
178 }
179}
180
181impl NamedPerson {
182 #[must_use]
183 pub(crate) fn unknown_person() -> Self {
184 Self {
185 full_name: "null".to_owned(),
186 id: PersonId::new(0),
187 }
188 }
189
190 #[must_use]
191 pub fn is_unknown(&self) -> bool {
192 *self.id == 0
193 }
194}
195
196id!(#[doc = "A [`u32`] that represents a person."] PersonId { id: u32 });
197
198#[derive(Debug, Clone, From)]
200pub enum Person<H: PersonHydrations = ()> {
201 Ballplayer(Ballplayer<H>),
202 Regular(RegularPerson<H>),
203}
204
205impl<'de, H: PersonHydrations> Deserialize<'de> for Person<H> {
206 #[allow(clippy::too_many_lines, reason = "still easy to understand cause low logic lines")]
207 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
208 where
209 D: Deserializer<'de>
210 {
211 #[serde_as]
212 #[derive(Deserialize)]
213 #[serde(bound = "H2: PersonHydrations")]
214 struct Repr<H2: PersonHydrations> {
215 #[serde(flatten)]
216 regular: RegularPerson<H2>,
217 #[serde_as(deserialize_as = "DefaultOnError")]
218 #[serde(flatten, default)]
219 ballplayer: Option<BallplayerContent>,
220 }
221
222 #[derive(Deserialize)]
223 struct BallplayerContent {
224 #[serde(deserialize_with = "crate::try_from_str")]
225 #[serde(default)]
226 primary_number: Option<u8>,
227 #[serde(flatten)]
228 birth_data: BirthData,
229 #[serde(flatten)]
230 body_measurements: BodyMeasurements,
231 gender: Gender,
232 draft_year: Option<u16>,
233 #[serde(rename = "mlbDebutDate")]
234 mlb_debut: Option<NaiveDate>,
235 bat_side: Handedness,
236 pitch_hand: Handedness,
237 #[serde(flatten)]
238 strike_zone: StrikeZoneMeasurements,
239 #[serde(rename = "nickName")]
240 nickname: Option<String>,
241 }
242
243 let Repr { regular, ballplayer } = Repr::<H>::deserialize(deserializer)?;
244
245 Ok(match ballplayer {
246 Some(BallplayerContent {
247 primary_number,
248 birth_data,
249 body_measurements,
250 gender,
251 draft_year,
252 mlb_debut,
253 bat_side,
254 pitch_hand,
255 strike_zone,
256 nickname,
257 }) => Self::Ballplayer(Ballplayer {
258 primary_number,
259 birth_data,
260 body_measurements,
261 gender,
262 draft_year,
263 mlb_debut,
264 bat_side,
265 pitch_hand,
266 strike_zone,
267 nickname,
268 inner: Box::new(regular),
269 }),
270 None => Self::Regular(regular),
271 })
272 }
273}
274
275impl<H: PersonHydrations> Person<H> {
276 #[must_use]
277 pub const fn as_ballplayer(&self) -> Option<&Ballplayer<H>> {
278 match self {
279 Self::Ballplayer(x) => Some(x),
280 Self::Regular(_) => None,
281 }
282 }
283}
284
285impl<H: PersonHydrations> Person<H> {
286 #[must_use]
287 pub const fn as_ballplayer_mut(&mut self) -> Option<&mut Ballplayer<H>> {
288 match self {
289 Self::Ballplayer(x) => Some(x),
290 Self::Regular(_) => None,
291 }
292 }
293}
294
295impl<H: PersonHydrations> Person<H> {
296 #[must_use]
297 pub fn into_ballplayer(self) -> Option<Ballplayer<H>> {
298 match self {
299 Self::Ballplayer(x) => Some(x),
300 Self::Regular(_) => None,
301 }
302 }
303}
304
305impl<H: PersonHydrations> Deref for Person<H> {
306 type Target = RegularPerson<H>;
307
308 fn deref(&self) -> &Self::Target {
309 match self {
310 Self::Ballplayer(x) => x,
311 Self::Regular(x) => x,
312 }
313 }
314}
315
316impl<H: PersonHydrations> DerefMut for Person<H> {
317 fn deref_mut(&mut self) -> &mut Self::Target {
318 match self {
319 Self::Ballplayer(x) => x,
320 Self::Regular(x) => x,
321 }
322 }
323}
324
325impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<Person<H2>> for Person<H1> {
326 fn eq(&self, other: &Person<H2>) -> bool {
327 self.id == other.id
328 }
329}
330
331impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<Ballplayer<H2>> for Ballplayer<H1> {
332 fn eq(&self, other: &Ballplayer<H2>) -> bool {
333 self.id == other.id
334 }
335}
336
337impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<RegularPerson<H2>> for RegularPerson<H1> {
338 fn eq(&self, other: &RegularPerson<H2>) -> bool {
339 self.id == other.id
340 }
341}
342
343id_only_eq_impl!(NamedPerson, id);
344
345#[derive(Builder)]
347#[builder(derive(Into))]
348pub struct PersonRequest<H: PersonHydrations> {
349 #[builder(into)]
350 id: PersonId,
351
352 #[builder(into)]
353 hydrations: H::RequestData,
354}
355
356impl PersonRequest<()> {
357 pub fn for_id(id: impl Into<PersonId>) -> PersonRequestBuilder<(), person_request_builder::SetHydrations<person_request_builder::SetId>> {
358 Self::builder().id(id).hydrations(())
359 }
360}
361
362impl<H: PersonHydrations, S: person_request_builder::State + person_request_builder::IsComplete> crate::request::RequestURLBuilderExt for PersonRequestBuilder<H, S> {
363 type Built = PersonRequest<H>;
364}
365
366impl<H: PersonHydrations> Display for PersonRequest<H> {
367 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
368 let hydration_text = H::hydration_text(&self.hydrations);
369 if hydration_text.is_empty() {
370 write!(f, "http://statsapi.mlb.com/api/v1/people/{}", self.id)
371 } else {
372 write!(f, "http://statsapi.mlb.com/api/v1/people/{}?hydrate={hydration_text}", self.id)
373 }
374 }
375}
376
377impl<H: PersonHydrations> RequestURL for PersonRequest<H> {
378 type Response = PeopleResponse<H>;
379}
380
381#[repr(transparent)]
383#[derive(Debug, Deref, Display, PartialEq, Eq, Copy, Clone, Hash, From)]
384pub struct JerseyNumber(u8);
385
386impl<'de> Deserialize<'de> for JerseyNumber {
387 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
388 where
389 D: Deserializer<'de>,
390 {
391 String::deserialize(deserializer)?.parse::<u8>().map(JerseyNumber).map_err(D::Error::custom)
392 }
393}
394
395#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
397#[serde(rename_all = "camelCase")]
398pub struct BirthData {
399 pub birth_date: NaiveDate,
400 pub birth_city: String,
401 #[serde(rename = "birthStateProvince")]
402 pub birth_state_or_province: Option<String>,
403 pub birth_country: String,
404}
405
406impl BirthData {
407 #[must_use]
408 pub fn current_age(&self) -> u16 {
409 Local::now().naive_local().date().years_since(self.birth_date).and_then(|x| u16::try_from(x).ok()).unwrap_or(0)
410 }
411}
412
413#[derive(Debug, Deserialize, PartialEq, Clone)]
415#[serde(rename_all = "camelCase")]
416pub struct BodyMeasurements {
417 pub height: HeightMeasurement,
418 pub weight: u16,
419}
420
421#[derive(Debug, Deserialize, PartialEq, Clone)]
423#[serde(rename_all = "camelCase")]
424pub struct StrikeZoneMeasurements {
425 pub strike_zone_top: f64,
426 pub strike_zone_bottom: f64,
427}
428
429#[serde_as]
431#[derive(Debug, Deserialize, PartialEq, Clone)]
432#[serde(rename_all = "camelCase")]
433pub struct PreferredTeamData {
434 #[serde(default)]
435 #[serde_as(deserialize_as = "DefaultOnError")]
436 pub jersey_number: Option<JerseyNumber>,
437 pub position: NamedPosition,
438 pub team: NamedTeam,
439}
440
441#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
443#[serde(rename_all = "camelCase")]
444pub struct Relative {
445 pub has_stats: bool,
446 pub relation: String,
447 #[serde(flatten)]
448 pub person: NamedPerson,
449}
450
451#[derive(Debug, Deserialize, PartialEq, Clone, Default)]
453pub struct Education {
454 #[serde(default)]
455 pub highschools: Vec<School>,
456 #[serde(default)]
457 pub colleges: Vec<School>,
458}
459
460pub trait PersonHydrations: Hydrations {}
462
463impl PersonHydrations for () {}
464
465#[macro_export]
526macro_rules! person_hydrations {
527 (@ inline_structs [stats: { $($contents:tt)* } $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
528 $crate::macro_use::pastey::paste! {
529 $crate::stats_hydrations! {
530 $vis struct [<$name InlineStats>] {
531 $($contents)*
532 }
533 }
534
535 $crate::person_hydrations! { @ inline_structs [$($($rest)*)?]
536 $vis struct $name {
537 $($field_tt)*
538 stats: [<$name InlineStats>],
539 }
540 }
541 }
542 };
543 (@ inline_structs [$marker:ident : { $($contents:tt)* } $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
544 ::core::compile_error!("Found unknown inline struct");
545 };
546 (@ inline_structs [$marker:ident $(: $value:ty)? $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
547 $crate::macro_use::pastey::paste! {
548 $crate::person_hydrations! { @ inline_structs [$($($rest)*)?]
549 $vis struct $name {
550 $($field_tt)*
551 $marker $(: $value)?,
552 }
553 }
554 }
555 };
556 (@ inline_structs [$(,)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
557 $crate::macro_use::pastey::paste! {
558 $crate::person_hydrations! { @ actual
559 $vis struct $name {
560 $($field_tt)*
561 }
562 }
563 }
564 };
565 (@ actual
566 $vis:vis struct $name:ident {
567 $(awards $awards_comma:tt)?
568 $(current_team $current_team_comma:tt)?
569 $(depth_charts $depth_charts_comma:tt)?
570 $(draft $draft_comma:tt)?
571 $(education $education_comma:tt)?
572 $(jobs $jobs_comma:tt)?
573 $(nicknames $nicknames_comma:tt)?
574 $(preferred_team $preferred_team_comma:tt)?
575 $(relatives $relatives_comma:tt)?
576 $(roster_entries $roster_entries_comma:tt)?
577 $(transactions $transactions_comma:tt)?
578 $(social $social_comma:tt)?
579 $(stats: $stats:ty ,)?
580 $(external_references $external_references_comma:tt)?
581 }
582 ) => {
583 $crate::macro_use::pastey::paste! {
584 #[derive(::core::fmt::Debug, $crate::macro_use::serde::Deserialize, ::core::cmp::PartialEq, ::core::clone::Clone)]
585 #[serde(rename_all = "camelCase")]
586 $vis struct $name {
587 $(#[serde(default)] pub awards: ::std::vec::Vec<$crate::awards::Award> $awards_comma)?
588 $(pub current_team: ::core::option::Option<$crate::team::NamedTeam> $current_team_comma)?
589 $(#[serde(default)] pub depth_charts: ::std::vec::Vec<$crate::team::roster::RosterEntry> $depth_charts_comma)?
590 $(#[serde(default, rename = "drafts")] pub draft: ::std::vec::Vec<$crate::draft::DraftPick> $draft_comma)?
591 $(#[serde(default)] pub education: $crate::person::Education $education_comma)?
592 $(#[serde(default, rename = "jobEntries")] pub jobs: ::std::vec::Vec<$crate::jobs::EmployedPerson> $jobs_comma)?
593 $(#[serde(default)] pub nicknames: ::std::vec::Vec<String> $nicknames_comma)?
594 $(pub preferred_team: ::core::option::Option<$crate::person::PreferredTeamData> $preferred_team_comma)?
595 $(#[serde(default)] pub relatives: ::std::vec::Vec<$crate::person::Relative> $relatives_comma)?
596 $(#[serde(default)] pub roster_entries: ::std::vec::Vec<$crate::team::roster::RosterEntry> $roster_entries_comma)?
597 $(#[serde(default)] pub transactions: ::std::vec::Vec<$crate::transactions::Transaction> $transactions_comma)?
598 $(#[serde(flatten)] pub stats: $stats ,)?
599 $(#[serde(default, rename = "social")] pub socials: ::std::collections::HashMap<String, Vec<String>> $social_comma)?
600 $(#[serde(default, rename = "xrefIds")] pub external_references: ::std::vec::Vec<$crate::ExternalReference> $external_references_comma)?
601 }
602
603 impl $crate::person::PersonHydrations for $name {}
604
605 impl $crate::hydrations::Hydrations for $name {
606 type RequestData = [<$name RequestData>];
607
608 fn hydration_text(_data: &Self::RequestData) -> ::std::borrow::Cow<'static, str> {
609 let text = ::std::borrow::Cow::Borrowed(::std::concat!(
610 $("awards," $awards_comma)?
611 $("currentTeam," $current_team_comma)?
612 $("depthCharts," $depth_charts_comma)?
613 $("draft," $draft_comma)?
614 $("education," $education_comma)?
615 $("jobs," $jobs_comma)?
616 $("nicknames," $nicknames_comma)?
617 $("preferredTeam," $preferred_team_comma)?
618 $("relatives," $relatives_comma)?
619 $("rosterEntries," $roster_entries_comma)?
620 $("transactions," $transactions_comma)?
621 $("social," $social_comma)?
622 $("xrefId," $external_references_comma)?
623 ));
624
625 $(
626 let text = ::std::borrow::Cow::Owned(::std::format!("{text}stats({}),", <$stats as $crate::hydrations::Hydrations>::hydration_text(&_data.stats)));
627 )?
628
629 text
630 }
631 }
632
633 #[derive($crate::macro_use::bon::Builder)]
634 #[builder(derive(Into))]
635 $vis struct [<$name RequestData>] {
636 $(#[builder(into)] stats: <$stats as $crate::hydrations::Hydrations>::RequestData,)?
637 }
638
639 impl $name {
640 #[allow(unused, reason = "potentially unused if the builder is Default")]
641 pub fn builder() -> [<$name RequestDataBuilder>] {
642 [<$name RequestData>]::builder()
643 }
644 }
645
646 impl ::core::default::Default for [<$name RequestData>]
647 where
648 $(for<'no_rfc_2056> <$stats as $crate::hydrations::Hydrations>::RequestData: ::core::default::Default,)?
649 {
650 fn default() -> Self {
651 Self {
652 $(stats: <<$stats as $crate::hydrations::Hydrations>::RequestData as ::core::default::Default>::default(),)?
653 }
654 }
655 }
656 }
657 };
658 ($vis:vis struct $name:ident {
659 $($tt:tt)*
660 }) => {
661 $crate::person_hydrations! { @ inline_structs [$($tt)*] $vis struct $name {} }
662 };
663}
664
665#[cfg(feature = "cache")]
666static CACHE: RwLock<CacheTable<Person<()>>> = rwlock_const_new(CacheTable::new());
667
668impl Requestable for Person<()> {
669 type Identifier = PersonId;
670 type URL = PersonRequest<()>;
671
672 fn id(&self) -> &Self::Identifier {
673 &self.id
674 }
675
676 fn url_for_id(id: &Self::Identifier) -> Self::URL {
677 PersonRequest::for_id(*id).build()
678 }
679
680 fn get_entries(response: <Self::URL as RequestURL>::Response) -> impl IntoIterator<Item = Self>
681 where
682 Self: Sized,
683 {
684 response.people
685 }
686
687 #[cfg(feature = "cache")]
688 fn get_cache_table() -> &'static RwLock<CacheTable<Self>>
689 where
690 Self: Sized,
691 {
692 &CACHE
693 }
694}
695
696entrypoint!(PersonId => Person);
697entrypoint!(NamedPerson.id => Person);
698entrypoint!(for < H > RegularPerson < H > . id => Person < > where H: PersonHydrations);
699entrypoint!(for < H > Ballplayer < H > . id => Person < > where H: PersonHydrations);
700
701#[cfg(test)]
702mod tests {
703 use crate::person::players::PlayersRequest;
704 use crate::request::RequestURLBuilderExt;
705 use crate::sport::SportId;
706 use super::*;
707 use crate::TEST_YEAR;
708
709 #[tokio::test]
710 async fn no_hydrations() {
711 person_hydrations! {
712 pub struct EmptyHydrations {}
713 }
714
715 let _ = PersonRequest::<()>::for_id(665_489).build_and_get().await.unwrap();
716 let _ = PersonRequest::<EmptyHydrations>::builder().id(665_489).hydrations(EmptyHydrationsRequestData::default()).build_and_get().await.unwrap();
717 }
718
719 #[tokio::test]
720 async fn all_but_stats_hydrations() {
721 person_hydrations! {
722 pub struct AllButStatHydrations {
723 awards,
724 current_team,
725 depth_charts,
726 draft,
727 education,
728 jobs,
729 nicknames,
730 preferred_team,
731 relatives,
732 roster_entries,
733 transactions,
734 social,
735 external_references
736 }
737 }
738
739 let _person = PersonRequest::<AllButStatHydrations>::builder().hydrations(AllButStatHydrationsRequestData::default()).id(665_489).build_and_get().await.unwrap().people.into_iter().next().unwrap();
740 }
741
742 #[rustfmt::skip]
743 #[tokio::test]
744 async fn only_stats_hydrations() {
745 person_hydrations! {
746 pub struct StatOnlyHydrations {
747 stats: { [Sabermetrics] + [Pitching] },
748 }
749 }
750
751 let player = PlayersRequest::<()>::for_sport(SportId::MLB)
752 .season(TEST_YEAR)
753 .build_and_get()
754 .await
755 .unwrap()
756 .people
757 .into_iter()
758 .find(|player| player.full_name == "Kevin Gausman")
759 .unwrap();
760
761 let request = PersonRequest::<StatOnlyHydrations>::builder()
762 .id(player.id)
763 .hydrations(StatOnlyHydrations::builder()
764 .stats(StatOnlyHydrationsInlineStats::builder()
765 .season(2023)
766 )
768 ).build();
769 println!("{request}");
770 let player = request.get()
771 .await
772 .unwrap()
773 .people
774 .into_iter()
775 .next()
776 .unwrap();
777
778 dbg!(&player.extras.stats);
779 }
780}