1#![allow(clippy::redundant_pub_crate, reason = "re-exported as pub lol")]
4
5use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, Timelike};
6use derive_more::{Display, FromStr, Not};
7use serde::de::Error;
8use serde::{Deserialize, Deserializer};
9use std::fmt::{Debug, Display, Formatter};
10use std::num::{ParseFloatError, ParseIntError};
11use std::ops::{Add, RangeInclusive};
12use std::str::FromStr;
13use thiserror::Error;
14use crate::season::SeasonId;
15
16#[derive(Debug, Deserialize, PartialEq, Clone)]
18#[serde(from = "__CopyrightStruct")]
19pub enum Copyright {
20 Typical {
22 year: u32,
24 },
25 UnknownSpec(Box<str>),
27}
28
29#[derive(Deserialize)]
30#[doc(hidden)]
31struct __CopyrightStruct(String);
32
33impl From<__CopyrightStruct> for Copyright {
34 fn from(value: __CopyrightStruct) -> Self {
35 let __CopyrightStruct(value) = value;
36 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>() {
37 Self::Typical { year }
38 } else {
39 Self::UnknownSpec(value.into_boxed_str())
40 }
41 }
42}
43
44impl Display for Copyright {
45 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
46 match self {
47 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"),
48 Self::UnknownSpec(copyright) => write!(f, "{copyright}"),
49 }
50 }
51}
52
53impl Default for Copyright {
54 #[allow(clippy::cast_sign_loss, reason = "jesus is not alive")]
55 fn default() -> Self {
56 Self::Typical { year: Local::now().year() as _ }
57 }
58}
59
60pub fn try_from_str<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<Option<T>, D::Error> {
64 Ok(String::deserialize(deserializer)?.parse::<T>().ok())
65}
66
67pub fn from_str<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<T, D::Error>
73where
74 <T as FromStr>::Err: Debug,
75{
76 String::deserialize(deserializer)?.parse::<T>().map_err(|e| Error::custom(format!("{e:?}")))
77}
78
79pub fn from_yes_no<'de, D: Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
84 #[derive(Deserialize)]
85 #[repr(u8)]
86 enum Boolean {
87 #[serde(rename = "Y")]
88 Yes = 1,
89 #[serde(rename = "N")]
90 No = 0,
91 }
92
93 Ok(match Boolean::deserialize(deserializer)? {
94 Boolean::Yes => true,
95 Boolean::No => false,
96 })
97}
98
99#[derive(Debug, PartialEq, Copy, Clone)]
103pub enum HeightMeasurement {
104 FeetAndInches { feet: u8, inches: u8 },
106 Centimeters { cm: u16 },
108}
109
110impl FromStr for HeightMeasurement {
111 type Err = HeightMeasurementParseError;
112
113 fn from_str(s: &str) -> Result<Self, Self::Err> {
117 if let Some((feet, Some((inches, "")))) = s.split_once("' ").map(|(feet, rest)| (feet, rest.split_once('"'))) {
118 let feet = feet.parse::<u8>()?;
119 let inches = inches.parse::<u8>()?;
120 Ok(Self::FeetAndInches { feet, inches })
121 } else if let Some((cm, "")) = s.split_once("cm") {
122 let cm = cm.parse::<u16>()?;
123 Ok(Self::Centimeters { cm })
124 } else {
125 Err(HeightMeasurementParseError::UnknownSpec(s.to_owned()))
126 }
127 }
128}
129
130impl<'de> Deserialize<'de> for HeightMeasurement {
131 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
132 where
133 D: Deserializer<'de>
134 {
135 String::deserialize(deserializer)?.parse().map_err(D::Error::custom)
136 }
137}
138
139#[derive(Debug, Error)]
141pub enum HeightMeasurementParseError {
142 #[error(transparent)]
144 ParseIntError(#[from] ParseIntError),
145 #[error("Unknown height '{0}'")]
147 UnknownSpec(String),
148}
149
150#[derive(Debug, Display, PartialEq, Copy, Clone, Default)]
152pub enum PlayerPool {
153 #[default]
155 #[display("ALL")]
156 All,
157 #[display("QUALIFIED")]
159 Qualified,
160 #[display("ROOKIES")]
162 Rookies,
163 #[display("QUALIFIED_ROOKIES")]
165 QualifiedAndRookies,
166 #[display("ORGANIZATION")]
168 Organization,
169 #[display("ORGANIZATION_NO_MLB")]
171 OrganizationNotMlb,
172 #[display("CURRENT")]
174 Current,
175 #[display("ALL_CURRENT")]
177 AllCurrent,
178 #[display("QUALIFIED_CURRENT")]
180 QualifiedAndCurrent,
181}
182
183#[derive(Debug, Deserialize, PartialEq, Copy, Clone, Default)]
187pub enum Gender {
188 #[serde(rename = "M")]
189 Male,
190 #[serde(rename = "F")]
191 Female,
192 #[default]
193 #[serde(other)]
194 Other,
195}
196
197#[derive(Debug, Deserialize, PartialEq, Copy, Clone)]
201#[serde(try_from = "__HandednessStruct")]
202pub enum Handedness {
203 Left,
204 Right,
205 Switch,
206}
207
208#[derive(Deserialize)]
209#[doc(hidden)]
210struct __HandednessStruct {
211 code: String,
212}
213
214#[derive(Debug, Error)]
216pub enum HandednessParseError {
217 #[error("Invalid handedness '{0}'")]
219 InvalidHandedness(String),
220}
221
222impl TryFrom<__HandednessStruct> for Handedness {
223 type Error = HandednessParseError;
224
225 fn try_from(value: __HandednessStruct) -> Result<Self, Self::Error> {
226 Ok(match &*value.code {
227 "L" => Self::Left,
228 "R" => Self::Right,
229 "S" => Self::Switch,
230 _ => return Err(HandednessParseError::InvalidHandedness(value.code)),
231 })
232 }
233}
234
235pub type NaiveDateRange = RangeInclusive<NaiveDate>;
242
243pub(crate) const MLB_API_DATE_FORMAT: &str = "%m/%d/%Y";
244
245pub(crate) fn deserialize_datetime<'de, D: Deserializer<'de>>(deserializer: D) -> Result<NaiveDateTime, D::Error> {
249 let string = String::deserialize(deserializer)?;
250 let fmt = match (string.ends_with('Z'), string.contains('.')) {
251 (false, false) => "%FT%TZ%#z",
252 (false, true) => "%FT%TZ%.3f%#z",
253 (true, false) => "%FT%TZ",
254 (true, true) => "%FT%T%.3fZ",
255 };
256 NaiveDateTime::parse_from_str(&string, fmt).map_err(D::Error::custom)
257}
258
259pub(crate) fn deserialize_comma_separated_vec<'de, D: Deserializer<'de>, T: FromStr>(deserializer: D) -> Result<Vec<T>, D::Error>
263where
264 <T as FromStr>::Err: Debug,
265{
266 String::deserialize(deserializer)?
267 .split(", ")
268 .map(|entry| T::from_str(entry))
269 .collect::<Result<Vec<T>, <T as FromStr>::Err>>()
270 .map_err(|e| Error::custom(format!("{e:?}")))
271}
272
273#[derive(Debug, PartialEq, Eq, Copy, Clone, Not, Default)]
274pub enum TeamSide {
275 #[default]
276 Home,
277 Away,
278}
279
280impl TeamSide {
281 #[must_use]
282 pub const fn is_home(self) -> bool {
283 matches!(self, Self::Home)
284 }
285
286 #[must_use]
287 pub const fn is_away(self) -> bool {
288 matches!(self, Self::Away)
289 }
290}
291
292pub fn deserialize_team_side_from_is_home<'de, D: Deserializer<'de>>(deserializer: D) -> Result<TeamSide, D::Error> {
293 Ok(match bool::deserialize(deserializer)? {
294 true => TeamSide::Home,
295 false => TeamSide::Away,
296 })
297}
298
299#[derive(Debug, Deserialize, PartialEq, Copy, Clone, Default)]
309pub struct HomeAway<T> {
310 pub home: T,
311 pub away: T,
312}
313
314impl<T> HomeAway<T> {
315 #[must_use]
317 pub const fn new(home: T, away: T) -> Self {
318 Self { home, away }
319 }
320
321 #[must_use]
323 pub fn choose(self, side: TeamSide) -> T {
324 match side {
325 TeamSide::Home => self.home,
326 TeamSide::Away => self.away,
327 }
328 }
329
330 #[must_use]
331 pub fn as_ref(&self) -> HomeAway<&T> {
332 HomeAway {
333 home: &self.home,
334 away: &self.away,
335 }
336 }
337
338 #[must_use]
339 pub fn as_mut(&mut self) -> HomeAway<&mut T> {
340 HomeAway {
341 home: &mut self.home,
342 away: &mut self.away,
343 }
344 }
345
346 #[must_use]
347 pub fn map<U, F: FnMut(T) -> U>(self, mut f: F) -> HomeAway<U> {
348 HomeAway {
349 home: f(self.home),
350 away: f(self.away),
351 }
352 }
353
354 #[must_use]
356 pub fn swap(self) -> Self {
357 Self {
358 home: self.away,
359 away: self.home,
360 }
361 }
362
363 #[must_use]
364 pub fn combine<U, V, F: FnMut(T, U) -> V>(self, other: HomeAway<U>, mut f: F) -> HomeAway<V> {
365 HomeAway {
366 home: f(self.home, other.home),
367 away: f(self.away, other.away),
368 }
369 }
370
371 #[must_use]
373 pub fn added(self) -> <T as Add>::Output where T: Add {
374 self.home + self.away
375 }
376}
377
378impl<T> From<(T, T)> for HomeAway<T> {
379 fn from((home, away): (T, T)) -> Self {
380 Self {
381 home,
382 away
383 }
384 }
385}
386
387#[derive(Debug, Deserialize, PartialEq, Clone, Default)]
391#[serde(rename_all = "camelCase")]
392pub struct Location {
393 pub address_line_1: Option<String>,
394 pub address_line_2: Option<String>,
395 pub address_line_3: Option<String>,
396 pub address_line_4: Option<String>,
397 pub attention: Option<String>,
398 #[serde(alias = "phone")]
399 pub phone_number: Option<String>,
400 pub city: Option<String>,
401 #[serde(alias = "province")]
402 pub state: Option<String>,
403 pub country: Option<String>,
404 #[serde(rename = "stateAbbrev")] pub state_abbreviation: Option<String>,
405 pub postal_code: Option<String>,
406 pub latitude: Option<f64>,
407 pub longitude: Option<f64>,
408 pub azimuth_angle: Option<f64>,
409 pub elevation: Option<u32>,
410}
411
412#[derive(Debug, Deserialize, PartialEq, Clone)]
414#[serde(rename_all = "camelCase")]
415pub struct FieldInfo {
416 pub capacity: u32,
417 pub turf_type: TurfType,
418 pub roof_type: RoofType,
419 pub left_line: Option<u32>,
420 pub left: Option<u32>,
421 pub left_center: Option<u32>,
422 pub center: Option<u32>,
423 pub right_center: Option<u32>,
424 pub right: Option<u32>,
425 pub right_line: Option<u32>,
426}
427
428#[derive(Debug, Deserialize, PartialEq, Clone, Display)]
430pub enum TurfType {
431 #[serde(rename = "Artificial Turf")]
432 #[display("Artificial Turf")]
433 ArtificialTurf,
434
435 #[serde(rename = "Grass")]
436 #[display("Grass")]
437 Grass,
438}
439
440#[derive(Debug, Deserialize, PartialEq, Clone, Display)]
442pub enum RoofType {
443 #[serde(rename = "Retractable")]
444 #[display("Retractable")]
445 Retractable,
446
447 #[serde(rename = "Open")]
448 #[display("Open")]
449 Open,
450
451 #[serde(rename = "Dome")]
452 #[display("Dome")]
453 Dome,
454}
455
456#[derive(Debug, Deserialize, PartialEq, Clone)]
458#[serde(rename_all = "camelCase")]
459pub struct TimeZoneData {
460 #[serde(rename = "id")]
461 pub timezone: chrono_tz::Tz,
462 pub offset: i32,
463 pub offset_at_game_time: i32,
464}
465
466#[derive(Debug, Deserialize, PartialEq, Clone)]
468pub struct ExternalReference {
469 #[serde(rename = "xrefId")]
470 pub id: String,
471 #[serde(rename = "xrefType")]
472 pub xref_type: String,
473 pub season: Option<SeasonId>,
474}
475
476#[derive(Debug, Deserialize, PartialEq, Clone)]
478#[serde(rename_all = "camelCase")]
479pub struct TrackingSystem {
480 pub id: TrackingSystemVendorId,
481 pub description: String,
482 pub pitch_vendor: Option<TrackingSystemVendor>,
483 pub hit_vendor: Option<TrackingSystemVendor>,
484 pub player_vendor: Option<TrackingSystemVendor>,
485 pub skeletal_vendor: Option<TrackingSystemVendor>,
486 pub bat_vendor: Option<TrackingSystemVendor>,
487 pub biomechanics_vendor: Option<TrackingSystemVendor>,
488}
489
490id!(TrackingSystemVendorId { id: u32 });
491
492#[derive(Debug, Deserialize, PartialEq, Clone)]
494pub struct TrackingSystemVendor {
495 pub id: TrackingSystemVendorId,
496 pub description: String,
497 #[serde(rename = "version")]
498 pub details: String,
499}
500
501#[derive(Debug, Copy, Clone)]
505pub enum IntegerOrFloatStat {
506 Integer(i64),
508 Float(f64),
510}
511
512impl PartialEq for IntegerOrFloatStat {
513 fn eq(&self, other: &Self) -> bool {
514 match (*self, *other) {
515 (Self::Integer(lhs), Self::Integer(rhs)) => lhs == rhs,
516 (Self::Float(lhs), Self::Float(rhs)) => lhs == rhs,
517
518 #[allow(clippy::cast_precision_loss, reason = "we checked if it's perfectly representable")]
519 #[allow(clippy::cast_possible_truncation, reason = "we checked if it's perfectly representable")]
520 (Self::Integer(int), Self::Float(float)) | (Self::Float(float), Self::Integer(int)) => {
521 if float.is_normal() && float.floor() == float && (i64::MIN as f64..-(i64::MIN as f64)).contains(&float) {
524 float as i64 == int
525 } else {
526 false
527 }
528 },
529 }
530 }
531}
532
533impl<'de> Deserialize<'de> for IntegerOrFloatStat {
534 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
535 where
536 D: Deserializer<'de>
537 {
538 struct Visitor;
539
540 impl serde::de::Visitor<'_> for Visitor {
541 type Value = IntegerOrFloatStat;
542
543 fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
544 formatter.write_str("integer, float, or string that can be parsed to either")
545 }
546
547 fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
548 where
549 E: Error,
550 {
551 Ok(IntegerOrFloatStat::Integer(v))
552 }
553
554 fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
555 where
556 E: Error,
557 {
558 Ok(IntegerOrFloatStat::Float(v))
559 }
560
561 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
562 where
563 E: Error,
564 {
565 if v == "-.--" || v == ".---" {
566 Ok(IntegerOrFloatStat::Float(0.0))
567 } else if let Ok(i) = v.parse::<i64>() {
568 Ok(IntegerOrFloatStat::Integer(i))
569 } else if let Ok(f) = v.parse::<f64>() {
570 Ok(IntegerOrFloatStat::Float(f))
571 } else {
572 Err(E::invalid_value(serde::de::Unexpected::Str(v), &self))
573 }
574 }
575 }
576
577 deserializer.deserialize_any(Visitor)
578 }
579}
580
581#[derive(Debug, Deserialize, Display)]
585#[display("An error occurred parsing the statsapi http request: {message}")]
586pub struct MLBError {
587 pub(crate) message: String,
588}
589
590impl std::error::Error for MLBError {}
591
592#[derive(Debug, Deserialize, PartialEq, Copy, Clone, Default)]
594#[serde(try_from = "&str")]
595pub struct RGBAColor {
596 pub red: u8,
597 pub green: u8,
598 pub blue: u8,
599 pub alpha: u8,
600}
601
602impl Display for RGBAColor {
603 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
604 write!(f, "0x{:02x}{:02x}{:02x}{:02x}", self.alpha, self.red, self.green, self.blue)
605 }
606}
607
608#[derive(Debug, Error)]
610pub enum RGBAColorFromStrError {
611 #[error("Invalid spec")]
612 InvalidFormat,
613 #[error(transparent)]
614 InvalidInt(#[from] ParseIntError),
615 #[error(transparent)]
616 InvalidFloat(#[from] ParseFloatError),
617}
618
619impl<'a> TryFrom<&'a str> for RGBAColor {
620 type Error = <Self as FromStr>::Err;
621
622 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
623 <Self as FromStr>::from_str(value)
624 }
625}
626
627impl FromStr for RGBAColor {
628 type Err = RGBAColorFromStrError;
629
630 #[allow(clippy::single_char_pattern, reason = "other patterns are strings, the choice to make that one a char does not denote any special case")]
632 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "intended behaviour with alpha channel")]
633 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
634 s = s.strip_suffix("rgba(").ok_or(Self::Err::InvalidFormat)?;
635 let (red, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
636 let red = red.parse::<u8>()?;
637 let (green, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
638 let green = green.parse::<u8>()?;
639 let (blue, s) = s.split_once(", ").ok_or(Self::Err::InvalidFormat)?;
640 let blue = blue.parse::<u8>()?;
641 let (alpha, s) = s.split_once(")").ok_or(Self::Err::InvalidFormat)?;
642 let alpha = (alpha.parse::<f32>()? * 255.0).round() as u8;
643 if !s.is_empty() { return Err(Self::Err::InvalidFormat); }
644 Ok(Self {
645 red,
646 green,
647 blue,
648 alpha
649 })
650 }
651}
652
653#[derive(Debug, Deserialize, PartialEq, Copy, Clone, Display, FromStr)]
655#[serde(try_from = "&str")]
656pub enum HeatmapTemperature {
657 Hot,
658 Warm,
659 Lukewarm,
660 Cool,
661 Cold,
662}
663
664impl<'a> TryFrom<&'a str> for HeatmapTemperature {
665 type Error = <Self as FromStr>::Err;
666
667 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
668 <Self as FromStr>::from_str(value)
669 }
670}
671
672pub(crate) fn write_nth(n: usize, f: &mut Formatter<'_>) -> std::fmt::Result {
673 write!(f, "{n}")?;
674 let (tens, ones) = (n / 10, n % 10);
675 let is_teen = (tens % 10) == 1;
676 if is_teen {
677 write!(f, "th")?;
678 } else {
679 write!(f, "{}", match ones {
680 1 => "st",
681 2 => "nd",
682 3 => "rd",
683 _ => "th",
684 })?;
685 }
686 Ok(())
687}
688
689pub fn deserialize_time_delta_from_hms<'de, D: Deserializer<'de>>(deserializer: D) -> Result<TimeDelta, D::Error> {
692 let string = String::deserialize(deserializer)?;
693 let (hour, rest) = string.split_once(':').ok_or_else(|| D::Error::custom("Unable to find `:`"))?;
694 let (minute, second) = rest.split_once(':').ok_or_else(|| D::Error::custom("Unable to find `:`"))?;
695 let hour = hour.parse::<u32>().map_err(D::Error::custom)?;
696 let minute = minute.parse::<u32>().map_err(D::Error::custom)?;
697 let second = second.parse::<u32>().map_err(D::Error::custom)?;
698
699 TimeDelta::new(((hour * 24 + minute) * 60 + second) as _, 0).ok_or_else(|| D::Error::custom("Invalid time quantity, overflow."))
700}
701
702#[derive(Debug, Deserialize, PartialEq, Copy, Clone, Display, FromStr)]
704#[serde(try_from = "&str")]
705pub enum DayHalf {
706 AM,
707 PM,
708}
709
710impl DayHalf {
711 #[must_use]
713 pub fn into_24_hour_time(self, mut time: NaiveTime) -> NaiveTime {
714 if (self == Self::PM) ^ (time.hour() == 12) {
715 time += TimeDelta::hours(12);
716 }
717
718 time
719 }
720}
721
722impl<'a> TryFrom<&'a str> for DayHalf {
723 type Error = <Self as FromStr>::Err;
724
725 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
726 <Self as FromStr>::from_str(value)
727 }
728}
729
730#[derive(Debug, Deserialize, PartialEq, Clone)]
731#[serde(rename_all = "camelCase")]
732pub struct ResourceUsage {
733 pub used: u32,
734 pub remaining: u32,
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740
741 #[test]
742 fn test_ampm() {
743 assert_eq!(NaiveTime::from_hms_opt(0, 0, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap()));
744 assert_eq!(NaiveTime::from_hms_opt(12, 0, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap()));
745 assert_eq!(NaiveTime::from_hms_opt(0, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
746 assert_eq!(NaiveTime::from_hms_opt(12, 1, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
747 assert_eq!(NaiveTime::from_hms_opt(0, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(12, 1, 0).unwrap()));
748 assert_eq!(NaiveTime::from_hms_opt(23, 59, 0).unwrap(), DayHalf::PM.into_24_hour_time(NaiveTime::from_hms_opt(11, 59, 0).unwrap()));
749 assert_eq!(NaiveTime::from_hms_opt(1, 1, 0).unwrap(), DayHalf::AM.into_24_hour_time(NaiveTime::from_hms_opt(1, 1, 0).unwrap()));
750 }
751}