1use chrono::{Datelike, Local, NaiveDate};
2use derive_more::Display;
3use serde::de::Error;
4use serde::{Deserialize, Deserializer};
5use std::fmt::{Debug, Display, Formatter};
6use std::num::ParseIntError;
7use std::ops::Add;
8use std::str::FromStr;
9use compact_str::CompactString;
10use thiserror::Error;
11
12#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
14#[serde(from = "__CopyrightStruct")]
15pub enum Copyright {
16 Typical {
17 year: u32,
18 },
19 UnknownSpec(CompactString),
20}
21
22#[derive(Deserialize)]
23struct __CopyrightStruct(String);
24
25impl From<__CopyrightStruct> for Copyright {
26 fn from(value: __CopyrightStruct) -> Self {
27 let __CopyrightStruct(value) = value;
28 if let Some(value) = value.strip_prefix("Copyright ") && let Some(value) = value.strip_suffix(" MLB Advanced Media, L.P. Use of any content on this page acknowledges agreement to the terms posted here http://gdx.mlb.com/components/copyright.txt") && let Ok(year) = value.parse::<u32>() {
29 Self::Typical { year }
30 } else {
31 Self::UnknownSpec(CompactString::from(value))
32 }
33 }
34}
35
36impl Display for Copyright {
37 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
38 match self {
39 Self::Typical { year } => write!(f, "Copyright {year} MLB Advanced Media, L.P. Use of any content on this page acknowledges agreement to the terms posted here http://gdx.mlb.com/components/copyright.txt"),
40 Self::UnknownSpec(copyright) => write!(f, "{copyright}"),
41 }
42 }
43}
44
45impl Default for Copyright {
46 fn default() -> Self {
47 Self::Typical { year: Local::now().year() as _ }
48 }
49}
50
51pub fn try_from_str<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<Option<T>, D::Error> {
52 Ok(String::deserialize(deserializer)?.parse::<T>().ok())
53}
54
55pub fn from_str<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<T, D::Error>
56where
57 <T as FromStr>::Err: Debug,
58{
59 String::deserialize(deserializer)?.parse::<T>().map_err(|e| Error::custom(format!("{e:?}")))
60}
61
62pub fn from_yes_no<'de, D: Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
63 #[derive(Deserialize)]
64 enum Boolean {
65 #[serde(rename = "Y")]
66 Yes,
67 #[serde(rename = "N")]
68 No,
69 }
70
71 Ok(match Boolean::deserialize(deserializer)? {
72 Boolean::Yes => true,
73 Boolean::No => false,
74 })
75}
76
77#[derive(Debug, PartialEq, Eq, Copy, Clone)]
78pub enum HeightMeasurement {
79 FeetAndInches { feet: u8, inches: u8 },
80 Centimeters { cm: u16 },
81}
82
83impl FromStr for HeightMeasurement {
84 type Err = HeightMeasurementParseError;
85
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 match s.split_once("' ").map(|(feet, rest)| (feet, rest.split_once(r#"""#))) {
88 Some((feet, Some((inches, "")))) => {
89 let feet = feet.parse::<u8>()?;
90 let inches = inches.parse::<u8>()?;
91 Ok(Self::FeetAndInches { feet, inches })
92 }
93 _ => match s.split_once("cm") {
94 Some((cm, "")) => {
95 let cm = cm.parse::<u16>()?;
96 Ok(Self::Centimeters { cm })
97 }
98 _ => Err(HeightMeasurementParseError::UnknownSpec(s.to_owned())),
99 },
100 }
101 }
102}
103
104#[derive(Debug, Error)]
105pub enum HeightMeasurementParseError {
106 #[error(transparent)]
107 ParseIntError(#[from] ParseIntError),
108 #[error("Unknown height '{0}'")]
109 UnknownSpec(String),
110}
111
112#[derive(Debug, Display, PartialEq, Eq, Copy, Clone, Default)]
113pub enum PlayerPool {
114 #[default]
115 #[display("ALL")]
116 All,
117 #[display("QUALIFIED")]
118 Qualified,
119 #[display("ROOKIES")]
120 Rookies,
121 #[display("QUALIFIED_ROOKIES")]
122 QualifiedAndRookies,
123 #[display("ORGANIZATION")]
124 Organization,
125 #[display("ORGANIZATION_NO_MLB")]
126 OrganizationNotMlb,
127 #[display("CURRENT")]
128 Current,
129 #[display("ALL_CURRENT")]
130 AllCurrent,
131 #[display("QUALIFIED_CURRENT")]
132 QualifiedAndCurrent,
133}
134
135#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
136pub enum Gender {
137 #[serde(rename = "M")]
138 Male,
139 #[serde(rename = "F")]
140 Female,
141 #[serde(other)]
142 Other,
143}
144
145#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
146#[serde(try_from = "__HandednessStruct")]
147pub enum Handedness {
148 Left,
149 Right,
150 Switch,
151}
152
153#[derive(Deserialize)]
154struct __HandednessStruct {
155 code: String,
156}
157
158#[derive(Debug, Error)]
159pub enum HandednessParseError {
160 #[error("Invalid handedness '{0}'")]
161 InvalidHandedness(String),
162}
163
164impl TryFrom<__HandednessStruct> for Handedness {
165 type Error = HandednessParseError;
166
167 fn try_from(value: __HandednessStruct) -> Result<Self, Self::Error> {
168 Ok(match &*value.code {
169 "L" => Self::Left,
170 "R" => Self::Right,
171 "S" => Self::Switch,
172 _ => return Err(HandednessParseError::InvalidHandedness(value.code)),
173 })
174 }
175}
176
177pub type NaiveDateRange = std::ops::RangeInclusive<NaiveDate>;
178
179pub(crate) const MLB_API_DATE_FORMAT: &str = "%m/%d/%Y";
180
181pub fn deserialize_comma_seperated_vec<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<Vec<T>, D::Error>
182where
183 <T as FromStr>::Err: Debug,
184{
185 String::deserialize(deserializer)?
186 .split(", ")
187 .map(|entry| T::from_str(entry))
188 .collect::<Result<Vec<T>, <T as FromStr>::Err>>()
189 .map_err(|e| Error::custom(format!("{e:?}")))
190}
191
192#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone)]
193pub struct HomeAwaySplits<T> {
194 pub home: T,
195 pub away: T,
196}
197
198impl<T> HomeAwaySplits<T> {
199 #[must_use]
200 pub const fn new(home: T, away: T) -> Self {
201 Self { home, away }
202 }
203}
204
205impl<T: Add> HomeAwaySplits<T> {
206 #[must_use]
207 pub fn combined(self) -> <T as Add>::Output {
208 self.home + self.away
209 }
210}
211
212#[derive(Debug, Deserialize, PartialEq, Clone)]
213#[serde(rename_all = "camelCase")]
214pub struct Location {
215 pub address_line_1: Option<String>,
216 pub address_line_2: Option<String>,
217 pub address_line_3: Option<String>,
218 pub address_line_4: Option<String>,
219 pub attention: Option<String>,
220 pub phone_number: Option<String>,
221 pub city: Option<String>,
222 pub state: Option<String>,
223 pub country: Option<String>,
224 #[serde(rename = "stateAbbrev")] pub state_abbreviation: Option<String>,
225 pub postal_code: Option<String>,
226 pub latitude: Option<f64>,
227 pub longitude: Option<f64>,
228 pub azimuth_angle: Option<f64>,
229 pub elevation: Option<u32>,
230}
231
232impl Eq for Location {}
233
234#[derive(Debug, Copy, Clone)]
235pub enum IntegerOrFloatStat {
236 Integer(i64),
237 Float(f64),
238}
239
240impl PartialEq for IntegerOrFloatStat {
241 fn eq(&self, other: &Self) -> bool {
242 match (*self, *other) {
243 (Self::Integer(lhs), Self::Integer(rhs)) => lhs == rhs,
244 (Self::Float(lhs), Self::Float(rhs)) => lhs == rhs,
245
246 (Self::Integer(int), Self::Float(float)) | (Self::Float(float), Self::Integer(int)) => {
247 if float.floor() == float && (i64::MIN as f64..=i64::MAX as f64).contains(&float) {
249 float as i64 == int
250 } else {
251 false
252 }
253 },
254 }
255 }
256}
257
258impl Eq for IntegerOrFloatStat {}
259
260impl<'de> Deserialize<'de> for IntegerOrFloatStat {
261 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
262 where
263 D: Deserializer<'de>
264 {
265 struct Visitor;
266
267 impl<'de> serde::de::Visitor<'de> for Visitor {
268 type Value = IntegerOrFloatStat;
269
270 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
271 formatter.write_str("integer, float, or string that can be parsed to either")
272 }
273
274 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
275 where
276 E: Error,
277 {
278 if v == "-.--" || v == ".---" {
279 Ok(IntegerOrFloatStat::Float(0.0))
280 } else if let Ok(i) = v.parse::<i64>() {
281 Ok(IntegerOrFloatStat::Integer(i))
282 } else if let Ok(f) = v.parse::<f64>() {
283 Ok(IntegerOrFloatStat::Float(f))
284 } else {
285 Err(E::invalid_value(serde::de::Unexpected::Str(v), &self))
286 }
287 }
288
289 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
290 where
291 E: Error,
292 {
293 Ok(IntegerOrFloatStat::Integer(v))
294 }
295
296 fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
297 where
298 E: Error,
299 {
300 Ok(IntegerOrFloatStat::Float(v))
301 }
302 }
303
304 deserializer.deserialize_any(Visitor)
305 }
306}
307
308#[derive(Debug, Deserialize, Display)]
309#[display("An error occurred parsing the statsapi http request: {message}")]
310pub struct StatsAPIError {
311 message: String,
312}
313
314impl std::error::Error for StatsAPIError {}