mlb_api/endpoints/person/
mod.rs1pub 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 #[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}