mlb_api/endpoints/stats/leaders/
mod.rs

1use crate::endpoints::league::League;
2use crate::endpoints::person::Person;
3use crate::endpoints::sports::{Sport, SportId};
4use crate::endpoints::teams::team::Team;
5use crate::endpoints::{BaseballStat, BaseballStatId, GameType, IdentifiableBaseballStat, StatGroup, StatType, StatsAPIUrl};
6use crate::gen_params;
7use crate::types::{Copyright, IntegerOrFloatStat, PlayerPool};
8use chrono::NaiveDate;
9use itertools::Itertools;
10use serde::Deserialize;
11use serde_with::serde_as;
12use serde_with::DisplayFromStr;
13use std::fmt::{Display, Formatter};
14use std::str::FromStr;
15
16#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
17#[serde(rename_all = "camelCase")]
18pub struct StatLeadersResponse {
19	pub copyright: Copyright,
20	pub league_leaders: Vec<StatLeaders>,
21}
22
23#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
24#[serde(try_from = "__StatLeadersStruct")]
25pub struct StatLeaders {
26	pub category: BaseballStat,
27	pub game_type: GameType,
28	pub leaders: Vec<StatLeader>,
29	pub stat_group: StatGroup,
30	pub total_splits: u32,
31}
32
33#[derive(Deserialize)]
34#[serde(rename_all = "camelCase")]
35struct __StatLeadersStruct {
36	leader_category: String,
37	game_type: GameType,
38	#[serde(default)]
39	leaders: Vec<StatLeader>,
40	stat_group: String,
41	total_splits: u32,
42}
43
44impl TryFrom<__StatLeadersStruct> for StatLeaders {
45	type Error = <StatGroup as FromStr>::Err;
46
47	fn try_from(value: __StatLeadersStruct) -> Result<Self, Self::Error> {
48		Ok(StatLeaders {
49			category: BaseballStat::Identifiable(IdentifiableBaseballStat {
50				id: BaseballStatId::new(value.leader_category),
51			}),
52			game_type: value.game_type,
53			leaders: value.leaders,
54			stat_group: StatGroup::from_str(&value.stat_group)?,
55			total_splits: value.total_splits,
56		})
57	}
58}
59
60#[serde_as]
61#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
62#[serde(rename_all = "camelCase")]
63pub struct StatLeader {
64	pub rank: u32,
65	pub value: IntegerOrFloatStat,
66	#[serde(default = "Team::unknown_team")]
67	pub team: Team,
68	pub num_teams: u32,
69	#[serde(default = "League::unknown_league")]
70	pub league: League,
71	pub person: Person,
72	pub sport: Sport,
73	#[serde_as(as = "DisplayFromStr")]
74	pub season: u16,
75}
76
77pub struct StatLeadersEndpointUrl {
78	pub stats: Vec<BaseballStat>,
79	pub stat_groups: Vec<StatGroup>,
80	pub season: Option<u16>,
81	pub sport_id: SportId,
82	pub stat_types: Vec<StatType>,
83	pub start_date: Option<NaiveDate>,
84	pub end_date: Option<NaiveDate>,
85	pub pool: PlayerPool,
86
87	/// Number of days to go back for data (starting from yesterday)
88	pub days_back: Option<u32>,
89
90	/// Limit on how many leaders to show per stat.
91	/// Default is 5.
92	pub limit: Option<u16>,
93	/// Offset into results.
94	pub offset: Option<u16>,
95
96	/// [`None`] represents all game types.
97	pub game_types: Option<Vec<GameType>>,
98}
99
100impl Display for StatLeadersEndpointUrl {
101	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
102		write!(f, "http://statsapi.mlb.com/api/v1/stats/leaders{params}", params = gen_params! {
103			"leaderCategories": self.stats.iter().map(|stat| &stat.id).join(","),
104			"statGroup": self.stat_groups.iter().join(","),
105			"season"?: self.season,
106			"sportId": self.sport_id,
107			"stats": self.stat_types.iter().join(","),
108			"startDate"?: self.start_date,
109			"endDate"?: self.end_date,
110			"playerPool": self.pool,
111			"daysBack"?: self.days_back,
112			"limit"?: self.limit,
113			"offset"?: self.offset,
114			"gameTypes"?: self.game_types.as_ref().map(|x| x.iter().join(",")),
115		})
116	}
117}
118
119impl StatsAPIUrl for StatLeadersEndpointUrl {
120	type Response = StatLeadersResponse;
121}
122
123#[cfg(test)]
124mod tests {
125	use crate::endpoints::meta::MetaEndpointUrl;
126	use crate::endpoints::sports::SportId;
127	use crate::endpoints::stats::leaders::StatLeadersEndpointUrl;
128	use crate::endpoints::{BaseballStat, GameType, StatsAPIUrl};
129	use crate::types::PlayerPool;
130
131	#[tokio::test]
132	async fn test_stat_leaders() {
133		let all_stats = MetaEndpointUrl::<BaseballStat>::new().get().await.unwrap().entries;
134		let all_game_types = MetaEndpointUrl::<GameType>::new().get().await.unwrap().entries;
135
136		let request = StatLeadersEndpointUrl {
137			stats: all_stats,
138			stat_groups: vec![],
139			season: None,
140			sport_id: SportId::MLB,
141			stat_types: vec![],
142			start_date: None,
143			end_date: None,
144			pool: PlayerPool::All,
145			days_back: None,
146			limit: Some(100),
147			offset: None,
148			game_types: Some(all_game_types),
149		};
150		let response_str = reqwest::get(request.to_string()).await.unwrap().text().await.unwrap();
151		let mut de = serde_json::Deserializer::from_str(&response_str);
152		let result: Result<<StatLeadersEndpointUrl as StatsAPIUrl>::Response, serde_path_to_error::Error<_>> = serde_path_to_error::deserialize(&mut de);
153		match result {
154			Ok(_) => {}
155			Err(e) if format!("{:?}", e.inner()).contains("missing field `copyright`") => {}
156			Err(e) => panic!("Err: {:?}", e),
157		}
158	}
159}