Skip to main content

mlb_api/requests/person/
mod.rs

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	// '? ? 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.
62	#[serde(default)]
63	pub first_name: String,
64	#[serde(rename = "nameSuffix")]
65	pub suffix: Option<String>,
66	#[serde(default)] // this is how their API does it, so I'll copy that.
67	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	// todo: rework to be like name in Team
128	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/*pub trait PersonRequestBuilderDefaultHydrationsEdgeCase<H: PersonHydrations> {
265	fn build(self) -> PersonRequest<H>;
266}
267
268impl<H: PersonHydrations, S: person_request_builder::State> PersonRequestBuilderDefaultHydrationsEdgeCase<H> for PersonRequestBuilder<H, S>
269where
270	<H as HydrationText>::RequestData: Default,
271	S::Hydrations: person_request_builder::IsUnset
272{
273	fn build(self) -> PersonRequest<H> {
274		PersonRequestBuilder::<H, person_request_builder::SetHydrations<S>>::build(self.hydrations(<H as HydrationText>::RequestData::default()))
275	}
276}*/
277
278#[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/// Creates hydrations for a person, ex:
375///```
376///person_hydrations! {
377///    pub struct ExampleHydrations {  ->  pub struct ExampleHydrations {
378///        awards,                     ->      awards: Vec<Award>,
379///        social,                     ->      social: HashMap<String, Vec<String>>,
380///        stats: MyStats,             ->      stats: MyStats,
381///    }                               ->  }
382///}
383///```
384///
385/// The list of valid hydrations are:
386/// - `awards`
387/// - `current_team`
388/// - `depth_charts`
389/// - `draft`
390/// - `education`
391/// - `jobs`
392/// - `nicknames`
393/// - `preferred_team`
394/// - `relatives`
395/// - `roster_entries`
396/// - `transactions`
397/// - `social`
398/// - `stats`
399/// - `xref_id`
400///
401/// Note: these must appear in exactly this order
402#[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					// .situation(SituationCodeId::new("h"))
627				)
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}