pub mod alumni;
pub mod coaches;
pub mod leaders;
pub mod personnel;
pub mod roster;
pub mod stats;
pub mod uniforms;
pub mod history;
pub mod affiliates;
use std::fmt::{Debug, Display, Formatter};
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use bon::Builder;
use serde_with::DefaultOnError;
use crate::division::NamedDivision;
use crate::league::{LeagueId, NamedLeague};
use crate::season::SeasonId;
use crate::venue::{NamedVenue, VenueId};
use derive_more::{Deref, DerefMut};
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde_with::serde_as;
use crate::Copyright;
use crate::hydrations::Hydrations;
use crate::request::RequestURL;
use crate::sport::SportId;
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", bound = "H: TeamHydrations")]
struct __TeamRaw<H: TeamHydrations> {
#[serde(default)]
all_star_status: AllStarStatus,
active: bool,
season: u32,
#[serde(default)]
venue: Option<H::Venue>,
location_name: Option<String>,
#[serde(default, deserialize_with = "crate::try_from_str")]
first_year_of_play: Option<u32>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
league: Option<H::League>,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
division: Option<H::Division>,
sport: H::Sport,
#[serde(flatten)]
parent_organization: Option<NamedOrganization>,
#[serde(flatten)]
name: __TeamNameRaw,
spring_venue: Option<H::SpringVenue>,
spring_league: Option<LeagueId>,
#[serde(flatten)]
inner: NamedTeam,
#[serde(flatten)]
extras: H,
}
#[derive(Debug, Deserialize, Deref, DerefMut, Clone)]
#[serde(from = "__TeamRaw<H>", bound = "H: TeamHydrations")]
pub struct Team<H: TeamHydrations> {
pub all_star_status: AllStarStatus,
pub active: bool,
pub season: SeasonId,
pub venue: H::Venue,
pub location_name: Option<String>,
pub first_year_of_play: SeasonId,
pub league: H::League,
pub division: Option<H::Division>,
pub sport: H::Sport,
pub parent_organization: Option<NamedOrganization>,
pub name: TeamName,
pub spring_venue: Option<H::SpringVenue>,
pub spring_league: Option<LeagueId>,
#[deref]
#[deref_mut]
#[serde(flatten)]
inner: NamedTeam,
pub extras: H,
}
impl<H: TeamHydrations> From<__TeamRaw<H>> for Team<H> {
fn from(value: __TeamRaw<H>) -> Self {
let __TeamRaw {
all_star_status,
active,
season,
venue,
location_name,
first_year_of_play,
league,
division,
sport,
parent_organization,
name,
spring_venue,
spring_league,
inner,
extras,
} = value;
Self {
all_star_status,
active,
season: SeasonId::new(season),
venue: venue.unwrap_or_else(H::unknown_venue),
location_name,
first_year_of_play: first_year_of_play.unwrap_or(season).into(),
league: league.unwrap_or_else(H::unknown_league),
division,
sport,
parent_organization,
spring_venue,
spring_league,
name: name.initialize(inner.id, inner.full_name.clone()),
inner,
extras,
}
}
}
#[derive(Debug, Deserialize, Clone, Eq)]
#[serde(rename_all = "camelCase")]
pub struct NamedTeam {
#[serde(alias = "name")]
pub full_name: String,
#[serde(flatten)]
pub id: TeamId,
}
impl Hash for NamedTeam {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl NamedTeam {
#[must_use]
pub(crate) fn unknown_team() -> Self {
Self {
full_name: "null".to_owned(),
id: TeamId::new(0),
}
}
#[must_use]
pub fn is_unknown(&self) -> bool {
*self.id == 0
}
}
id_only_eq_impl!(NamedTeam, id);
impl<H: TeamHydrations> PartialEq for Team<H> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct __TeamNameRaw {
pub team_code: String,
pub abbreviation: String,
pub team_name: String,
pub short_name: String,
#[serde(default)]
pub file_code: Option<String>,
#[serde(default)]
pub franchise_name: Option<String>,
#[serde(default)]
pub club_name: Option<String>,
}
#[derive(Debug, PartialEq, Eq, Deref, DerefMut, Clone)]
pub struct TeamName {
pub team_code: String,
pub file_code: String,
pub abbreviation: String,
pub team_name: String,
pub short_name: String,
pub franchise_name: String,
pub club_name: String,
#[deref]
#[deref_mut]
pub full_name: String,
}
impl __TeamNameRaw {
fn initialize(self, id: TeamId, full_name: String) -> TeamName {
let Self {
team_code,
abbreviation,
team_name,
short_name,
file_code,
franchise_name,
club_name,
} = self;
TeamName {
file_code: file_code.unwrap_or_else(|| format!("t{id}")),
franchise_name: franchise_name.unwrap_or_else(|| short_name.clone()),
club_name: club_name.unwrap_or_else(|| team_name.clone()),
team_code,
abbreviation,
team_name,
short_name,
full_name,
}
}
}
id!(#[doc = "A [`u32`] representing a team's ID."] TeamId { id: u32 });
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct NamedOrganization {
#[serde(rename = "parentOrgName")]
pub name: String,
#[serde(rename = "parentOrgId")]
pub id: OrganizationId,
}
id!(#[doc = "ID of a parent organization -- still don't know what this is."] OrganizationId { id: u32 });
#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Default)]
pub enum AllStarStatus {
#[serde(rename = "Y")]
Yes,
#[default]
#[serde(rename = "N")]
No,
#[serde(rename = "F")]
F,
#[serde(rename = "O")]
O,
}
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(rename_all = "camelCase", bound = "H: TeamHydrations")]
pub struct TeamsResponse<H: TeamHydrations> {
pub copyright: Copyright,
pub teams: Vec<Team<H>>,
}
pub trait TeamHydrations: Hydrations<RequestData=()> {
type Sport: Debug + DeserializeOwned + PartialEq + Clone;
type Venue: Debug + DeserializeOwned + PartialEq + Clone;
type SpringVenue: Debug + DeserializeOwned + PartialEq + Clone;
type League: Debug + DeserializeOwned + PartialEq + Clone;
type Division: Debug + DeserializeOwned + PartialEq + Clone;
fn unknown_venue() -> Self::Venue;
fn unknown_league() -> Self::League;
}
impl TeamHydrations for () {
type Sport = SportId;
type Venue = NamedVenue;
type SpringVenue = VenueId;
type League = NamedLeague;
type Division = NamedDivision;
fn unknown_venue() -> Self::Venue {
NamedVenue::unknown_venue()
}
fn unknown_league() -> Self::League {
NamedLeague::unknown_league()
}
}
#[macro_export]
macro_rules! team_hydrations {
(@ inline_structs [previous_schedule: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::macro_use::pastey::paste! {
$crate::schedule_hydrations! {
$vis struct [<$name InlinePreviousSchedule>] {
$($inline_tt)*
}
}
$crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
$vis struct $name {
$($field_tt)*
venue: [<$name InlinePreviousSchedule>],
}
}
}
};
(@ inline_structs [next_schedule: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::macro_use::pastey::paste! {
$crate::schedule_hydrations! {
$vis struct [<$name InlineNextSchedule>] {
$($inline_tt)*
}
}
$crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
$vis struct $name {
$($field_tt)*
venue: [<$name InlineNextSchedule>],
}
}
}
};
(@ inline_structs [venue: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::macro_use::pastey::paste! {
$crate::venue_hydrations! {
$vis struct [<$name InlineVenue>] {
$($inline_tt)*
}
}
$crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
$vis struct $name {
$($field_tt)*
venue: [<$name InlineVenue>],
}
}
}
};
(@ inline_structs [spring_venue: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::macro_use::pastey::paste! {
$crate::venue_hydrations! {
$vis struct [<$name InlineSpringVenue>] {
$($inline_tt)*
}
}
$crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
$vis struct $name {
$($field_tt)*
spring_venue: [<$name InlineSpringVenue>],
}
}
}
};
(@ inline_structs [sport: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::macro_use::pastey::paste! {
$crate::sports_hydrations! {
$vis struct [<$name InlineSport>] {
$($inline_tt)*
}
}
$crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
$vis struct $name {
$($field_tt)*
sport: [<$name InlineSport>],
}
}
}
};
(@ inline_structs [standings: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::macro_use::pastey::paste! {
$crate::standings_hydrations! {
$vis struct [<$name InlineStandings>] {
$($inline_tt)*
}
}
$crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
$vis struct $name {
$($field_tt)*
standings: [<$name InlineStandings>],
}
}
}
};
(@ inline_structs [$_01:ident : { $($_02:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
::core::compile_error!("Found unknown inline struct");
};
(@ inline_structs [$field:ident $(: $value:ty)? $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
$vis struct $name {
$($field_tt)*
$field $(: $value)?,
}
}
};
(@ inline_structs [] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
$crate::team_hydrations! { @ actual
$vis struct $name {
$($field_tt)*
}
}
};
(@ sport) => { $crate::sport::SportId };
(@ sport $hydrations:ty) => { $crate::sport::Sport<$hydrations> };
(@ venue) => { $crate::venue::NamedVenue };
(@ venue $hydrations:ty) => { $crate::venue::Venue<$hydrations> };
(@ unknown_venue) => { $crate::venue::NamedVenue::unknown_venue() };
(@ unknown_venue $hydrations:ty) => { unimplemented!() };
(@ spring_venue) => { $crate::venue::VenueId };
(@ spring_venue $hydrations:ty) => { $crate::venue::Venue<$hydrations> };
(@ league) => { $crate::league::NamedLeague };
(@ league ,) => { $crate::league::League };
(@ unknown_league) => { $crate::league::NamedLeague::unknown_league() };
(@ unknown_league ,) => { unimplemented!() };
(@ division) => { $crate::division::NamedDivision };
(@ division ,) => { $crate::division::Division };
(@ actual $vis:vis struct $name:ident {
$(previous_schedule: $previous_schedule:ty ,)?
$(next_schedule: $next_schedule:ty ,)?
$(venue: $venue:ty ,)?
$(spring_venue: $spring_venue:ty ,)?
$(social $social_comma:tt)?
$(league $league_comma:tt)?
$(sport: $sport:ty ,)?
$(standings: $standings:ty ,)?
$(division $division_comma:tt)?
$(external_references $external_references_comma:tt)?
}) => {
#[derive(::core::fmt::Debug, $crate::macro_use::serde::Deserialize, ::core::cmp::PartialEq, ::core::clone::Clone)]
$vis struct $name {
$(#[serde(rename = "previousGameSchedule")] previous_schedule: $crate::schedule::ScheduleResponse<$previous_schedule>,)?
$(#[serde(rename = "nextGameSchedule")] next_schedule: $crate::schedule::ScheduleResponse<$next_schedule>,)?
$(#[serde(rename = "xrefIds")] external_references: ::std::vec::Vec<$crate::ExternalReference> $external_references_comma)?
$(#[serde(default, rename = "social")] socials: ::std::collections::HashMap<::std::string::String, ::std::vec::Vec<::std::string::String> $social_comma>)?
}
impl $crate::team::TeamHydrations for $name {
type Sport = $crate::team_hydrations!(@ sport $($sport)?);
type Venue = $crate::team_hydrations!(@ venue $($venue)?);
type SpringVenue = $crate::team_hydrations!(@ spring_venue $($spring_venue)?);
type League = $crate::team_hydrations!(@ league $($league_comma)?);
type Division = $crate::team_hydrations!(@ league $($division_comma)?);
fn unknown_venue() -> Self::Venue {
$crate::team_hydrations!(@ unknown_venue $($venue)?)
}
fn unknown_league() -> Self::League {
$crate::team_hydrations!(@ unknown_league $($league_comma)?)
}
}
impl $crate::hydrations::Hydrations for $name {
type RequestData = ();
fn hydration_text(&(): &Self::RequestData) -> ::std::borrow::Cow<'static, str> {
let text = ::std::borrow::Cow::Borrowed(::core::concat!(
$("social," $social_comma)?
$("xrefId," $external_references_comma)?
$("league," $league_comma)?
$("division," $division_comma)?
));
$(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}previousSchedule({}),", <$previous_schedule as $crate::hydrations::Hydrations>::hydration_text(&())));)?
$(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}nextSchedule({}),", <$next_schedule as $crate::hydrations::Hydrations>::hydration_text(&())));)?
$(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}venue({}),", <$venue as $crate::hydrations::Hydrations>::hydration_text(&())));)?
$(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}springVenue({}),", <$spring_venue as $crate::hydrations::Hydrations>::hydration_text(&())));)?
$(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}sport({}),", <$sport as $crate::hydrations::Hydrations>::hydration_text(&())));)?
$(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}standings({}),", <$standings as $crate::hydrations::Hydrations>::hydration_text(&())));)?
text
}
}
};
($vis:vis struct $name:ident {
$($tt:tt)*
}) => {
$crate::team_hydrations! { @ inline_structs [$($tt)*] $vis struct $name {} }
};
}
/// Returns a [`TeamsResponse`].
#[derive(Builder)]
#[builder(derive(Into))]
pub struct TeamsRequest<H: TeamHydrations> {
#[builder(into)]
sport_id: Option<SportId>,
#[builder(into)]
season: Option<SeasonId>,
#[builder(into)]
team_id: Option<TeamId>,
#[builder(skip)]
_marker: PhantomData<H>,
}
impl TeamsRequest<()> {
pub fn for_sport(sport_id: impl Into<SportId>) -> TeamsRequestBuilder<(), teams_request_builder::SetSportId> {
Self::builder().sport_id(sport_id)
}
pub fn mlb_teams() -> TeamsRequestBuilder<(), teams_request_builder::SetSportId> {
Self::for_sport(SportId::MLB)
}
pub fn all_sports() -> TeamsRequestBuilder<()> {
Self::builder()
}
}
impl<H: TeamHydrations, S: teams_request_builder::State + teams_request_builder::IsComplete> crate::request::RequestURLBuilderExt for TeamsRequestBuilder<H, S> {
type Built = TeamsRequest<H>;
}
impl<H: TeamHydrations> Display for TeamsRequest<H> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let hydrations = Some(H::hydration_text(&())).filter(|s| !s.is_empty());
write!(f, "http://statsapi.mlb.com/api/v1/teams{}", gen_params! { "sportId"?: self.sport_id, "season"?: self.season, "teamId"?: self.team_id, "hydrate"?: hydrations })
}
}
impl<H: TeamHydrations> RequestURL for TeamsRequest<H> {
type Response = TeamsResponse<H>;
}
#[cfg(test)]
mod tests {
use crate::request::RequestURLBuilderExt;
use crate::TEST_YEAR;
use super::*;
#[tokio::test]
#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
async fn parse_all_teams_all_seasons() {
for season in 1871..=TEST_YEAR {
let _response = TeamsRequest::all_sports().season(season).build_and_get().await.unwrap();
}
}
#[tokio::test]
async fn parse_all_mlb_teams_this_season() {
let _ = TeamsRequest::mlb_teams().build_and_get().await.unwrap();
}
#[tokio::test]
async fn parse_all_mlb_teams_this_season_hydrated() {
team_hydrations! {
pub struct TestHydrations {
previous_schedule: (),
next_schedule: (),
venue: (),
spring_venue: (),
social,
league,
sport: (),
standings: (),
division,
external_references,
}
}
let _ = TeamsRequest::<TestHydrations>::builder().sport_id(SportId::MLB).season(TEST_YEAR).build_and_get().await.unwrap();
}
}