Skip to main content

mlb_api/requests/
draft.rs

1//! Draft endpoint data. Picks, rounds, etc.
2
3use crate::person::{Person, PersonId};
4use crate::season::SeasonId;
5use crate::team::TeamId;
6use crate::{Copyright, Location};
7use crate::meta::PositionCode;
8use crate::request::RequestURL;
9use bon::Builder;
10use derive_more::Display;
11use serde::Deserialize;
12use std::fmt::{Display, Formatter};
13use thiserror::Error;
14use crate::team::NamedTeam;
15
16/// Returns a [`DraftYear`].
17#[derive(Debug, Deserialize, PartialEq, Clone)]
18#[serde(rename_all = "camelCase")]
19pub struct DraftResponse {
20	pub copyright: Copyright,
21	pub drafts: DraftYear,
22}
23
24/// A collection of [`DraftRound`]s in a year's draft.
25#[derive(Debug, Deserialize, PartialEq, Clone)]
26#[serde(rename_all = "camelCase")]
27pub struct DraftYear {
28	#[serde(rename = "draftYear")]
29	pub year: u32,
30	pub rounds: Vec<DraftRound>,
31}
32
33/// A round of [`DraftPick`]s
34///
35/// Note that rounds typically do not always line up as `"1"`, `"2"`, etc. and instead have non-integer names, for example, the 2025 draft had:
36/// - `"3"`
37/// - `"SUP-3"`
38/// - `"4"`
39/// - `"4C"`
40#[derive(Debug, Deserialize, PartialEq, Clone)]
41#[serde(rename_all = "camelCase")]
42pub struct DraftRound {
43	pub round: String,
44	pub picks: Vec<DraftPick>,
45}
46
47id!(#[doc = "Different from [`PersonId`](crate::person::PersonId).\n\nInternal eBIS person id, I'd be surprised if you had a use for this."] #[allow(non_camel_case_types)] eBISPersonId { id: u32 });
48
49/// Returns a [`Vec`] of [`DraftPick`]s for the prospects.
50#[derive(Debug, Deserialize, PartialEq, Clone)]
51#[serde(rename_all = "camelCase")]
52pub struct DraftProspectsResponse {
53	pub copyright: Copyright,
54	#[serde(rename = "totalSize")]
55	pub total_prospects: usize,
56	#[serde(rename = "returnedSize")]
57	pub returned_prospects: usize,
58	pub offset: usize,
59	pub prospects: Vec<DraftPick>,
60}
61
62/// An individual draft pick.
63#[derive(Debug, Deserialize, PartialEq, Clone)]
64#[serde(rename_all = "camelCase")]
65pub struct DraftPick {
66	/// a `PlayerId` on the eBIS System
67	#[serde(rename = "bisPlayerId")]
68	pub ebis_player_id: Option<eBISPersonId>,
69	#[serde(default, rename = "pickRound")]
70	pub round: String,
71	#[serde(default)]
72	pub pick_number: u32,
73	#[serde(rename = "displayPickNumber")]
74	pub displayed_pick_number: Option<u32>,
75	pub rank: Option<u32>,
76	#[serde(default, deserialize_with = "crate::try_from_str")]
77	pub signing_bonus: Option<u32>,
78	pub home: Location,
79	pub scouting_report_url: Option<String>,
80	pub school: School,
81	pub blurb: Option<String>,
82	#[serde(rename = "headshotLink", default = "get_default_headshot")]
83	pub headshot_url: String,
84	pub person: Option<Person>,
85	#[serde(default = "NamedTeam::unknown_team")]
86	pub team: NamedTeam,
87	pub draft_type: DraftType,
88	pub is_drafted: bool,
89	pub is_pass: bool,
90	pub year: SeasonId,
91}
92
93#[must_use]
94fn get_default_headshot() -> String {
95	"https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:silo:current.png/w_120,q_auto:best/v1/people/0/headshot/draft/current".to_owned()
96}
97
98impl DraftPick {
99	#[must_use]
100	pub fn displayed_pick_number(&self) -> u32 {
101		self.displayed_pick_number.unwrap_or(self.pick_number)
102	}
103}
104
105#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
106#[serde(rename_all = "camelCase")]
107pub struct School {
108	pub name: Option<String>,
109	pub city: Option<String>,
110	pub class: Option<String>,
111	pub country: Option<String>,
112	pub state: Option<String>,
113}
114
115#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display)]
116#[serde(try_from = "__DraftTypeStruct")]
117pub enum DraftType {
118	#[display("June Amateur Draft")]
119	JR,
120	/// Never appears
121	JS,
122	/// Never appears
123	NS,
124	/// Never appears
125	NR,
126	/// Never appears
127	AL,
128	/// Never appears
129	RA,
130	/// Never appears
131	RT,
132	/// Never appears
133	JD,
134	/// Never appears
135	AD,
136}
137
138#[derive(Deserialize)]
139#[doc(hidden)]
140struct __DraftTypeStruct {
141	code: String,
142}
143
144#[derive(Debug, Error)]
145enum DraftTypeParseError {
146	#[error("Invalid draft type code {0}")]
147	InvalidCode(String),
148}
149
150impl TryFrom<__DraftTypeStruct> for DraftType {
151	type Error = DraftTypeParseError;
152
153	fn try_from(value: __DraftTypeStruct) -> Result<Self, Self::Error> {
154		Ok(match &*value.code {
155			"JR" => Self::JR,
156			_ => return Err(DraftTypeParseError::InvalidCode(value.code)),
157		})
158	}
159}
160
161/// Returns a [`DraftResponse`]
162#[derive(Builder)]
163#[builder(start_fn = __latest)]
164pub struct DraftRequestLatest {
165	/// Year of the draft.
166	#[builder(into)]
167	year: Option<SeasonId>,
168}
169
170impl Display for DraftRequestLatest {
171	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
172		write!(f, "http://statsapi.mlb.com/api/v1/draft/{year}/latest", year = self.year.map_or(String::new(), |x| x.to_string()))
173	}
174}
175
176impl RequestURL for DraftRequestLatest {
177	type Response = DraftResponse;
178}
179
180/// This request sorts into rounds
181///
182/// Returns a [`DraftProspectsResponse`]
183#[derive(Builder)]
184#[builder(start_fn = regular)]
185#[builder(derive(Into))]
186pub struct DraftRequest {
187	/// Year of the draft.
188	#[builder(into)]
189	year: Option<SeasonId>,
190	/// Number of results to return.
191	#[builder(into)]
192	limit: Option<u32>,
193	/// Offset in the results (used for pagination).
194	#[builder(into)]
195	offset: Option<u32>,
196	/// Draft round.
197	#[builder(into)]
198	round: Option<u32>,
199
200	/// Include only successfully drafted players
201	#[builder(into)]
202	drafted_only: Option<bool>,
203	/// Filter players by the first character of their last name.
204	#[builder(into)]
205	last_name: Option<char>,
206	/// Filter players by the first character of their school they were drafted from.
207	#[builder(into)]
208	school: Option<char>,
209	/// Filter players by their position.
210	#[builder(into)]
211	position: Option<PositionCode>,
212	/// Filter players by the team they were drafted by.
213	#[builder(into)]
214	team_id: Option<TeamId>,
215	/// Filter players by their home country.
216	#[builder(into)]
217	home_country: Option<String>,
218	/// Filter for a specific player id.
219	#[builder(into)]
220	player_id: Option<PersonId>,
221}
222
223impl<S: draft_request_builder::State + draft_request_builder::IsComplete> crate::request::RequestURLBuilderExt for DraftRequestBuilder<S> {
224	type Built = DraftRequest;
225}
226
227impl DraftRequest {
228	pub fn latest() -> DraftRequestLatestBuilder {
229		DraftRequestLatest::__latest()
230	}
231}
232
233impl Display for DraftRequest {
234	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
235		let Self {
236			year,
237			limit,
238			offset,
239			round,
240			drafted_only,
241			last_name,
242			school,
243			position,
244			team_id,
245			home_country,
246			player_id,
247		} = self;
248		write!(
249			f,
250			"http://statsapi.mlb.com/api/v1/draft/{year}{params}",
251			year = year.map_or(String::new(), |x| x.to_string()),
252			params = gen_params! {
253					"limit"?: limit,
254					"offset"?: offset,
255					"round"?: round,
256					"drafted"?: drafted_only,
257					"name"?: last_name,
258					"school"?: school,
259					"position"?: position,
260					"teamId"?: team_id,
261					"homeCountry"?: home_country,
262					"playerId"?: player_id,
263				}
264		)
265	}
266}
267
268impl RequestURL for DraftRequest {
269	type Response = DraftResponse;
270}
271
272/// This request gives a list of prospects.
273///
274/// Returns a [`DraftProspectsResponse`]
275#[derive(Builder)]
276#[builder(start_fn = regular)]
277#[builder(derive(Into))]
278pub struct DraftProspectsRequest {
279	/// Year of the draft.
280	#[builder(into)]
281	year: Option<SeasonId>,
282	/// Number of results to return.
283	#[builder(into)]
284	limit: Option<u32>,
285	/// Offset in the results (used for pagination).
286	#[builder(into)]
287	offset: Option<u32>,
288	/// Draft round.
289	#[builder(into)]
290	round: Option<u32>,
291
292	/// Include only successfully drafted players
293	#[builder(into)]
294	drafted_only: Option<bool>,
295	/// Filter players by the first character of their last name.
296	#[builder(into)]
297	last_name: Option<char>,
298	/// Filter players by the first character of their school they were drafted from.
299	#[builder(into)]
300	school: Option<char>,
301	/// Filter players by their position.
302	#[builder(into)]
303	position: Option<PositionCode>,
304	/// Filter players by the team they were drafted by.
305	#[builder(into)]
306	team_id: Option<TeamId>,
307	/// Filter players by their home country.
308	#[builder(into)]
309	home_country: Option<String>,
310	/// Filter for a specific player id.
311	#[builder(into)]
312	player_id: Option<PersonId>,
313}
314
315impl<S: draft_prospects_request_builder::State + draft_prospects_request_builder::IsComplete> crate::request::RequestURLBuilderExt for DraftProspectsRequestBuilder<S> {
316    type Built = DraftProspectsRequest;
317}
318
319impl Display for DraftProspectsRequest {
320	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
321		let Self {
322			year,
323			limit,
324			offset,
325			round,
326			drafted_only,
327			last_name,
328			school,
329			position,
330			team_id,
331			home_country,
332			player_id,
333		} = self;
334		write!(
335			f,
336			"http://statsapi.mlb.com/api/v1/draft/prospects/{year}{params}",
337			year = year.map_or(String::new(), |x| x.to_string()),
338			params = gen_params! {
339						"limit"?: limit,
340						"offset"?: offset,
341						"round"?: round,
342						"drafted"?: drafted_only,
343						"name"?: last_name,
344						"school"?: school,
345						"position"?: position,
346						"teamId"?: team_id,
347						"homeCountry"?: home_country,
348						"playerId"?: player_id,
349				}
350		)
351	}
352}
353
354impl RequestURL for DraftProspectsRequest {
355	type Response = DraftProspectsResponse;
356}
357
358#[cfg(test)]
359mod tests {
360	use crate::draft::{DraftProspectsRequest, DraftRequest};
361	use crate::request::RequestURLBuilderExt;
362	use crate::TEST_YEAR;
363
364	#[tokio::test]
365	async fn draft_test_year() {
366		let _ = DraftRequest::regular().year(TEST_YEAR).build_and_get().await.unwrap();
367		let _ = DraftProspectsRequest::regular().year(TEST_YEAR).build_and_get().await.unwrap();
368	}
369
370	#[tokio::test]
371	#[cfg_attr(not(feature = "_heavy_tests"), ignore)]
372	async fn draft_all_years() {
373		for year in 1965..=TEST_YEAR {
374			let _ = DraftRequest::regular().year(year).build_and_get().await.unwrap();
375			let _ = DraftProspectsRequest::regular().year(year).build_and_get().await.unwrap();
376		}
377	}
378}