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::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#[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 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 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#[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#[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#[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#[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#[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#[derive(Debug, Deserialize, PartialEq, Clone)]
345#[serde(rename_all = "camelCase")]
346pub struct BodyMeasurements {
347 pub height: HeightMeasurement,
348 pub weight: u16,
349}
350
351#[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#[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#[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#[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
390pub trait PersonHydrations: Hydrations {}
392
393impl PersonHydrations for () {}
394
395#[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 )
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}