1pub mod alumni;
4pub mod coaches;
5pub mod leaders;
6pub mod personnel;
7pub mod roster;
8pub mod stats;
9pub mod uniforms;
10pub mod history;
11pub mod affiliates;
12
13use std::fmt::{Debug, Display, Formatter};
14use std::hash::{Hash, Hasher};
15use std::marker::PhantomData;
16use bon::Builder;
17use serde_with::DefaultOnError;
18use crate::division::NamedDivision;
19use crate::league::{LeagueId, NamedLeague};
20use crate::season::SeasonId;
21use crate::venue::{NamedVenue, VenueId};
22use derive_more::{Deref, DerefMut};
23use serde::de::DeserializeOwned;
24use serde::Deserialize;
25use serde_with::serde_as;
26use crate::Copyright;
27use crate::hydrations::Hydrations;
28use crate::request::RequestURL;
29use crate::sport::SportId;
30
31#[serde_as]
32#[derive(Deserialize)]
33#[serde(rename_all = "camelCase", bound = "H: TeamHydrations")]
34struct __TeamRaw<H: TeamHydrations> {
35 #[serde(default)]
36 all_star_status: AllStarStatus,
37 active: bool,
38 season: u32,
39 #[serde(default)]
40 venue: Option<H::Venue>,
41 location_name: Option<String>,
42 #[serde(default, deserialize_with = "crate::try_from_str")]
43 first_year_of_play: Option<u32>,
44 #[serde(default)]
45 #[serde_as(deserialize_as = "DefaultOnError")]
46 league: Option<H::League>,
47 #[serde(default)]
48 #[serde_as(deserialize_as = "DefaultOnError")]
49 division: Option<H::Division>,
50 sport: H::Sport,
51 #[serde(flatten)]
52 parent_organization: Option<NamedOrganization>,
53 #[serde(flatten)]
54 name: __TeamNameRaw,
55 spring_venue: Option<H::SpringVenue>,
56 spring_league: Option<LeagueId>,
57 #[serde(flatten)]
58 inner: NamedTeam,
59 #[serde(flatten)]
60 extras: H,
61}
62
63#[derive(Debug, Deserialize, Deref, DerefMut, Clone)]
94#[serde(from = "__TeamRaw<H>", bound = "H: TeamHydrations")]
95pub struct Team<H: TeamHydrations> {
96 pub all_star_status: AllStarStatus,
97 pub active: bool,
98 pub season: SeasonId,
99 pub venue: H::Venue,
100 pub location_name: Option<String>,
101 pub first_year_of_play: SeasonId,
102 pub league: H::League,
103 pub division: Option<H::Division>,
104 pub sport: H::Sport,
105 pub parent_organization: Option<NamedOrganization>,
106 pub name: TeamName,
107 pub spring_venue: Option<H::SpringVenue>,
108 pub spring_league: Option<LeagueId>,
109
110 #[deref]
111 #[deref_mut]
112 #[serde(flatten)]
113 inner: NamedTeam,
114
115 pub extras: H,
116}
117
118impl<H: TeamHydrations> From<__TeamRaw<H>> for Team<H> {
119 fn from(value: __TeamRaw<H>) -> Self {
120 let __TeamRaw {
121 all_star_status,
122 active,
123 season,
124 venue,
125 location_name,
126 first_year_of_play,
127 league,
128 division,
129 sport,
130 parent_organization,
131 name,
132 spring_venue,
133 spring_league,
134 inner,
135 extras,
136 } = value;
137
138 Self {
139 all_star_status,
140 active,
141 season: SeasonId::new(season),
142 venue: venue.unwrap_or_else(H::unknown_venue),
143 location_name,
144 first_year_of_play: first_year_of_play.unwrap_or(season).into(),
145 league: league.unwrap_or_else(H::unknown_league),
146 division,
147 sport,
148 parent_organization,
149 spring_venue,
150 spring_league,
151 name: name.initialize(inner.id, inner.full_name.clone()),
152 inner,
153 extras,
154 }
155 }
156}
157
158#[derive(Debug, Deserialize, Clone, Eq)]
170#[serde(rename_all = "camelCase")]
171pub struct NamedTeam {
172 #[serde(alias = "name")]
173 pub full_name: String,
174 #[serde(flatten)]
175 pub id: TeamId,
176}
177
178
179impl Hash for NamedTeam {
180 fn hash<H: Hasher>(&self, state: &mut H) {
181 self.id.hash(state);
182 }
183}
184
185impl NamedTeam {
186 #[must_use]
187 pub(crate) fn unknown_team() -> Self {
188 Self {
189 full_name: "null".to_owned(),
190 id: TeamId::new(0),
191 }
192 }
193
194 #[must_use]
195 pub fn is_unknown(&self) -> bool {
196 *self.id == 0
197 }
198}
199
200id_only_eq_impl!(NamedTeam, id);
201
202impl<H: TeamHydrations> PartialEq for Team<H> {
203 fn eq(&self, other: &Self) -> bool {
204 self.id == other.id
205 }
206}
207
208#[derive(Deserialize)]
209#[serde(rename_all = "camelCase")]
210struct __TeamNameRaw {
211 pub team_code: String,
212 pub abbreviation: String,
213 pub team_name: String,
214 pub short_name: String,
215 #[serde(default)]
216 pub file_code: Option<String>,
217 #[serde(default)]
218 pub franchise_name: Option<String>,
219 #[serde(default)]
220 pub club_name: Option<String>,
221}
222
223#[derive(Debug, PartialEq, Deref, DerefMut, Clone)]
259pub struct TeamName {
260 pub team_code: String,
262 pub file_code: String,
263 pub abbreviation: String,
264 pub team_name: String,
265 pub short_name: String,
267 pub franchise_name: String,
268 pub club_name: String,
269 #[deref]
270 #[deref_mut]
271 pub full_name: String,
272}
273
274impl __TeamNameRaw {
275 fn initialize(self, id: TeamId, full_name: String) -> TeamName {
276 let Self {
277 team_code,
278 abbreviation,
279 team_name,
280 short_name,
281 file_code,
282 franchise_name,
283 club_name,
284 } = self;
285
286
287 TeamName {
288 file_code: file_code.unwrap_or_else(|| format!("t{id}")),
289 franchise_name: franchise_name.unwrap_or_else(|| short_name.clone()),
290 club_name: club_name.unwrap_or_else(|| team_name.clone()),
291 team_code,
292 abbreviation,
293 team_name,
294 short_name,
295 full_name,
296 }
297 }
298}
299
300id!(#[doc = "A [`u32`] representing a team's ID."] TeamId { id: u32 });
301
302#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
304#[serde(rename_all = "camelCase")]
305pub struct NamedOrganization {
306 #[serde(rename = "parentOrgName")]
307 pub name: String,
308 #[serde(rename = "parentOrgId")]
309 pub id: OrganizationId,
310}
311
312id!(#[doc = "ID of a parent organization -- still don't know what this is."] OrganizationId { id: u32 });
313
314#[derive(Debug, Deserialize, PartialEq, Copy, Clone, Default)]
316pub enum AllStarStatus {
317 #[serde(rename = "Y")]
319 Yes,
320 #[default]
322 #[serde(rename = "N")]
323 No,
324 #[serde(rename = "F")]
326 F,
327 #[serde(rename = "O")]
329 O,
330}
331
332#[derive(Debug, Deserialize, PartialEq, Clone)]
334#[serde(rename_all = "camelCase", bound = "H: TeamHydrations")]
335pub struct TeamsResponse<H: TeamHydrations> {
336 pub copyright: Copyright,
337 pub teams: Vec<Team<H>>,
338}
339
340pub trait TeamHydrations: Hydrations<RequestData=()> {
341 type Sport: Debug + DeserializeOwned + PartialEq + Clone;
343
344 type Venue: Debug + DeserializeOwned + PartialEq + Clone;
346
347 type SpringVenue: Debug + DeserializeOwned + PartialEq + Clone;
349
350 type League: Debug + DeserializeOwned + PartialEq + Clone;
352
353 type Division: Debug + DeserializeOwned + PartialEq + Clone;
355
356 fn unknown_venue() -> Self::Venue;
357
358 fn unknown_league() -> Self::League;
359}
360
361impl TeamHydrations for () {
362 type Sport = SportId;
363 type Venue = NamedVenue;
364 type SpringVenue = VenueId;
365 type League = NamedLeague;
366 type Division = NamedDivision;
367
368 fn unknown_venue() -> Self::Venue {
369 NamedVenue::unknown_venue()
370 }
371
372 fn unknown_league() -> Self::League {
373 NamedLeague::unknown_league()
374 }
375}
376
377#[macro_export]
422macro_rules! team_hydrations {
423 (@ inline_structs [previous_schedule: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
424 ::pastey::paste! {
425 $crate::schedule_hydrations! {
426 $vis struct [<$name InlinePreviousSchedule>] {
427 $($inline_tt)*
428 }
429 }
430
431 $crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
432 $vis struct $name {
433 $($field_tt)*
434 venue: [<$name InlinePreviousSchedule>],
435 }
436 }
437 }
438 };
439 (@ inline_structs [next_schedule: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
440 ::pastey::paste! {
441 $crate::schedule_hydrations! {
442 $vis struct [<$name InlineNextSchedule>] {
443 $($inline_tt)*
444 }
445 }
446
447 $crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
448 $vis struct $name {
449 $($field_tt)*
450 venue: [<$name InlineNextSchedule>],
451 }
452 }
453 }
454 };
455 (@ inline_structs [venue: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
456 ::pastey::paste! {
457 $crate::venue_hydrations! {
458 $vis struct [<$name InlineVenue>] {
459 $($inline_tt)*
460 }
461 }
462
463 $crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
464 $vis struct $name {
465 $($field_tt)*
466 venue: [<$name InlineVenue>],
467 }
468 }
469 }
470 };
471 (@ inline_structs [spring_venue: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
472 ::pastey::paste! {
473 $crate::venue_hydrations! {
474 $vis struct [<$name InlineSpringVenue>] {
475 $($inline_tt)*
476 }
477 }
478
479 $crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
480 $vis struct $name {
481 $($field_tt)*
482 spring_venue: [<$name InlineSpringVenue>],
483 }
484 }
485 }
486 };
487 (@ inline_structs [sport: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
488 ::pastey::paste! {
489 $crate::sports_hydrations! {
490 $vis struct [<$name InlineSport>] {
491 $($inline_tt)*
492 }
493 }
494
495 $crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
496 $vis struct $name {
497 $($field_tt)*
498 sport: [<$name InlineSport>],
499 }
500 }
501 }
502 };
503 (@ inline_structs [standings: { $($inline_tt:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
504 ::pastey::paste! {
505 $crate::standings_hydrations! {
506 $vis struct [<$name InlineStandings>] {
507 $($inline_tt)*
508 }
509 }
510
511 $crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
512 $vis struct $name {
513 $($field_tt)*
514 standings: [<$name InlineStandings>],
515 }
516 }
517 }
518 };
519 (@ inline_structs [$_01:ident : { $($_02:tt)* } $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
520 ::core::compile_error!("Found unknown inline struct");
521 };
522 (@ inline_structs [$field:ident $(: $value:ty)? $(, $($tt:tt)*)?] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
523 $crate::team_hydrations! { @ inline_structs [$($($tt)*)?]
524 $vis struct $name {
525 $($field_tt)*
526 $field $(: $value)?,
527 }
528 }
529 };
530 (@ inline_structs [] $vis:vis struct $name:ident { $($field_tt:tt)* }) => {
531 $crate::team_hydrations! { @ actual
532 $vis struct $name {
533 $($field_tt)*
534 }
535 }
536 };
537
538 (@ sport) => { $crate::sport::SportId };
539 (@ sport $hydrations:ty) => { $crate::sport::Sport<$hydrations> };
540
541 (@ venue) => { $crate::venue::NamedVenue };
542 (@ venue $hydrations:ty) => { $crate::venue::Venue<$hydrations> };
543 (@ unknown_venue) => { $crate::venue::NamedVenue::unknown_venue() };
544 (@ unknown_venue $hydrations:ty) => { unimplemented!() }; (@ spring_venue) => { $crate::venue::VenueId };
547 (@ spring_venue $hydrations:ty) => { $crate::venue::Venue<$hydrations> };
548
549 (@ league) => { $crate::league::NamedLeague };
550 (@ league ,) => { $crate::league::League };
551 (@ unknown_league) => { $crate::league::NamedLeague::unknown_league() };
552 (@ unknown_league ,) => { unimplemented!() }; (@ division) => { $crate::division::NamedDivision };
555 (@ division ,) => { $crate::division::Division };
556
557 (@ actual $vis:vis struct $name:ident {
558 $(previous_schedule: $previous_schedule:ty ,)?
559 $(next_schedule: $next_schedule:ty ,)?
560 $(venue: $venue:ty ,)?
561 $(spring_venue: $spring_venue:ty ,)?
562 $(social $social_comma:tt)?
563 $(league $league_comma:tt)?
564 $(sport: $sport:ty ,)?
565 $(standings: $standings:ty ,)?
566 $(division $division_comma:tt)?
567 $(external_references $external_references_comma:tt)?
568 }) => {
569 #[derive(::core::fmt::Debug, ::serde::Deserialize, ::core::cmp::PartialEq, ::core::clone::Clone)]
570 $vis struct $name {
571 $(#[serde(rename = "previousGameSchedule")] previous_schedule: $crate::schedule::ScheduleResponse<$previous_schedule>,)?
572 $(#[serde(rename = "nextGameSchedule")] next_schedule: $crate::schedule::ScheduleResponse<$next_schedule>,)?
573 $(#[serde(rename = "xrefIds")] external_references: ::std::vec::Vec<$crate::types::ExternalReference> $external_references_comma)?
574 $(#[serde(default, rename = "social")] socials: ::std::collections::HashMap<::std::string::String, ::std::vec::Vec<::std::string::String> $social_comma>)?
575 }
576
577 impl $crate::team::TeamHydrations for $name {
578 type Sport = $crate::team_hydrations!(@ sport $($sport)?);
579
580 type Venue = $crate::team_hydrations!(@ venue $($venue)?);
581
582 type SpringVenue = $crate::team_hydrations!(@ spring_venue $($spring_venue)?);
583
584 type League = $crate::team_hydrations!(@ league $($league_comma)?);
585
586 type Division = $crate::team_hydrations!(@ league $($division_comma)?);
587
588 fn unknown_venue() -> Self::Venue {
589 $crate::team_hydrations!(@ unknown_venue $($venue)?)
590 }
591
592 fn unknown_league() -> Self::League {
593 $crate::team_hydrations!(@ unknown_league $($league_comma)?)
594 }
595 }
596
597 impl $crate::hydrations::Hydrations for $name {
598 type RequestData = ();
599
600 fn hydration_text(&(): &Self::RequestData) -> ::std::borrow::Cow<'static, str> {
601 let text = ::std::borrow::Cow::Borrowed(::core::concat!(
602 $("social," $social_comma)?
603 $("xrefId," $external_references_comma)?
604 $("league," $league_comma)?
605 $("division," $division_comma)?
606 ));
607
608 $(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}previousSchedule({}),", <$previous_schedule as $crate::hydrations::Hydrations>::hydration_text(&())));)?
609 $(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}nextSchedule({}),", <$next_schedule as $crate::hydrations::Hydrations>::hydration_text(&())));)?
610 $(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}venue({}),", <$venue as $crate::hydrations::Hydrations>::hydration_text(&())));)?
611 $(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}springVenue({}),", <$spring_venue as $crate::hydrations::Hydrations>::hydration_text(&())));)?
612 $(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}sport({}),", <$sport as $crate::hydrations::Hydrations>::hydration_text(&())));)?
613 $(let text = ::std::borrow::Cow::<'static, str>::Owned(::std::format!("{text}standings({}),", <$standings as $crate::hydrations::Hydrations>::hydration_text(&())));)?
614
615 text
616 }
617 }
618 };
619 ($vis:vis struct $name:ident {
620 $($tt:tt)*
621 }) => {
622 $crate::team_hydrations! { @ inline_structs [$($tt)*] $vis struct $name {} }
623 };
624}
625
626#[derive(Builder)]
628#[builder(derive(Into))]
629pub struct TeamsRequest<H: TeamHydrations> {
630 #[builder(into)]
631 sport_id: Option<SportId>,
632 #[builder(into)]
633 season: Option<SeasonId>,
634 #[builder(into)]
635 team_id: Option<TeamId>,
636 #[builder(skip)]
637 _marker: PhantomData<H>,
638}
639
640impl TeamsRequest<()> {
641 pub fn for_sport(sport_id: impl Into<SportId>) -> TeamsRequestBuilder<(), teams_request_builder::SetSportId> {
642 Self::builder().sport_id(sport_id)
643 }
644
645 pub fn mlb_teams() -> TeamsRequestBuilder<(), teams_request_builder::SetSportId> {
646 Self::for_sport(SportId::MLB)
647 }
648
649 pub fn all_sports() -> TeamsRequestBuilder<()> {
650 Self::builder()
651 }
652}
653
654impl<H: TeamHydrations, S: teams_request_builder::State + teams_request_builder::IsComplete> crate::request::RequestURLBuilderExt for TeamsRequestBuilder<H, S> {
655 type Built = TeamsRequest<H>;
656}
657
658impl<H: TeamHydrations> Display for TeamsRequest<H> {
659 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
660 let hydrations = Some(H::hydration_text(&())).filter(|s| !s.is_empty());
661 write!(f, "http://statsapi.mlb.com/api/v1/teams{}", gen_params! { "sportId"?: self.sport_id, "season"?: self.season, "teamId"?: self.team_id, "hydrate"?: hydrations })
662 }
663}
664
665impl<H: TeamHydrations> RequestURL for TeamsRequest<H> {
666 type Response = TeamsResponse<H>;
667}
668
669#[cfg(test)]
670mod tests {
671 use crate::request::RequestURLBuilderExt;
672 use crate::TEST_YEAR;
673 use super::*;
674
675 #[tokio::test]
676 #[cfg_attr(not(feature = "_heavy_tests"), ignore)]
677 async fn parse_all_teams_all_seasons() {
678 for season in 1871..=TEST_YEAR {
679 let _response = TeamsRequest::all_sports().season(season).build_and_get().await.unwrap();
680 }
681 }
682
683 #[tokio::test]
684 async fn parse_all_mlb_teams_this_season() {
685 let _ = TeamsRequest::mlb_teams().build_and_get().await.unwrap();
686 }
687
688 #[tokio::test]
689 async fn parse_all_mlb_teams_this_season_hydrated() {
690 team_hydrations! {
691 pub struct TestHydrations {
692 previous_schedule: (),
693 next_schedule: (),
694 venue: (),
695 spring_venue: (),
696 social,
697 league,
698 sport: (),
699 standings: (),
700 division,
701 external_references,
702 }
703 }
704
705 let _ = TeamsRequest::<TestHydrations>::builder().sport_id(SportId::MLB).season(TEST_YEAR).build_and_get().await.unwrap();
706 }
707}