ifsc-calendar-api 0.2.0

Interface to the IFSC's (unofficial) calendar API
Documentation
use std::collections::HashMap;
use serde::{Deserialize, Deserializer, Serialize};
use chrono::{DateTime, NaiveDate, TimeZone, Utc};
pub use reqwest;

const API_INDEX_URL: &str = "https://components.ifsc-climbing.org/results-api.php?api=index";
const API_CALENDAR_URL: &str = "https://components.ifsc-climbing.org/results-api.php?api=season_leagues_calendar&league=%LEAGUE_ID%";

#[derive(Deserialize)]
struct IfscLeagueRaw {
    id: i32,
    name: String
}

#[derive(Deserialize)]
struct IfscSeasonRaw {
    id: i32,
    name: String,
    leagues: Vec<IfscLeagueRaw>
}

#[derive(Deserialize)]
struct IfscCurrentSeasonRaw {
    id: i32
}

#[derive(Deserialize)]
struct IfscIndexRaw {
    current: IfscCurrentSeasonRaw,
    seasons: Vec<IfscSeasonRaw>
}

#[derive(Serialize)]
pub struct IfscLeague {
    pub id: i32,
    /// ID of this league's season
    pub parent_id: i32,
    pub name: String
}

#[derive(Serialize)]
pub struct IfscSeason {
    pub id: i32,
    pub name: String,
    /// IDs of leagues in this season
    pub leagues: Vec<i32>
}

/// Index of seasons and leagues
#[derive(Serialize)]
pub struct IfscIndex {
    /// ID of current season
    pub current_season_id: i32,
    /// Collection of all seasons by their ID
    pub seasons: HashMap<i32, IfscSeason>,
    /// Collection of all leagues (of all seasons) by their ID
    pub leagues: HashMap<i32, IfscLeague>
}

impl IfscIndex {
    /// Retrieve index from IFSC API
    pub async fn retrieve() -> Result<IfscIndex, reqwest::Error> {
        let parsed = reqwest::get(API_INDEX_URL)
            .await?
            .error_for_status()?
            .json::<IfscIndexRaw>()
            .await?;
        Ok(Self::from_raw(&parsed))
    }

    /// Convert ownership-based raw index tree structure into ID-based index tree using hash maps
    fn from_raw(raw: &IfscIndexRaw) -> IfscIndex {
        let mut seasons = HashMap::new();
        let mut leagues = HashMap::new();

        for season in &raw.seasons {
            let mut season_leagues_ids = Vec::new();

            for league in &season.leagues {
                leagues.insert(league.id, IfscLeague {
                    id: league.id,
                    parent_id: season.id,
                    name: league.name.clone()
                });

                season_leagues_ids.push(league.id);
            }

            seasons.insert(season.id, IfscSeason {
                id: season.id,
                name: season.name.clone(),
                leagues: season_leagues_ids
            });
        }

        IfscIndex {
            seasons,
            leagues,
            current_season_id: raw.current.id
        }
    }
}

/// Deserializes DateTime in the format "2022-10-05 16:00:00 UTC"
fn deserialize_event_datetime<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
    where
        D: Deserializer<'de>
{
    let s = String::deserialize(deserializer)?;
    Utc.datetime_from_str(&s, "%Y-%m-%d %H:%M:%S UTC").map_err(serde::de::Error::custom)
}

/// Deserializes NaiveDate in the format "2022-10-05"
fn deserialize_event_date<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
    where
        D: Deserializer<'de>
{
    let s = String::deserialize(deserializer)?;
    NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(serde::de::Error::custom)
}

#[derive(Clone, Deserialize)]
pub struct IfscEvent {
    pub event_id: i32,
    /// Event name
    pub event: String,
    /// Event start date & time in UTC
    #[serde(deserialize_with = "deserialize_event_datetime")]
    pub starts_at: DateTime<Utc>,
    /// Event end date & time in UTC
    #[serde(deserialize_with = "deserialize_event_datetime")]
    pub ends_at: DateTime<Utc>,
    /// Event start date in the event's local time zone
    #[serde(deserialize_with = "deserialize_event_date")]
    pub local_start_date: NaiveDate,
    /// Event end date in the event's local time zone
    #[serde(deserialize_with = "deserialize_event_date")]
    pub local_end_date: NaiveDate
}

/// Collection of events
#[derive(Clone, Deserialize)]
pub struct IfscCalendar {
    pub events: Vec<IfscEvent>
}

impl IfscCalendar {
    /// Retrieve events of a league from the IFSC API
    pub async fn retrieve(league_id: i32) -> Result<IfscCalendar, reqwest::Error> {
        let url = API_CALENDAR_URL.replace("%LEAGUE_ID%", &league_id.to_string());
        reqwest::get(url)
            .await?
            .error_for_status()?
            .json::<IfscCalendar>()
            .await
    }

    /// Create a new empty calendar
    pub fn empty() -> IfscCalendar {
        IfscCalendar {
            events: Vec::new()
        }
    }
}