1#![allow(clippy::trait_duplication_in_bounds, reason = "serde duplicates it")]
26
27pub mod free_agents;
28pub mod stats;
29pub mod players;
30
31use crate::cache::Requestable;
32use crate::draft::School;
33use crate::hydrations::Hydrations;
34use crate::{Copyright, Gender, Handedness, HeightMeasurement};
35use crate::request::RequestURL;
36use bon::Builder;
37use chrono::{Local, NaiveDate};
38use derive_more::{Deref, DerefMut, Display, From};
39use serde::de::Error;
40use serde::{Deserialize, Deserializer};
41use serde_with::{serde_as, DefaultOnError};
42use std::fmt::{Debug, Display, Formatter};
43use std::hash::{Hash, Hasher};
44use std::ops::{Deref, DerefMut};
45use crate::meta::NamedPosition;
46use crate::team::NamedTeam;
47
48#[cfg(feature = "cache")]
49use crate::{rwlock_const_new, RwLock, cache::CacheTable};
50
51#[derive(Debug, Deserialize, PartialEq, Clone)]
53#[serde(rename_all = "camelCase")]
54#[serde(bound = "H: PersonHydrations")]
55pub struct PeopleResponse<H: PersonHydrations> {
56 pub copyright: Copyright,
57 #[serde(default)]
58 pub people: Vec<Person<H>>,
59}
60
61#[derive(Debug, Deref, DerefMut, Deserialize, Clone)]
65#[serde(rename_all = "camelCase")]
66#[serde(bound = "H: PersonHydrations")]
67pub struct Ballplayer<H: PersonHydrations> {
68 #[serde(deserialize_with = "crate::try_from_str")]
69 #[serde(default)]
70 pub primary_number: Option<u8>,
71 #[serde(flatten)]
72 pub birth_data: BirthData,
73 #[serde(flatten)]
74 pub body_measurements: BodyMeasurements,
75 pub gender: Gender,
76 pub draft_year: Option<u16>,
77 #[serde(rename = "mlbDebutDate")]
78 pub mlb_debut: Option<NaiveDate>,
79 pub bat_side: Handedness,
80 pub pitch_hand: Handedness,
81 #[serde(flatten)]
82 pub strike_zone: StrikeZoneMeasurements,
83 #[serde(rename = "nickName")]
84 pub nickname: Option<String>,
85
86 #[deref]
87 #[deref_mut]
88 #[serde(flatten)]
89 inner: Box<RegularPerson<H>>,
90}
91
92#[derive(Debug, Deserialize, Deref, DerefMut, Clone)]
96#[serde(rename_all = "camelCase")]
97#[serde(bound = "H: PersonHydrations")]
98pub struct RegularPerson<H: PersonHydrations> {
99 pub primary_position: NamedPosition,
100 #[serde(default)]
102 pub first_name: String,
103 #[serde(rename = "nameSuffix")]
104 pub suffix: Option<String>,
105 #[serde(default)] pub middle_name: String,
107 #[serde(default)]
108 pub last_name: String,
109 #[serde(default)]
110 #[serde(rename = "useName")]
111 pub use_first_name: String,
112 #[serde(default)]
113 pub use_last_name: String,
114 #[serde(default)]
115 pub boxscore_name: String,
116
117 pub is_player: bool,
118 #[serde(default)]
119 pub is_verified: bool,
120 pub active: bool,
121
122 #[deref]
123 #[deref_mut]
124 #[serde(flatten)]
125 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, Deserialize, Clone, From)]
200#[serde(untagged)]
201#[serde(bound = "H: PersonHydrations")]
202pub enum Person<H: PersonHydrations = ()> {
203 Ballplayer(Ballplayer<H>),
204 Regular(RegularPerson<H>),
205}
206
207impl<H: PersonHydrations> Person<H> {
208 #[must_use]
209 pub fn as_ballplayer(&self) -> Option<&Ballplayer<H>> {
210 match self {
211 Self::Ballplayer(x) => Some(x),
212 Self::Regular(_) => None,
213 }
214 }
215}
216
217impl<H: PersonHydrations> Person<H> {
218 #[must_use]
219 pub fn as_ballplayer_mut(&mut self) -> Option<&mut Ballplayer<H>> {
220 match self {
221 Self::Ballplayer(x) => Some(x),
222 Self::Regular(_) => None,
223 }
224 }
225}
226
227impl<H: PersonHydrations> Person<H> {
228 #[must_use]
229 pub fn into_ballplayer(self) -> Option<Ballplayer<H>> {
230 match self {
231 Self::Ballplayer(x) => Some(x),
232 Self::Regular(_) => None,
233 }
234 }
235}
236
237impl<H: PersonHydrations> Deref for Person<H> {
238 type Target = RegularPerson<H>;
239
240 fn deref(&self) -> &Self::Target {
241 match self {
242 Self::Ballplayer(x) => x,
243 Self::Regular(x) => x,
244 }
245 }
246}
247
248impl<H: PersonHydrations> DerefMut for Person<H> {
249 fn deref_mut(&mut self) -> &mut Self::Target {
250 match self {
251 Self::Ballplayer(x) => x,
252 Self::Regular(x) => x,
253 }
254 }
255}
256
257impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<Person<H2>> for Person<H1> {
258 fn eq(&self, other: &Person<H2>) -> bool {
259 self.id == other.id
260 }
261}
262
263impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<Ballplayer<H2>> for Ballplayer<H1> {
264 fn eq(&self, other: &Ballplayer<H2>) -> bool {
265 self.id == other.id
266 }
267}
268
269impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<RegularPerson<H2>> for RegularPerson<H1> {
270 fn eq(&self, other: &RegularPerson<H2>) -> bool {
271 self.id == other.id
272 }
273}
274
275id_only_eq_impl!(NamedPerson, id);
276
277#[derive(Builder)]
279#[builder(derive(Into))]
280pub struct PersonRequest<H: PersonHydrations> {
281 #[builder(into)]
282 id: PersonId,
283
284 #[builder(into)]
285 hydrations: H::RequestData,
286}
287
288impl PersonRequest<()> {
289 pub fn for_id(id: impl Into<PersonId>) -> PersonRequestBuilder<(), person_request_builder::SetHydrations<person_request_builder::SetId>> {
290 Self::builder().id(id).hydrations(<() as Hydrations>::RequestData::default())
291 }
292}
293
294impl<H: PersonHydrations, S: person_request_builder::State + person_request_builder::IsComplete> crate::request::RequestURLBuilderExt for PersonRequestBuilder<H, S> {
295 type Built = PersonRequest<H>;
296}
297
298impl<H: PersonHydrations> Display for PersonRequest<H> {
299 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
300 let hydration_text = H::hydration_text(&self.hydrations);
301 if hydration_text.is_empty() {
302 write!(f, "http://statsapi.mlb.com/api/v1/people/{}", self.id)
303 } else {
304 write!(f, "http://statsapi.mlb.com/api/v1/people/{}?hydrate={hydration_text}", self.id)
305 }
306 }
307}
308
309impl<H: PersonHydrations> RequestURL for PersonRequest<H> {
310 type Response = PeopleResponse<H>;
311}
312
313#[repr(transparent)]
315#[derive(Debug, Deref, Display, PartialEq, Copy, Clone, Hash, From)]
316pub struct JerseyNumber(u8);
317
318impl<'de> Deserialize<'de> for JerseyNumber {
319 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
320 where
321 D: Deserializer<'de>,
322 {
323 String::deserialize(deserializer)?.parse::<u8>().map(JerseyNumber).map_err(D::Error::custom)
324 }
325}
326
327#[derive(Debug, Deserialize, PartialEq, Clone)]
329#[serde(rename_all = "camelCase")]
330pub struct BirthData {
331 pub birth_date: NaiveDate,
332 pub birth_city: String,
333 #[serde(rename = "birthStateProvince")]
334 pub birth_state_or_province: Option<String>,
335 pub birth_country: String,
336}
337
338impl BirthData {
339 #[must_use]
340 pub fn current_age(&self) -> u16 {
341 Local::now().naive_local().date().years_since(self.birth_date).and_then(|x| u16::try_from(x).ok()).unwrap_or(0)
342 }
343}
344
345#[derive(Debug, Deserialize, PartialEq, Clone)]
347#[serde(rename_all = "camelCase")]
348pub struct BodyMeasurements {
349 pub height: HeightMeasurement,
350 pub weight: u16,
351}
352
353#[derive(Debug, Deserialize, PartialEq, Clone)]
355#[serde(rename_all = "camelCase")]
356pub struct StrikeZoneMeasurements {
357 pub strike_zone_top: f64,
358 pub strike_zone_bottom: f64,
359}
360
361#[serde_as]
363#[derive(Debug, Deserialize, PartialEq, Clone)]
364#[serde(rename_all = "camelCase")]
365pub struct PreferredTeamData {
366 #[serde(default)]
367 #[serde_as(deserialize_as = "DefaultOnError")]
368 pub jersey_number: Option<JerseyNumber>,
369 pub position: NamedPosition,
370 pub team: NamedTeam,
371}
372
373#[derive(Debug, Deserialize, PartialEq, Clone)]
375#[serde(rename_all = "camelCase")]
376pub struct Relative {
377 pub has_stats: bool,
378 pub relation: String,
379 #[serde(flatten)]
380 pub person: NamedPerson,
381}
382
383#[derive(Debug, Deserialize, PartialEq, Clone, Default)]
385pub struct Education {
386 #[serde(default)]
387 pub highschools: Vec<School>,
388 #[serde(default)]
389 pub colleges: Vec<School>,
390}
391
392pub trait PersonHydrations: Hydrations {}
394
395impl PersonHydrations for () {}
396
397#[macro_export]
458macro_rules! person_hydrations {
459 (@ inline_structs [stats: { $($contents:tt)* } $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
460 ::pastey::paste! {
461 $crate::stats_hydrations! {
462 $vis struct [<$name InlineStats>] {
463 $($contents)*
464 }
465 }
466
467 $crate::person_hydrations! { @ inline_structs [$($($rest)*)?]
468 $vis struct $name {
469 $($field_tt)*
470 stats: [<$name InlineStats>],
471 }
472 }
473 }
474 };
475 (@ inline_structs [$marker:ident : { $($contents:tt)* } $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
476 ::core::compile_error!("Found unknown inline struct");
477 };
478 (@ inline_structs [$marker:ident $(: $value:ty)? $(, $($rest:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
479 ::pastey::paste! {
480 $crate::person_hydrations! { @ inline_structs [$($($rest)*)?]
481 $vis struct $name {
482 $($field_tt)*
483 $marker $(: $value)?,
484 }
485 }
486 }
487 };
488 (@ inline_structs [$(,)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
489 ::pastey::paste! {
490 $crate::person_hydrations! { @ actual
491 $vis struct $name {
492 $($field_tt)*
493 }
494 }
495 }
496 };
497 (@ actual
498 $vis:vis struct $name:ident {
499 $(awards $awards_comma:tt)?
500 $(current_team $current_team_comma:tt)?
501 $(depth_charts $depth_charts_comma:tt)?
502 $(draft $draft_comma:tt)?
503 $(education $education_comma:tt)?
504 $(jobs $jobs_comma:tt)?
505 $(nicknames $nicknames_comma:tt)?
506 $(preferred_team $preferred_team_comma:tt)?
507 $(relatives $relatives_comma:tt)?
508 $(roster_entries $roster_entries_comma:tt)?
509 $(transactions $transactions_comma:tt)?
510 $(social $social_comma:tt)?
511 $(stats: $stats:ty ,)?
512 $(external_references $external_references_comma:tt)?
513 }
514 ) => {
515 ::pastey::paste! {
516 #[derive(::core::fmt::Debug, ::serde::Deserialize, ::core::cmp::PartialEq, ::core::clone::Clone)]
517 #[serde(rename_all = "camelCase")]
518 $vis struct $name {
519 $(#[serde(default)] pub awards: ::std::vec::Vec<$crate::awards::Award> $awards_comma)?
520 $(pub current_team: ::core::option::Option<$crate::team::NamedTeam> $current_team_comma)?
521 $(#[serde(default)] pub depth_charts: ::std::vec::Vec<$crate::team::roster::RosterEntry> $depth_charts_comma)?
522 $(#[serde(default, rename = "drafts")] pub draft: ::std::vec::Vec<$crate::draft::DraftPick> $draft_comma)?
523 $(#[serde(default)] pub education: $crate::person::Education $education_comma)?
524 $(#[serde(default, rename = "jobEntries")] pub jobs: ::std::vec::Vec<$crate::jobs::EmployedPerson> $jobs_comma)?
525 $(#[serde(default)] pub nicknames: ::std::vec::Vec<String> $nicknames_comma)?
526 $(pub preferred_team: ::core::option::Option<$crate::person::PreferredTeamData> $preferred_team_comma)?
527 $(#[serde(default)] pub relatives: ::std::vec::Vec<$crate::person::Relative> $relatives_comma)?
528 $(#[serde(default)] pub roster_entries: ::std::vec::Vec<$crate::team::roster::RosterEntry> $roster_entries_comma)?
529 $(#[serde(default)] pub transactions: ::std::vec::Vec<$crate::transactions::Transaction> $transactions_comma)?
530 $(#[serde(flatten)] pub stats: $stats ,)?
531 $(#[serde(default, rename = "social")] pub socials: ::std::collections::HashMap<String, Vec<String>> $social_comma)?
532 $(#[serde(default, rename = "xrefIds")] pub external_references: ::std::vec::Vec<$crate::types::ExternalReference> $external_references_comma)?
533 }
534
535 impl $crate::person::PersonHydrations for $name {}
536
537 impl $crate::hydrations::Hydrations for $name {
538 type RequestData = [<$name RequestData>];
539
540 fn hydration_text(_data: &Self::RequestData) -> ::std::borrow::Cow<'static, str> {
541 let text = ::std::borrow::Cow::Borrowed(::std::concat!(
542 $("awards," $awards_comma)?
543 $("currentTeam," $current_team_comma)?
544 $("depthCharts," $depth_charts_comma)?
545 $("draft," $draft_comma)?
546 $("education," $education_comma)?
547 $("jobs," $jobs_comma)?
548 $("nicknames," $nicknames_comma)?
549 $("preferredTeam," $preferred_team_comma)?
550 $("relatives," $relatives_comma)?
551 $("rosterEntries," $roster_entries_comma)?
552 $("transactions," $transactions_comma)?
553 $("social," $social_comma)?
554 $("xrefId," $external_references_comma)?
555 ));
556
557 $(
558 let text = ::std::borrow::Cow::Owned(::std::format!("{text}stats({}),", <$stats as $crate::hydrations::Hydrations>::hydration_text(&_data.stats)));
559 )?
560
561 text
562 }
563 }
564
565 #[derive(::bon::Builder)]
566 #[builder(derive(Into))]
567 $vis struct [<$name RequestData>] {
568 $(#[builder(into)] stats: <$stats as $crate::hydrations::Hydrations>::RequestData,)?
569 }
570
571 impl $name {
572 #[allow(unused, reason = "potentially unused if the builder is Default")]
573 pub fn builder() -> [<$name RequestDataBuilder>] {
574 [<$name RequestData>]::builder()
575 }
576 }
577
578 impl ::core::default::Default for [<$name RequestData>]
579 where
580 $(for<'no_rfc_2056> <$stats as $crate::hydrations::Hydrations>::RequestData: ::core::default::Default,)?
581 {
582 fn default() -> Self {
583 Self {
584 $(stats: <<$stats as $crate::hydrations::Hydrations>::RequestData as ::core::default::Default>::default(),)?
585 }
586 }
587 }
588 }
589 };
590 ($vis:vis struct $name:ident {
591 $($tt:tt)*
592 }) => {
593 $crate::person_hydrations! { @ inline_structs [$($tt)*] $vis struct $name {} }
594 };
595}
596
597#[cfg(feature = "cache")]
598static CACHE: RwLock<CacheTable<Person<()>>> = rwlock_const_new(CacheTable::new());
599
600impl Requestable for Person<()> {
601 type Identifier = PersonId;
602 type URL = PersonRequest<()>;
603
604 fn id(&self) -> &Self::Identifier {
605 &self.id
606 }
607
608 fn url_for_id(id: &Self::Identifier) -> Self::URL {
609 PersonRequest::for_id(*id).build()
610 }
611
612 fn get_entries(response: <Self::URL as RequestURL>::Response) -> impl IntoIterator<Item = Self>
613 where
614 Self: Sized,
615 {
616 response.people
617 }
618
619 #[cfg(feature = "cache")]
620 fn get_cache_table() -> &'static RwLock<CacheTable<Self>>
621 where
622 Self: Sized,
623 {
624 &CACHE
625 }
626}
627
628entrypoint!(PersonId => Person);
629entrypoint!(NamedPerson.id => Person);
630entrypoint!(for < H > RegularPerson < H > . id => Person < > where H: PersonHydrations);
631entrypoint!(for < H > Ballplayer < H > . id => Person < > where H: PersonHydrations);
632
633#[cfg(test)]
634mod tests {
635 use crate::person::players::PlayersRequest;
636 use crate::request::RequestURLBuilderExt;
637 use crate::sport::SportId;
638 use super::*;
639 use crate::TEST_YEAR;
640
641 #[tokio::test]
642 async fn no_hydrations() {
643 person_hydrations! {
644 pub struct EmptyHydrations {}
645 }
646
647 let _ = PersonRequest::<()>::for_id(665_489).build_and_get().await.unwrap();
648 let _ = PersonRequest::<EmptyHydrations>::builder().id(665_489).hydrations(EmptyHydrationsRequestData::default()).build_and_get().await.unwrap();
649 }
650
651 #[tokio::test]
652 async fn all_but_stats_hydrations() {
653 person_hydrations! {
654 pub struct AllButStatHydrations {
655 awards,
656 current_team,
657 depth_charts,
658 draft,
659 education,
660 jobs,
661 nicknames,
662 preferred_team,
663 relatives,
664 roster_entries,
665 transactions,
666 social,
667 external_references
668 }
669 }
670
671 let _person = PersonRequest::<AllButStatHydrations>::builder().hydrations(AllButStatHydrationsRequestData::default()).id(665_489).build_and_get().await.unwrap().people.into_iter().next().unwrap();
672 }
673
674 #[rustfmt::skip]
675 #[tokio::test]
676 async fn only_stats_hydrations() {
677 person_hydrations! {
678 pub struct StatOnlyHydrations {
679 stats: { [Sabermetrics] + [Pitching] },
680 }
681 }
682
683 let player = PlayersRequest::<()>::for_sport(SportId::MLB)
684 .season(TEST_YEAR)
685 .build_and_get()
686 .await
687 .unwrap()
688 .people
689 .into_iter()
690 .find(|player| player.full_name == "Kevin Gausman")
691 .unwrap();
692
693 let request = PersonRequest::<StatOnlyHydrations>::builder()
694 .id(player.id)
695 .hydrations(StatOnlyHydrations::builder()
696 .stats(StatOnlyHydrationsInlineStats::builder()
697 .season(2023)
698 )
700 ).build();
701 println!("{request}");
702 let player = request.get()
703 .await
704 .unwrap()
705 .people
706 .into_iter()
707 .next()
708 .unwrap();
709
710 dbg!(&player.extras.stats);
711 }
712}