1use crate::endpoints::person::{Person, PersonId};
2use crate::endpoints::teams::team::{Team, TeamId};
3use crate::endpoints::{Position, StatsAPIUrl};
4use crate::gen_params;
5use crate::types::{Copyright, Location};
6use derive_more::{Deref, Display};
7use serde::Deserialize;
8use serde_with::DisplayFromStr;
9use serde_with::serde_as;
10use std::fmt::{Display, Formatter};
11use thiserror::Error;
12
13#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
14#[serde(rename_all = "camelCase")]
15pub struct DraftResponse {
16 pub copyright: Copyright,
17 pub drafts: DraftYear,
18}
19
20#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
21#[serde(rename_all = "camelCase")]
22pub struct DraftYear {
23 #[serde(rename = "draftYear")]
24 pub year: u32,
25 pub rounds: Vec<DraftRound>,
26}
27
28#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
29#[serde(rename_all = "camelCase")]
30pub struct DraftRound {
31 pub round: String,
32 pub picks: Vec<DraftPick>,
33}
34
35#[repr(transparent)]
36#[derive(Debug, Deserialize, Deref, Display, PartialEq, Eq, Copy, Clone, Hash)]
37pub struct EBISPersonId(u32);
38
39impl EBISPersonId {
40 #[must_use]
41 pub const fn new(id: u32) -> Self {
42 Self(id)
43 }
44}
45
46#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
47#[serde(rename_all = "camelCase")]
48pub struct DraftProspectsResponse {
49 pub copyright: Copyright,
50 #[serde(rename = "totalSize")]
51 pub total_prospects: usize,
52 #[serde(rename = "returnedSize")]
53 pub returned_prospects: usize,
54 pub offset: usize,
55 pub prospects: Vec<DraftPick>,
56}
57
58#[serde_as]
59#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
60#[serde(rename_all = "camelCase")]
61pub struct DraftPick {
62 #[serde(rename = "bisPlayerId")]
64 pub ebis_player_id: Option<EBISPersonId>,
65 #[serde(default, rename = "pickRound")]
66 pub round: String,
67 #[serde(default)]
68 pub pick_number: u32,
69 #[serde(rename = "displayPickNumber")]
70 pub displayed_pick_number: Option<u32>,
71 pub rank: Option<u32>,
72 #[serde(default, deserialize_with = "crate::types::try_from_str")]
73 pub signing_bonus: Option<u32>,
74 pub home: Location,
75 pub scouting_report_url: Option<String>,
76 pub school: School,
77 pub blurb: Option<String>,
78 #[serde(rename = "headshotLink", default = "get_default_headshot")]
79 pub headshot_url: String,
80 #[serde(default = "Person::unknown_person")]
81 pub person: Person,
82 #[serde(default = "Team::unknown_team")]
83 pub team: Team,
84 pub draft_type: DraftType,
85 pub is_drafted: bool,
86 pub is_pass: bool,
87 #[serde_as(as = "DisplayFromStr")]
88 pub year: u32,
89}
90
91#[must_use]
92pub fn get_default_headshot() -> String {
93 "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()
94}
95
96impl DraftPick {
97 #[must_use]
98 pub fn displayed_pick_number(&self) -> u32 {
99 self.displayed_pick_number.unwrap_or(self.pick_number)
100 }
101}
102
103#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
104#[serde(rename_all = "camelCase")]
105pub struct School {
106 pub name: Option<String>,
107 pub city: Option<String>,
108 pub class: Option<String>,
109 pub country: Option<String>,
110 pub state: Option<String>,
111}
112
113#[derive(Debug, Deserialize, PartialEq, Eq, Copy, Clone, Display)]
114#[serde(try_from = "__DraftTypeStruct")]
115pub enum DraftType {
116 #[display("June Amateur Draft")]
117 JR,
118 JS,
120 NS,
122 NR,
124 AL,
126 RA,
128 RT,
130 JD,
132 AD,
134}
135
136#[derive(Deserialize)]
137struct __DraftTypeStruct {
138 code: String,
139}
140
141#[derive(Debug, Error)]
142enum DraftTypeParseError {
143 #[error("Invalid draft type code {0}")]
144 InvalidCode(String),
145}
146
147impl TryFrom<__DraftTypeStruct> for DraftType {
148 type Error = DraftTypeParseError;
149
150 fn try_from(value: __DraftTypeStruct) -> Result<Self, Self::Error> {
151 Ok(match &*value.code {
152 "JR" => DraftType::JR,
153 _ => return Err(DraftTypeParseError::InvalidCode(value.code)),
154 })
155 }
156}
157
158#[derive(Clone)]
160pub struct DraftEndpointUrl {
161 pub year: Option<u32>,
163 pub kind: DraftEndpointUrlKind,
165}
166
167#[derive(Clone)]
168pub enum DraftEndpointUrlKind {
169 Latest,
172 Regular(DraftEndpointUrlData),
174}
175
176#[derive(Clone)]
177pub struct DraftEndpointUrlData {
178 pub limit: Option<u32>,
180 pub offset: Option<u32>,
182 pub round: Option<u32>,
184
185 pub drafted_only: Option<bool>,
187 pub last_name: Option<char>,
189 pub school: Option<char>,
191 pub position: Option<Position>,
193 pub team_id: Option<TeamId>,
195 pub home_country: Option<String>,
197 pub player_id: Option<PersonId>,
199}
200
201impl Display for DraftEndpointUrl {
202 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
203 match self.kind.clone() {
204 DraftEndpointUrlKind::Latest => write!(f, "http://statsapi.mlb.com/api/v1/draft/{year}/latest", year = self.year.map_or(String::new(), |x| x.to_string())),
205 DraftEndpointUrlKind::Regular(DraftEndpointUrlData {
206 limit,
207 offset,
208 round,
209 drafted_only,
210 last_name,
211 school,
212 position,
213 team_id,
214 home_country,
215 player_id,
216 }) => write!(
217 f,
218 "http://statsapi.mlb.com/api/v1/draft/{year}{params}",
219 year = self.year.map_or(String::new(), |x| x.to_string()),
220 params = gen_params! {
221 "limit"?: limit,
222 "offset"?: offset,
223 "round"?: round,
224 "drafted"?: drafted_only,
225 "name"?: last_name,
226 "school"?: school,
227 "position"?: position.as_ref().map(|pos| &pos.code),
228 "teamId"?: team_id,
229 "homeCountry"?: home_country,
230 "playerId"?: player_id,
231 }
232 ),
233 }
234 }
235}
236
237impl StatsAPIUrl for DraftEndpointUrl {
238 type Response = DraftResponse;
239}
240
241pub struct DraftProspectsEndpointUrl {
244 pub year: Option<u32>,
246 pub kind: DraftEndpointUrlData,
248}
249
250impl Display for DraftProspectsEndpointUrl {
251 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
252 let DraftEndpointUrlData {
253 limit,
254 offset,
255 round,
256 drafted_only,
257 last_name,
258 school,
259 position,
260 team_id,
261 home_country,
262 player_id,
263 } = &self.kind;
264 write!(
265 f,
266 "http://statsapi.mlb.com/api/v1/draft/prospects/{year}{params}",
267 year = self.year.map_or(String::new(), |x| x.to_string()),
268 params = gen_params! {
269 "limit"?: limit,
270 "offset"?: offset,
271 "round"?: round,
272 "drafted"?: drafted_only,
273 "name"?: last_name,
274 "school"?: school,
275 "position"?: position.as_ref().map(|pos| &pos.code),
276 "teamId"?: team_id,
277 "homeCountry"?: home_country,
278 "playerId"?: player_id,
279 }
280 )
281 }
282}
283
284impl StatsAPIUrl for DraftProspectsEndpointUrl {
285 type Response = DraftProspectsResponse;
286}
287
288#[cfg(test)]
289mod tests {
290 use crate::endpoints::StatsAPIUrl;
291 use crate::endpoints::draft::{DraftEndpointUrl, DraftEndpointUrlKind, DraftProspectsEndpointUrl, DraftEndpointUrlData};
292 use chrono::{Datelike, Local};
293
294 #[tokio::test]
295 async fn draft_2025() {
296 let _ = DraftEndpointUrl {
297 year: Some(2025),
298 kind: DraftEndpointUrlKind::Regular(DraftEndpointUrlData {
299 limit: None,
300 offset: None,
301 round: None,
302 drafted_only: None,
303 last_name: None,
304 school: None,
305 position: None,
306 team_id: None,
307 home_country: None,
308 player_id: None,
309 }),
310 }
311 .get()
312 .await
313 .unwrap();
314
315 let _ = DraftProspectsEndpointUrl {
316 year: Some(2025),
317 kind: DraftEndpointUrlData {
318 limit: None,
319 offset: None,
320 round: None,
321 drafted_only: None,
322 last_name: None,
323 school: None,
324 position: None,
325 team_id: None,
326 home_country: None,
327 player_id: None,
328 },
329 }
330 .get()
331 .await
332 .unwrap();
333 }
334
335 #[tokio::test]
336 #[cfg_attr(not(feature = "_heavy_tests"), ignore)]
337 async fn draft_all_years() {
338 for year in 1965..=Local::now().year() as _ {
339 let json = reqwest::get(
340 DraftEndpointUrl {
341 year: Some(year),
342 kind: DraftEndpointUrlKind::Regular(DraftEndpointUrlData {
343 limit: None,
344 offset: None,
345 round: None,
346 drafted_only: None,
347 last_name: None,
348 school: None,
349 position: None,
350 team_id: None,
351 home_country: None,
352 player_id: None,
353 }),
354 }
355 .to_string(),
356 )
357 .await
358 .unwrap()
359 .bytes()
360 .await
361 .unwrap();
362 let mut de = serde_json::Deserializer::from_slice(&json);
363 let result: Result<<DraftEndpointUrl as StatsAPIUrl>::Response, serde_path_to_error::Error<_>> = serde_path_to_error::deserialize(&mut de);
364 match result {
365 Ok(_) => {}
366 Err(e) if format!("{:?}", e.inner()).contains("missing field `copyright`") => {}
367 Err(e) => panic!("Err: {:?} (yr: {year})", e),
368 }
369
370 let json = reqwest::get(
371 DraftProspectsEndpointUrl {
372 year: Some(year),
373 kind: DraftEndpointUrlData {
374 limit: None,
375 offset: None,
376 round: None,
377 drafted_only: None,
378 last_name: None,
379 school: None,
380 position: None,
381 team_id: None,
382 home_country: None,
383 player_id: None,
384 },
385 }
386 .to_string(),
387 )
388 .await
389 .unwrap()
390 .bytes()
391 .await
392 .unwrap();
393 let mut de = serde_json::Deserializer::from_slice(&json);
394 let result: Result<<DraftProspectsEndpointUrl as StatsAPIUrl>::Response, serde_path_to_error::Error<_>> = serde_path_to_error::deserialize(&mut de);
395 match result {
396 Ok(_) => {}
397 Err(e) if format!("{:?}", e.inner()).contains("missing field `copyright`") => {}
398 Err(e) => panic!("Err: {:?} (yr: {year})", e),
399 }
400 }
401 }
402}