mlb_api/
types.rs

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/// Shared types across multiple endpoints
13#[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				// fast way to check if the float is representable perfectly as an integer and if it's within range of `i64`
248				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 {}