sbb-api 0.1.0

A library to interact with sbb.ch's unofficial API
Documentation
use serde::{Serialize, Deserialize};
use crate::models::koordinaten::Koordinaten;
use crate::models::transport::TransportBezeichnung;
use crate::models::realtime_info::{SectionRealtimeInfo, RealtimeInfo};
use crate::models::legend::{LegendOccupancy, LegendItem};
use crate::models::ticketing::TicketingInfo;
use core::fmt;
use std::time::Duration;
use std::str::FromStr;

use mockall::predicate::*;
use regex::bytes::Regex;

#[derive(Serialize, Deserialize, Debug)]
pub struct Verbindung {
   #[serde(rename = "abfahrt")]
    pub abfahrt: String,
 
   #[serde(rename = "abfahrtDate")]
    pub abfahrt_date: String,
 
   #[serde(rename = "abfahrtGleis")]
    pub abfahrt_gleis: Option<String>,
 
   #[serde(rename = "abfahrtTime")]
    pub abfahrt_time: String,
 
   #[serde(rename = "angeboteUrl")]
    pub angebote_url: String,
 
   #[serde(rename = "ankunft")]
    pub ankunft: String,
 
   #[serde(rename = "ankunftDate")]
    pub ankunft_date: String,
 
   #[serde(rename = "ankunftTime")]
    pub ankunft_time: String,
 
   #[serde(rename = "belegungErste")]
    pub belegung_erste: String,
 
   #[serde(rename = "belegungZweite")]
    pub belegung_zweite: String,
 
   #[serde(rename = "dayDifference")]
    pub day_difference: String,
 
   #[serde(rename = "dayDifferenceAccessibility")]
    pub day_difference_accessibility: String,
 
   #[serde(rename = "departureTrackLabel")]
    pub departure_track_label: String,
 
   #[serde(rename = "departureTrackLabelAccessibility")]
    pub departure_track_label_accessibility: String,
 
   #[serde(rename = "duration")]
    pub duration: String,
 
   #[serde(rename = "durationAccessibility")]
    pub duration_accessibility: String,
 
   #[serde(rename = "isInternationalVerbindung")]
    pub is_international_verbindung: Option<bool>,
 
   #[serde(rename = "legendBfrItems")]
    pub legend_bfr_items: Vec<String>,
 
   #[serde(rename = "legendItems")]
    pub legend_items: Vec<LegendItem>,
 
   #[serde(rename = "legendOccupancyItems")]
    pub legend_occupancy_items: Vec<LegendOccupancy>,
 
   #[serde(rename = "realtimeInfo")]
    pub realtime_info: RealtimeInfo,
 
   #[serde(rename = "reconstructionContext")]
    pub reconstruction_context: String,
 
   #[serde(rename = "serviceAttributes")]
    pub service_attributes: Vec<String>,
 
   #[serde(rename = "ticketingInfo")]
    pub ticketing_info: TicketingInfo,
 
   #[serde(rename = "transfers")]
    pub transfers: i32,
 
   #[serde(rename = "transportBezeichnung")]
    pub transport_bezeichnung: TransportBezeichnung,
 
   #[serde(rename = "verbindungAbpreisContext")]
    pub verbindung_abpreis_context: String,
 
   #[serde(rename = "verbindungId")]
    pub verbindung_id: String,
 
   #[serde(rename = "verbindungSections")]
    pub verbindung_sections: Vec<VerbindungSection>,
 
   #[serde(rename = "verkehrstage")]
    pub verkehrstage: Vec<String>,
 
   #[serde(rename = "vias")]
    pub vias: Option<Vec<String>>,
 
   #[serde(rename = "zuschlagspflicht")]
    pub zuschlagspflicht: bool,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct VerbindungSection {
    #[serde(rename = "abfahrtCancellation")]
    pub abfahrt_cancellation: bool,

    #[serde(rename = "abfahrtDatum")]
    pub abfahrt_datum: String,

    #[serde(rename = "abfahrtGleis")]
    pub abfahrt_gleis: Option<String>,

    #[serde(rename = "abfahrtKoordinaten")]
    pub abfahrt_koordinaten: Koordinaten,

    #[serde(rename = "abfahrtName")]
    pub abfahrt_name: String,

    #[serde(rename = "abfahrtPlatformChange")]
    pub abfahrt_platform_change: bool,

    #[serde(rename = "abfahrtTime")]
    pub abfahrt_time: String,

    #[serde(rename = "actionUrl")]
    pub action_url: Option<String>,

    #[serde(rename = "ankunftCancellation")]
    pub ankunft_cancellation: bool,

    #[serde(rename = "ankunftDatum")]
    pub ankunft_datum: String,

    #[serde(rename = "ankunftGleis")]
    pub ankunft_gleis: Option<String>,

    #[serde(rename = "ankunftKoordinaten")]
    pub ankunft_koordinaten: Koordinaten,

    #[serde(rename = "ankunftName")]
    pub ankunft_name: String,

    #[serde(rename = "ankunftPlatformChange")]
    pub ankunft_platform_change: bool,

    #[serde(rename = "ankunftTime")]
    pub ankunft_time: String,

    #[serde(rename = "arrivalTrackLabel")]
    pub arrival_track_label: Option<String>,

    #[serde(rename = "arrivalTrackLabelAccessibility")]
    pub arrival_track_label_accessibility: Option<String>,

    #[serde(rename = "belegungErste")]
    pub belegung_erste: String,

    #[serde(rename = "belegungZweite")]
    pub belegung_zweite: String,

    #[serde(rename = "departureTrackLabel")]
    pub departure_track_label: Option<String>,

    #[serde(rename = "departureTrackLabelAccessibility")]
    pub departure_track_label_accessibility: Option<String>,

    #[serde(rename = "durationProzent")]
    pub duration_prozent: Option<String>,

    #[serde(rename = "formationUrl")]
    pub formation_url: Option<String>,

    #[serde(rename = "previewType")]
    pub preview_type: String,

    #[serde(rename = "realtimeInfo")]
    pub realtime_info: SectionRealtimeInfo,

    #[serde(rename = "transportBezeichnung")]
    pub transport_bezeichnung: Option<TransportBezeichnung>,

    #[serde(rename = "transportHinweis")]
    pub transport_hinweis: Option<String>,

    #[serde(rename = "transportServiceAttributes")]
    pub transport_service_attributes: Vec<String>,

    #[serde(rename = "type")]
    pub verbindung_type: String,
}

impl fmt::Display for Verbindung {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut verbindungen= String::new();
        for vs in self.verbindung_sections.as_slice() {
            verbindungen = format!("{}, {}", verbindungen, vs)
        }

        write!(f, "{} ({}): {}", self.transport_bezeichnung, self.duration, verbindungen)
    }
}

impl fmt::Display for VerbindungSection {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} {} {} - {} {}",
                match &self.transport_bezeichnung {
                    Some(t) => format!("{}", t),
                    None => String::new()
                },
                self.abfahrt_name,
                self.abfahrt_time,
                self.ankunft_name,
                self.ankunft_time)
    }
}

fn cap_to_u64(m: Option<regex::bytes::Match>) -> u64 {
    if m.is_none() {
        return 0;
    }

    let s = std::str::from_utf8(m.unwrap().as_bytes()).unwrap_or(&"");
    return u64::from_str(s).unwrap_or(0);
}

impl Verbindung {
    pub fn duration(&self) -> Duration {
        let duration = &self.duration;
        let re = Regex::new(r"^(?:(\d+) h|)(?: |)(?:(\d+) min|)$").unwrap();

        if !re.is_match(duration.as_bytes()) {
            return Duration::from_millis(0)
        }

        for cap in re.captures_iter(duration.as_bytes()) {
            let hours = cap_to_u64(cap.get(1));
            let minutes = cap_to_u64(cap.get(2));


            return Duration::from_secs(hours*60*60 + minutes*60);
        }

        return Duration::from_millis(1)
    }
}

impl AsRef<Verbindung> for Verbindung {
    fn as_ref(&self) -> &Verbindung {
        return self
    }
}

#[cfg(test)]
mod test {
    use std::fs;

    use std::time::Duration;
    use crate::models::results::VerbindungenResults;
    use crate::models::verbindung::Verbindung;

    #[test]
    fn test_verbindung_duration() {
        let f = fs::read("./resources/test/verbindung-1.json")
            .expect("File not found");
    
        let vr : Verbindung = serde_json::from_str(
            std::str::from_utf8(&f)
            .expect("Unable to parse file into string"))
            .expect("Unable to decode from JSON");
    
        assert_eq!(Duration::from_secs(56 * 60), vr.duration())
    }
    
    #[test]
    fn test_verbindung_duration_2() {
        let f = fs::read("./resources/test/verbindung-2.json")
            .expect("File not found");
    
        let vr : Verbindung = serde_json::from_str(
            std::str::from_utf8(&f)
            .expect("Unable to parse file into string"))
            .expect("Unable to decode from JSON");
    
        assert_eq!(Duration::from_secs(1 * 60 * 60 + 5 * 60), vr.duration())
    }

    #[test]
    fn test_decode_verbindung() {
        let f = fs::read("./resources/test/verbindungen-6.json")
            .expect("File not found");

        let vr : VerbindungenResults = serde_json::from_str(
            std::str::from_utf8(&f)
                .expect("Unable to parse file into string"))
            .expect("Unable to decode from JSON");

        println!("Verbindungen = {:?}", vr)
    }

    #[test]
    fn test_decode_verbindung_7() {
        let f = fs::read("./resources/test/verbindungen-7.json")
            .expect("File not found");

        let vr : VerbindungenResults = serde_json::from_str(
            std::str::from_utf8(&f)
                .expect("Unable to parse file into string"))
            .expect("Unable to decode from JSON");

        println!("Verbindungen = {:?}", vr)
    }
}