ifsc_calendar_api/
lib.rs

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    /// ID of this league's season
37    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    /// IDs of leagues in this season
46    pub leagues: Vec<i32>
47}
48
49/// Index of seasons and leagues
50#[derive(Serialize)]
51pub struct IfscIndex {
52    /// ID of current season
53    pub current_season_id: i32,
54    /// Collection of all seasons by their ID
55    pub seasons: HashMap<i32, IfscSeason>,
56    /// Collection of all leagues (of all seasons) by their ID
57    pub leagues: HashMap<i32, IfscLeague>
58}
59
60impl IfscIndex {
61    /// Retrieve index from IFSC API
62    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    /// Convert ownership-based raw index tree structure into ID-based index tree using hash maps
72    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
104/// Deserializes DateTime in the format "2022-10-05 16:00:00 UTC"
105fn 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
113/// Deserializes NaiveDate in the format "2022-10-05"
114fn 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    /// Event name
126    pub event: String,
127    /// Event start date & time in UTC
128    #[serde(deserialize_with = "deserialize_event_datetime")]
129    pub starts_at: DateTime<Utc>,
130    /// Event end date & time in UTC
131    #[serde(deserialize_with = "deserialize_event_datetime")]
132    pub ends_at: DateTime<Utc>,
133    /// Event start date in the event's local time zone
134    #[serde(deserialize_with = "deserialize_event_date")]
135    pub local_start_date: NaiveDate,
136    /// Event end date in the event's local time zone
137    #[serde(deserialize_with = "deserialize_event_date")]
138    pub local_end_date: NaiveDate
139}
140
141/// Collection of events
142#[derive(Clone, Deserialize)]
143pub struct IfscCalendar {
144    pub events: Vec<IfscEvent>
145}
146
147impl IfscCalendar {
148    /// Retrieve events of a league from the IFSC API
149    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    /// Create a new empty calendar
159    pub fn empty() -> IfscCalendar {
160        IfscCalendar {
161            events: Vec::new()
162        }
163    }
164}