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