1use std::collections::HashMap;
2use serde::{Deserialize, Deserializer, Serialize};
3use chrono::{DateTime, NaiveDate, TimeZone, Utc};
4pub use reqwest;
5
6const API_INDEX_URL: &str = "https://components.ifsc-climbing.org/results-api.php?api=index";
7const API_CALENDAR_URL: &str = "https://components.ifsc-climbing.org/results-api.php?api=season_leagues_calendar&league=%LEAGUE_ID%";
8
9#[derive(Deserialize)]
10struct IfscLeagueRaw {
11 id: i32,
12 name: String
13}
14
15#[derive(Deserialize)]
16struct IfscSeasonRaw {
17 id: i32,
18 name: String,
19 leagues: Vec<IfscLeagueRaw>
20}
21
22#[derive(Deserialize)]
23struct IfscCurrentSeasonRaw {
24 id: i32
25}
26
27#[derive(Deserialize)]
28struct IfscIndexRaw {
29 current: IfscCurrentSeasonRaw,
30 seasons: Vec<IfscSeasonRaw>
31}
32
33#[derive(Serialize)]
34pub struct IfscLeague {
35 pub id: i32,
36 pub parent_id: i32,
38 pub name: String
39}
40
41#[derive(Serialize)]
42pub struct IfscSeason {
43 pub id: i32,
44 pub name: String,
45 pub leagues: Vec<i32>
47}
48
49#[derive(Serialize)]
51pub struct IfscIndex {
52 pub current_season_id: i32,
54 pub seasons: HashMap<i32, IfscSeason>,
56 pub leagues: HashMap<i32, IfscLeague>
58}
59
60impl IfscIndex {
61 pub async fn retrieve() -> Result<IfscIndex, reqwest::Error> {
63 let parsed = reqwest::get(API_INDEX_URL)
64 .await?
65 .error_for_status()?
66 .json::<IfscIndexRaw>()
67 .await?;
68 Ok(Self::from_raw(&parsed))
69 }
70
71 fn from_raw(raw: &IfscIndexRaw) -> IfscIndex {
73 let mut seasons = HashMap::new();
74 let mut leagues = HashMap::new();
75
76 for season in &raw.seasons {
77 let mut season_leagues_ids = Vec::new();
78
79 for league in &season.leagues {
80 leagues.insert(league.id, IfscLeague {
81 id: league.id,
82 parent_id: season.id,
83 name: league.name.clone()
84 });
85
86 season_leagues_ids.push(league.id);
87 }
88
89 seasons.insert(season.id, IfscSeason {
90 id: season.id,
91 name: season.name.clone(),
92 leagues: season_leagues_ids
93 });
94 }
95
96 IfscIndex {
97 seasons,
98 leagues,
99 current_season_id: raw.current.id
100 }
101 }
102}
103
104fn deserialize_event_datetime<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
106 where
107 D: Deserializer<'de>
108{
109 let s = String::deserialize(deserializer)?;
110 Utc.datetime_from_str(&s, "%Y-%m-%d %H:%M:%S UTC").map_err(serde::de::Error::custom)
111}
112
113fn deserialize_event_date<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
115 where
116 D: Deserializer<'de>
117{
118 let s = String::deserialize(deserializer)?;
119 NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(serde::de::Error::custom)
120}
121
122#[derive(Clone, Deserialize)]
123pub struct IfscEvent {
124 pub event_id: i32,
125 pub event: String,
127 #[serde(deserialize_with = "deserialize_event_datetime")]
129 pub starts_at: DateTime<Utc>,
130 #[serde(deserialize_with = "deserialize_event_datetime")]
132 pub ends_at: DateTime<Utc>,
133 #[serde(deserialize_with = "deserialize_event_date")]
135 pub local_start_date: NaiveDate,
136 #[serde(deserialize_with = "deserialize_event_date")]
138 pub local_end_date: NaiveDate
139}
140
141#[derive(Clone, Deserialize)]
143pub struct IfscCalendar {
144 pub events: Vec<IfscEvent>
145}
146
147impl IfscCalendar {
148 pub async fn retrieve(league_id: i32) -> Result<IfscCalendar, reqwest::Error> {
150 let url = API_CALENDAR_URL.replace("%LEAGUE_ID%", &league_id.to_string());
151 reqwest::get(url)
152 .await?
153 .error_for_status()?
154 .json::<IfscCalendar>()
155 .await
156 }
157
158 pub fn empty() -> IfscCalendar {
160 IfscCalendar {
161 events: Vec::new()
162 }
163 }
164}