mlb_api/endpoints/person/
mod.rs

1pub mod stats;
2
3use std::fmt::{Display, Formatter};
4use crate::endpoints::positions::Position;
5use crate::types::{Gender, Handedness, HeightMeasurement};
6use chrono::NaiveDate;
7use derive_more::{Deref, DerefMut, Display, From};
8use serde::Deserialize;
9use serde_with::DisplayFromStr;
10use serde_with::serde_as;
11use std::ops::{Deref, DerefMut};
12use strum::EnumTryAs;
13use crate::cache::{EndpointEntryCache, HydratedCacheTable};
14use crate::{rwlock_const_new, RwLock};
15use crate::endpoints::people::PeopleResponse;
16use crate::endpoints::StatsAPIUrl;
17
18#[serde_as]
19#[derive(Debug, Deref, DerefMut, Deserialize, PartialEq, Eq, Clone)]
20#[serde(rename_all = "camelCase")]
21pub struct Ballplayer {
22	#[serde(deserialize_with = "crate::types::try_from_str")]
23	#[serde(default)]
24	pub primary_number: Option<u8>,
25	pub current_age: u8,
26	#[serde(flatten)]
27	pub birth_data: BirthData,
28	#[serde(flatten)]
29	pub body_measurements: BodyMeasurements,
30	pub gender: Gender,
31	pub draft_year: Option<u16>,
32	#[serde(rename = "mlbDebutDate")]
33	pub mlb_debut: Option<NaiveDate>,
34	pub bat_side: Handedness,
35	pub pitch_hand: Handedness,
36	#[serde(flatten)]
37	pub strike_zone: StrikeZone,
38	#[serde(rename = "nickName")]
39	pub nickname: Option<String>,
40
41	#[deref]
42	#[deref_mut]
43	#[serde(flatten)]
44	inner: HydratedPerson,
45}
46
47#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
48#[serde(rename_all = "camelCase")]
49pub struct BirthData {
50	pub birth_date: NaiveDate,
51	pub birth_city: String,
52	#[serde(rename = "birthStateProvince")]
53	pub birth_state_or_province: Option<String>,
54	pub birth_country: String,
55}
56
57#[serde_as]
58#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
59#[serde(rename_all = "camelCase")]
60pub struct BodyMeasurements {
61	#[serde_as(as = "DisplayFromStr")]
62	pub height: HeightMeasurement,
63	pub weight: u16,
64}
65
66#[derive(Debug, Deserialize, PartialEq, Clone)]
67#[serde(rename_all = "camelCase")]
68pub struct StrikeZone {
69	pub strike_zone_top: f64,
70	pub strike_zone_bottom: f64,
71}
72
73impl Eq for StrikeZone {}
74
75#[derive(Debug, Deref, DerefMut, Deserialize, PartialEq, Eq, Clone)]
76#[serde(rename_all = "camelCase")]
77pub struct HydratedPerson {
78	pub primary_position: Position,
79	// '? ? 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.
80	#[serde(default)]
81	pub first_name: String,
82	pub middle_name: Option<String>,
83	#[serde(default)]
84	pub last_name: String,
85	#[serde(default)]
86	#[serde(rename = "useName")]
87	pub use_first_name: String,
88	#[serde(default)]
89	pub use_last_name: String,
90	#[serde(default)]
91	pub boxscore_name: String,
92
93	pub is_player: bool,
94	#[serde(default)]
95	pub is_verified: bool,
96	pub active: bool,
97
98	#[deref]
99	#[deref_mut]
100	#[serde(flatten)]
101	inner: NamedPerson,
102}
103
104impl HydratedPerson {
105	#[must_use]
106	pub fn name_first_last(&self) -> String {
107		format!("{0} {1}", self.first_name, self.last_name)
108	}
109
110	#[must_use]
111	pub fn name_last_first(&self) -> String {
112		format!("{1}, {0}", self.first_name, self.last_name)
113	}
114
115	#[must_use]
116	pub fn name_last_first_initial(&self) -> String {
117		if let Some(char) = self.first_name.chars().next() {
118			format!("{1}, {0}", char, self.last_name)
119		} else {
120			self.last_name.clone()
121		}
122	}
123
124	#[must_use]
125	pub fn name_first_initial_last(&self) -> String {
126		if let Some(char) = self.first_name.chars().next() {
127			format!("{0} {1}", char, self.last_name)
128		} else {
129			self.last_name.clone()
130		}
131	}
132
133	#[must_use]
134	pub fn name_fml(&self) -> String {
135		if let Some(middle) = &self.middle_name {
136			format!("{0} {1} {2}", self.first_name, middle, self.last_name)
137		} else {
138			format!("{0} {1}", self.first_name, self.last_name)
139		}
140	}
141
142	#[must_use]
143	pub fn name_lfm(&self) -> String {
144		if let Some(middle) = &self.middle_name {
145			format!("{2}, {0} {1}", self.first_name, middle, self.last_name)
146		} else {
147			format!("{1}, {0}", self.first_name, self.last_name)
148		}
149	}
150}
151
152#[derive(Debug, Deserialize, Deref, DerefMut, PartialEq, Eq, Clone)]
153#[serde(rename_all = "camelCase")]
154pub struct NamedPerson {
155	pub full_name: String,
156
157	#[deref]
158	#[deref_mut]
159	#[serde(flatten)]
160	inner: IdentifiablePerson,
161}
162
163#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
164#[serde(rename_all = "camelCase")]
165pub struct IdentifiablePerson {
166	pub id: PersonId,
167}
168
169#[repr(transparent)]
170#[derive(Debug, Deserialize, Deref, Display, PartialEq, Eq, Copy, Clone, Hash)]
171pub struct PersonId(u32);
172
173impl PersonId {
174	#[must_use]
175	pub const fn new(id: u32) -> Self {
176		Self(id)
177	}
178}
179
180#[derive(Debug, Deserialize, Eq, Clone, From, EnumTryAs)]
181#[serde(untagged)]
182pub enum Person {
183	Ballplayer(Ballplayer),
184	Hydrated(HydratedPerson),
185	Named(NamedPerson),
186	Identifiable(IdentifiablePerson),
187}
188
189impl Person {
190	#[must_use]
191	pub(crate) const fn unknown_person() -> Self {
192		Self::Identifiable(IdentifiablePerson { id: PersonId::new(0) })
193	}
194}
195
196impl PartialEq for Person {
197	fn eq(&self, other: &Self) -> bool {
198		self.id == other.id
199	}
200}
201
202impl Deref for Person {
203	type Target = IdentifiablePerson;
204
205	fn deref(&self) -> &Self::Target {
206		match self {
207			Self::Ballplayer(inner) => inner,
208			Self::Hydrated(inner) => inner,
209			Self::Named(inner) => inner,
210			Self::Identifiable(inner) => inner,
211		}
212	}
213}
214
215impl DerefMut for Person {
216	fn deref_mut(&mut self) -> &mut Self::Target {
217		match self {
218			Self::Ballplayer(inner) => inner,
219			Self::Hydrated(inner) => inner,
220			Self::Named(inner) => inner,
221			Self::Identifiable(inner) => inner,
222		}
223	}
224}
225
226pub struct PersonEndpointUrl {
227	pub id: PersonId,
228}
229
230impl Display for PersonEndpointUrl {
231	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
232		write!(f, "http://statsapimlb.com/api/v1/people/{}", self.id)
233	}
234}
235
236impl StatsAPIUrl for PersonEndpointUrl {
237	type Response = PeopleResponse;
238}
239
240static CACHE: RwLock<HydratedCacheTable<Person>> = rwlock_const_new(HydratedCacheTable::new());
241
242impl EndpointEntryCache for Person {
243	type HydratedVariant = HydratedPerson;
244	type Identifier = PersonId;
245	type URL = PersonEndpointUrl;
246
247	fn into_hydrated_variant(self) -> Option<Self::HydratedVariant> {
248		self.try_as_hydrated()
249	}
250
251	fn id(&self) -> &Self::Identifier {
252		&self.id
253	}
254
255	fn url_for_id(id: &Self::Identifier) -> Self::URL {
256		PersonEndpointUrl { id: id.clone() }
257	}
258
259	fn get_entries(response: <Self::URL as StatsAPIUrl>::Response) -> impl IntoIterator<Item=Self>
260	where
261		Self: Sized
262	{
263		response.people
264	}
265
266	fn get_hydrated_cache_table() -> &'static RwLock<HydratedCacheTable<Self>>
267	where
268		Self: Sized
269	{
270		&CACHE
271	}
272}