1#![allow(clippy::trait_duplication_in_bounds, reason = "serde duplicates it")]
2
3pub mod free_agents;
4pub mod stats;
5pub mod people;
6pub mod players;
7
8use crate::cache::Requestable;
9use crate::draft::School;
10use crate::hydrations::Hydrations;
11use crate::season::SeasonId;
12use crate::types::{Gender, Handedness, HeightMeasurement};
13use crate::request::RequestURL;
14use bon::Builder;
15use chrono::{Local, NaiveDate};
16use derive_more::{Deref, DerefMut, Display, From};
17use people::PeopleResponse;
18use serde::de::Error;
19use serde::{Deserialize, Deserializer};
20use std::fmt::{Debug, Display, Formatter};
21use std::ops::{Deref, DerefMut};
22use crate::positions::NamedPosition;
23use crate::team::NamedTeam;
24
25#[cfg(feature = "cache")]
26use crate::{rwlock_const_new, RwLock, cache::CacheTable};
27
28#[derive(Debug, Deref, DerefMut, Deserialize, Eq, Clone)]
29#[serde(rename_all = "camelCase")]
30#[serde(bound = "H: PersonHydrations")]
31pub struct Ballplayer<H: PersonHydrations> {
32 #[serde(deserialize_with = "crate::types::try_from_str")]
33 #[serde(default)]
34 pub primary_number: Option<u8>,
35 #[serde(flatten)]
36 pub birth_data: BirthData,
37 #[serde(flatten)]
38 pub body_measurements: BodyMeasurements,
39 pub gender: Gender,
40 pub draft_year: Option<u16>,
41 #[serde(rename = "mlbDebutDate")]
42 pub mlb_debut: Option<NaiveDate>,
43 pub bat_side: Handedness,
44 pub pitch_hand: Handedness,
45 #[serde(flatten)]
46 pub strike_zone: StrikeZoneMeasurements,
47 #[serde(rename = "nickName")]
48 pub nickname: Option<String>,
49
50 #[deref]
51 #[deref_mut]
52 #[serde(flatten)]
53 inner: Box<RegularPerson<H>>,
54}
55
56#[derive(Debug, Deserialize, Deref, DerefMut, Eq, Clone)]
57#[serde(rename_all = "camelCase")]
58#[serde(bound = "H: PersonHydrations")]
59pub struct RegularPerson<H: PersonHydrations> {
60 pub primary_position: NamedPosition,
61 #[serde(default)]
63 pub first_name: String,
64 #[serde(rename = "nameSuffix")]
65 pub suffix: Option<String>,
66 #[serde(default)] pub middle_name: String,
68 #[serde(default)]
69 pub last_name: String,
70 #[serde(default)]
71 #[serde(rename = "useName")]
72 pub use_first_name: String,
73 #[serde(default)]
74 pub use_last_name: String,
75 #[serde(default)]
76 pub boxscore_name: String,
77
78 pub is_player: bool,
79 #[serde(default)]
80 pub is_verified: bool,
81 pub active: bool,
82
83 #[deref]
84 #[deref_mut]
85 #[serde(flatten)]
86 inner: NamedPerson,
87
88 #[serde(flatten)]
89 pub extras: Box<H>,
90}
91
92impl<H: PersonHydrations> RegularPerson<H> {
93 #[must_use]
94 pub fn name_first_last(&self) -> String {
95 format!("{0} {1}", self.use_first_name, self.use_last_name)
96 }
97
98 #[must_use]
99 pub fn name_last_first(&self) -> String {
100 format!("{1}, {0}", self.use_first_name, self.use_last_name)
101 }
102
103 #[must_use]
104 pub fn name_last_first_initial(&self) -> String {
105 self.use_first_name.chars().next().map_or_else(|| self.use_last_name.clone(), |char| format!("{1}, {0}", char, self.use_last_name))
106 }
107
108 #[must_use]
109 pub fn name_first_initial_last(&self) -> String {
110 self.use_first_name.chars().next().map_or_else(|| self.use_last_name.clone(), |char| format!("{0} {1}", char, self.use_last_name))
111 }
112
113 #[must_use]
114 pub fn name_fml(&self) -> String {
115 format!("{0} {1} {2}", self.use_first_name, self.middle_name, self.use_last_name)
116 }
117
118 #[must_use]
119 pub fn name_lfm(&self) -> String {
120 format!("{2}, {0} {1}", self.use_first_name, self.middle_name, self.use_last_name)
121 }
122}
123
124#[derive(Debug, Deserialize, Clone, Hash)]
125#[serde(rename_all = "camelCase")]
126pub struct NamedPerson {
127 pub full_name: String,
129
130 #[serde(flatten)]
131 pub id: PersonId,
132}
133
134impl NamedPerson {
135 #[must_use]
136 pub(crate) fn unknown_person() -> Self {
137 Self {
138 full_name: "null".to_owned(),
139 id: PersonId::new(0),
140 }
141 }
142
143 #[must_use]
144 pub fn is_unknown(&self) -> bool {
145 *self.id == 0
146 }
147}
148
149id!(PersonId { id: u32 });
150
151#[derive(Debug, Deserialize, Clone, Eq, From)]
152#[serde(untagged)]
153#[serde(bound = "H: PersonHydrations")]
154pub enum Person<H: PersonHydrations = ()> {
155 Ballplayer(Box<Ballplayer<H>>),
156 Regular(Box<RegularPerson<H>>),
157}
158
159impl<H: PersonHydrations> Person<H> {
160 #[must_use]
161 pub fn as_ballplayer(&self) -> Option<&Ballplayer<H>> {
162 match self {
163 Self::Ballplayer(x) => Some(x),
164 Self::Regular(_) => None,
165 }
166 }
167}
168
169impl<H: PersonHydrations> Person<H> {
170 #[must_use]
171 pub fn as_ballplayer_mut(&mut self) -> Option<&mut Ballplayer<H>> {
172 match self {
173 Self::Ballplayer(x) => Some(x),
174 Self::Regular(_) => None,
175 }
176 }
177}
178
179impl<H: PersonHydrations> Person<H> {
180 #[must_use]
181 pub fn into_ballplayer(self) -> Option<Box<Ballplayer<H>>> {
182 match self {
183 Self::Ballplayer(x) => Some(x),
184 Self::Regular(_) => None,
185 }
186 }
187}
188
189impl<H: PersonHydrations> Deref for Person<H> {
190 type Target = RegularPerson<H>;
191
192 fn deref(&self) -> &Self::Target {
193 match self {
194 Self::Ballplayer(x) => x,
195 Self::Regular(x) => x,
196 }
197 }
198}
199
200impl<H: PersonHydrations> DerefMut for Person<H> {
201 fn deref_mut(&mut self) -> &mut Self::Target {
202 match self {
203 Self::Ballplayer(x) => x,
204 Self::Regular(x) => x,
205 }
206 }
207}
208
209impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<Person<H2>> for Person<H1> {
210 fn eq(&self, other: &Person<H2>) -> bool {
211 self.id == other.id
212 }
213}
214
215impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<Ballplayer<H2>> for Ballplayer<H1> {
216 fn eq(&self, other: &Ballplayer<H2>) -> bool {
217 self.id == other.id
218 }
219}
220
221impl<H1: PersonHydrations, H2: PersonHydrations> PartialEq<RegularPerson<H2>> for RegularPerson<H1> {
222 fn eq(&self, other: &RegularPerson<H2>) -> bool {
223 self.id == other.id
224 }
225}
226
227id_only_eq_impl!(NamedPerson, id);
228
229#[derive(Builder)]
230#[builder(derive(Into))]
231pub struct PersonRequest<H: PersonHydrations> {
232 #[builder(into)]
233 id: PersonId,
234
235 #[builder(into)]
236 hydrations: H::RequestData,
237}
238
239impl<H: PersonHydrations> PersonRequest<H> {
240 pub fn for_id(id: impl Into<PersonId>) -> PersonRequestBuilder<H, person_request_builder::SetHydrations<person_request_builder::SetId>> where H::RequestData: Default {
241 PersonRequest::builder().id(id).hydrations(H::RequestData::default())
242 }
243}
244
245impl<H: PersonHydrations, S: person_request_builder::State + person_request_builder::IsComplete> crate::request::RequestURLBuilderExt for PersonRequestBuilder<H, S> {
246 type Built = PersonRequest<H>;
247}
248
249impl<H: PersonHydrations> Display for PersonRequest<H> {
250 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
251 let hydration_text = H::hydration_text(&self.hydrations);
252 if hydration_text.is_empty() {
253 write!(f, "http://statsapi.mlb.com/api/v1/people/{}", self.id)
254 } else {
255 write!(f, "http://statsapi.mlb.com/api/v1/people/{}?hydrate={hydration_text}", self.id)
256 }
257 }
258}
259
260impl<H: PersonHydrations> RequestURL for PersonRequest<H> {
261 type Response = PeopleResponse<H>;
262}
263
264#[repr(transparent)]
279#[derive(Debug, Deref, Display, PartialEq, Eq, Copy, Clone, Hash, From)]
280pub struct JerseyNumber(u8);
281
282impl<'de> Deserialize<'de> for JerseyNumber {
283 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
284 where
285 D: Deserializer<'de>,
286 {
287 String::deserialize(deserializer)?.parse::<u8>().map(JerseyNumber).map_err(D::Error::custom)
288 }
289}
290
291#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
292#[serde(rename_all = "camelCase")]
293pub struct BirthData {
294 pub birth_date: NaiveDate,
295 pub birth_city: String,
296 #[serde(rename = "birthStateProvince")]
297 pub birth_state_or_province: Option<String>,
298 pub birth_country: String,
299}
300
301impl BirthData {
302 #[must_use]
303 pub fn current_age(&self) -> u16 {
304 Local::now().naive_local().date().years_since(self.birth_date).and_then(|x| u16::try_from(x).ok()).unwrap_or(0)
305 }
306}
307
308#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
309#[serde(rename_all = "camelCase")]
310pub struct BodyMeasurements {
311 pub height: HeightMeasurement,
312 pub weight: u16,
313}
314
315#[derive(Debug, Deserialize, PartialEq, Clone)]
316#[serde(rename_all = "camelCase")]
317pub struct StrikeZoneMeasurements {
318 pub strike_zone_top: f64,
319 pub strike_zone_bottom: f64,
320}
321
322impl Eq for StrikeZoneMeasurements {}
323
324#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
325#[serde(rename_all = "camelCase")]
326pub struct PreferredTeamData {
327 pub jersey_number: JerseyNumber,
328 pub position: NamedPosition,
329 pub team: NamedTeam,
330}
331
332#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
333#[serde(rename_all = "camelCase")]
334pub struct Relative {
335 pub has_stats: bool,
336 pub relation: String,
337 #[serde(flatten)]
338 pub person: NamedPerson,
339}
340
341#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
342pub struct Education {
343 #[serde(default)]
344 pub highschools: Vec<School>,
345 #[serde(default)]
346 pub colleges: Vec<School>,
347}
348
349#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
350pub struct ExternalReference {
351 #[serde(rename = "xrefId")]
352 pub id: String,
353 #[serde(rename = "xrefType")]
354 pub xref_type: String,
355 pub season: Option<SeasonId>,
356}
357
358pub trait PersonHydrations: Hydrations {}
359
360impl PersonHydrations for () {}
361
362#[doc(hidden)]
363#[macro_export]
364macro_rules! __person_hydrations_hydration_text {
365 () => { "" };
366 ($first:literal $(, $rest:literal)* $(,)?) => {
367 ::core::concat!(
368 ::core::stringify!($first),
369 $(",", ::core::stringify!($rest),)*
370 )
371 };
372}
373
374#[macro_export]
403macro_rules! person_hydrations {
404 (
405 $vis:vis struct $name:ident {
406 $(awards $awards_comma:tt)?
407 $(current_team $current_team_comma:tt)?
408 $(depth_charts $depth_charts_comma:tt)?
409 $(draft $draft_comma:tt)?
410 $(education $education_comma:tt)?
411 $(jobs $jobs_comma:tt)?
412 $(nicknames $nicknames_comma:tt)?
413 $(preferred_team $preferred_team_comma:tt)?
414 $(relatives $relatives_comma:tt)?
415 $(roster_entries $roster_entries_comma:tt)?
416 $(transactions $transactions_comma:tt)?
417 $(social $social_comma:tt)?
418 $(stats: $stats:path ,)?
419 $(xref_id $xref_id_comma:tt)?
420 }
421 ) => {
422 ::pastey::paste! {
423 #[derive(::core::fmt::Debug, ::serde::Deserialize, ::core::cmp::PartialEq, ::core::cmp::Eq, ::core::clone::Clone)]
424 #[serde(rename_all = "camelCase")]
425 $vis struct $name {
426 $(#[serde(default)] pub awards: ::std::vec::Vec<$crate::awards::Award> $awards_comma)?
427 $(pub current_team: ::core::option::Option<$crate::team::NamedTeam> $current_team_comma)?
428 $(#[serde(default)] pub depth_charts: ::std::vec::Vec<$crate::team::roster::RosterEntry> $depth_charts_comma)?
429 $(#[serde(default, rename = "drafts")] pub draft: ::std::vec::Vec<$crate::draft::DraftPick> $draft_comma)?
430 $(#[serde(default)] pub education: $crate::person::Education $education_comma)?
431 $(#[serde(default, rename = "jobEntries")] pub jobs: ::std::vec::Vec<$crate::jobs::EmployedPerson> $jobs_comma)?
432 $(#[serde(default)] pub nicknames: ::std::vec::Vec<String> $nicknames_comma)?
433 $(pub preferred_team: ::core::option::Option<$crate::person::PreferredTeamData> $preferred_team_comma)?
434 $(#[serde(default)] pub relatives: ::std::vec::Vec<$crate::person::Relative> $relatives_comma)?
435 $(#[serde(default)] pub roster_entries: ::std::vec::Vec<$crate::team::roster::RosterEntry> $roster_entries_comma)?
436 $(#[serde(default)] pub transactions: ::std::vec::Vec<$crate::transactions::Transaction> $transactions_comma)?
437 $(#[serde(flatten)] pub stats: $stats ,)?
438 $(#[serde(default)] pub social: ::std::collections::HashMap<String, Vec<String>> $social_comma)?
439 $(#[serde(default, rename = "xrefIds")] pub xref_id: ::std::vec::Vec<ExternalReference> $xref_id_comma)?
440 }
441
442 impl $crate::person::PersonHydrations for $name {}
443
444 impl $crate::hydrations::Hydrations for $name {}
445
446 impl $crate::hydrations::HydrationText for $name {
447 type RequestData = [<$name RequestData>];
448
449 fn hydration_text(_data: &Self::RequestData) -> ::std::borrow::Cow<'static, str> {
450 let text = ::std::borrow::Cow::Borrowed($crate::__person_hydrations_hydration_text!(
451 $("awards" $awards_comma)?
452 $("currentTeam" $current_team_comma)?
453 $("depthCharts" $depth_charts_comma)?
454 $("draft" $draft_comma)?
455 $("education" $education_comma)?
456 $("jobs" $jobs_comma)?
457 $("nicknames" $nicknames_comma)?
458 $("preferredTeam" $preferred_team_comma)?
459 $("relatives" $relatives_comma)?
460 $("rosterEntries" $roster_entries_comma)?
461 $("transactions" $transactions_comma)?
462 $("social" $social_comma)?
463 $("xrefId" $xref_id_comma)?
464 ));
465
466 $(
467 let text = if text.is_empty() {
468 ::std::borrow::Cow::Owned(::std::format!("stats({})", <$stats as $crate::hydrations::HydrationText>::hydration_text(&_data.stats)))
469 } else {
470 ::std::borrow::Cow::Owned(::std::format!("{text},stats({})", <$stats as $crate::hydrations::HydrationText>::hydration_text(&_data.stats)))
471 };
472 )?
473
474 text
475 }
476 }
477
478 #[derive(::bon::Builder)]
479 #[builder(derive(Into))]
480 $vis struct [<$name RequestData>] {
481 $(#[builder(into)] stats: <$stats as $crate::hydrations::HydrationText>::RequestData,)?
482 }
483
484 impl $name {
485 #[allow(unused)]
486 pub fn builder() -> [<$name RequestDataBuilder>] {
487 [<$name RequestData>]::builder()
488 }
489 }
490
491 impl ::core::default::Default for [<$name RequestData>]
492 where
493 $(for<'no_rfc_2056> <$stats as $crate::hydrations::HydrationText>::RequestData: ::core::default::Default,)?
494 {
495 fn default() -> Self {
496 Self {
497 $(stats: <<$stats as $crate::hydrations::HydrationText>::RequestData as ::core::default::Default>::default(),)?
498 }
499 }
500 }
501 }
502 };
503}
504
505#[cfg(feature = "cache")]
506static CACHE: RwLock<CacheTable<Person<()>>> = rwlock_const_new(CacheTable::new());
507
508impl Requestable for Person<()> {
509 type Identifier = PersonId;
510 type URL = PersonRequest<()>;
511
512 fn id(&self) -> &Self::Identifier {
513 &self.id
514 }
515
516 fn url_for_id(id: &Self::Identifier) -> Self::URL {
517 PersonRequest::for_id(*id).build()
518 }
519
520 fn get_entries(response: <Self::URL as RequestURL>::Response) -> impl IntoIterator<Item = Self>
521 where
522 Self: Sized,
523 {
524 response.people.into_iter().map(Box::new).map(Person::Ballplayer)
525 }
526
527 #[cfg(feature = "cache")]
528 fn get_cache_table() -> &'static RwLock<CacheTable<Self>>
529 where
530 Self: Sized,
531 {
532 &CACHE
533 }
534}
535
536entrypoint!(PersonId => Person);
537entrypoint!(NamedPerson.id => Person);
538entrypoint!(for < H > RegularPerson < H > . id => Person < > where H: PersonHydrations);
539entrypoint!(for < H > Ballplayer < H > . id => Person < > where H: PersonHydrations);
540
541#[cfg(test)]
542mod tests {
543 use crate::request::RequestURLBuilderExt;
544 use crate::roster_types::RosterType;
545 use super::*;
546 use crate::team::roster::RosterRequest;
547 use crate::team::teams::TeamsRequest;
548 use crate::{stats, TEST_YEAR};
549
550 #[tokio::test]
551 async fn no_hydrations() {
552 person_hydrations! {
553 pub struct EmptyHydrations {}
554 }
555
556 let _ = PersonRequest::<()>::for_id(665_489).build_and_get().await.unwrap();
557 let _ = PersonRequest::<EmptyHydrations>::for_id(665_489).build_and_get().await.unwrap();
558 }
559
560 #[tokio::test]
561 async fn all_but_stats_hydrations() {
562 person_hydrations! {
563 pub struct AllButStatHydrations {
564 awards,
565 current_team,
566 depth_charts,
567 draft,
568 education,
569 jobs,
570 nicknames,
571 preferred_team,
572 relatives,
573 roster_entries,
574 transactions,
575 social,
576 xref_id,
577 }
578 }
579
580 let _person = PersonRequest::<AllButStatHydrations>::for_id(665_489).build_and_get().await.unwrap().people.into_iter().next().unwrap();
581 }
582
583 #[rustfmt::skip]
584 #[tokio::test]
585 async fn only_stats_hydrations() {
586 stats! {
587 pub struct TestStats {
588 [Sabermetrics] = [Pitching]
589 }
590 }
591
592 person_hydrations! {
593 pub struct StatOnlyHydrations {
594 stats: TestStats,
595 }
596 }
597
598 let toronto_blue_jays = TeamsRequest::mlb_teams()
599 .season(TEST_YEAR)
600 .build_and_get()
601 .await
602 .unwrap()
603 .teams
604 .into_iter()
605 .find(|team| team.name.full_name == "Toronto Blue Jays")
606 .unwrap();
607
608 let roster = RosterRequest::builder()
609 .team_id(toronto_blue_jays.id)
610 .roster_type(RosterType::AllTime)
611 .build_and_get()
612 .await
613 .unwrap();
614
615 let player = roster
616 .roster
617 .into_iter()
618 .find(|player| player.person.full_name == "Kevin Gausman")
619 .unwrap();
620
621 let request = PersonRequest::<StatOnlyHydrations>::builder()
622 .id(player.person.id)
623 .hydrations(StatOnlyHydrations::builder()
624 .stats(TestStats::builder()
625 .season(2023)
626 )
628 ).build();
629 println!("{request}");
630 let player = request.get()
631 .await
632 .unwrap()
633 .people
634 .into_iter()
635 .next()
636 .unwrap();
637
638 dbg!(&player.extras.stats);
639 }
640}