railway-api 0.3.1

High-level API for the Railway backend
Documentation
#![doc = include_str!("../README.md")]

mod error;
pub use error::*;

use rcore::Requester;

// XXX: Ugly that this needs to be hard-coded; most of the enum is general over the requester, but the Motis one is not.
type R = rcore::ReqwestRequester;

// Note: The derive macro automatically generates the following:
// - `RailwayProviderType`: The enum equivalent to this enum without the data for each enum variant. It furthermore generates `FromStr` and `Display` for it. There is also a `RailwayProviderType::variants` which lists all providers that are available.
// - `RailwayProvider::new`: Construct `RailwayProvider` from `RailwayProviderType` and `R: Requester` using specified `constructor`.
// - `impl<R: Requester> Provider<R> for RailwayProvider<R>`: Using error `BoxedError` and directly passing through the wrapped type.
// It also looks for `#[cfg(...)]` and enables the specific code only if requested.
/// A wrapper around all implemented [`Provider`s](rcore::Provider).
///
/// Note that the variants of this enum depend on the features activated via the API.
#[derive(Clone, rapi_derive::ProviderApi)]
pub enum RailwayProvider {
    // Hafas
    #[cfg(feature = "vbb-provider")]
    #[provider(hafas = rhafas::profile::vbb::VbbProfile {})]
    Vbb(rhafas::client::HafasClient<R>),
    #[cfg(feature = "oebb-provider")]
    #[provider(hafas = rhafas::profile::oebb::OebbProfile {})]
    Oebb(rhafas::client::HafasClient<R>),
    #[cfg(feature = "nahsh-provider")]
    #[provider(hafas = rhafas::profile::nahsh::NahSHProfile {})]
    NahSh(rhafas::client::HafasClient<R>),
    #[cfg(feature = "vvt-provider")]
    #[provider(hafas = rhafas::profile::vvt::VvtProfile {})]
    Vvt(rhafas::client::HafasClient<R>),
    #[cfg(feature = "pkp-provider")]
    #[provider(hafas = rhafas::profile::pkp::PkpProfile {})]
    Pkp(rhafas::client::HafasClient<R>),
    #[cfg(feature = "irish-rail-provider")]
    #[provider(hafas = rhafas::profile::irish_rail::IrishRailProfile {})]
    IrishRail(rhafas::client::HafasClient<R>),
    #[cfg(feature = "mobiliteit-lu-provider")]
    #[provider(hafas = rhafas::profile::mobiliteit_lu::MobiliteitLuProfile {})]
    MobiliteitLu(rhafas::client::HafasClient<R>),
    #[cfg(feature = "dart-provider")]
    #[provider(hafas = rhafas::profile::dart::DartProfile {})]
    Dart(rhafas::client::HafasClient<R>),
    #[cfg(feature = "rmv-provider")]
    #[provider(hafas = rhafas::profile::rmv::RmvProfile {})]
    Rmv(rhafas::client::HafasClient<R>),
    #[cfg(feature = "insa-provider")]
    #[provider(hafas = rhafas::profile::insa::InsaProfile {})]
    Insa(rhafas::client::HafasClient<R>),
    #[cfg(feature = "cmta-provider")]
    #[provider(hafas = rhafas::profile::cmta::CmtaProfile {})]
    Cmta(rhafas::client::HafasClient<R>),
    #[cfg(feature = "sbahn-muenchen-provider")]
    #[provider(hafas = rhafas::profile::sbahn_muenchen::SBahnMuenchenProfile {})]
    SBahnMuenchen(rhafas::client::HafasClient<R>),
    #[cfg(feature = "saarvv-provider")]
    #[provider(hafas = rhafas::profile::saarvv::SaarvvProfile {})]
    Saarvv(rhafas::client::HafasClient<R>),
    #[cfg(feature = "cfl-provider")]
    #[provider(hafas = rhafas::profile::cfl::CflProfile {})]
    Cfl(rhafas::client::HafasClient<R>),
    #[cfg(feature = "nvv-provider")]
    #[provider(hafas = rhafas::profile::nvv::NvvProfile {})]
    Nvv(rhafas::client::HafasClient<R>),
    #[cfg(feature = "mobil-nrw-provider")]
    #[provider(hafas = rhafas::profile::mobil_nrw::MobilNrwProfile {})]
    MobilNrw(rhafas::client::HafasClient<R>),
    #[cfg(feature = "vsn-provider")]
    #[provider(hafas = rhafas::profile::vsn::VsnProfile {})]
    Vsn(rhafas::client::HafasClient<R>),
    #[cfg(feature = "vgi-provider")]
    #[provider(hafas = rhafas::profile::vgi::VgiProfile {})]
    Vgi(rhafas::client::HafasClient<R>),
    #[cfg(feature = "vbn-provider")]
    #[provider(hafas = rhafas::profile::vbn::VbnProfile {})]
    Vbn(rhafas::client::HafasClient<R>),
    #[cfg(feature = "vrn-provider")]
    #[provider(hafas = rhafas::profile::vrn::VrnProfile {})]
    Vrn(rhafas::client::HafasClient<R>),
    #[cfg(feature = "rsag-provider")]
    #[provider(hafas = rhafas::profile::rsag::RsagProfile {})]
    Rsag(rhafas::client::HafasClient<R>),
    #[cfg(feature = "vmt-provider")]
    #[provider(hafas = rhafas::profile::vmt::VmtProfile {})]
    Vmt(rhafas::client::HafasClient<R>),
    #[cfg(feature = "vos-provider")]
    #[provider(hafas = rhafas::profile::vos::VosProfile {})]
    Vos(rhafas::client::HafasClient<R>),
    #[cfg(feature = "avv-provider")]
    #[provider(hafas = rhafas::profile::avv::AvvProfile {})]
    Avv(rhafas::client::HafasClient<R>),
    #[cfg(feature = "rejseplanen-provider")]
    #[provider(hafas = rhafas::profile::rejseplanen::RejseplanenProfile {})]
    Rejseplanen(rhafas::client::HafasClient<R>),
    #[cfg(feature = "ooevv-provider")]
    #[provider(hafas = rhafas::profile::ooevv::OoevvProfile {})]
    Ooevv(rhafas::client::HafasClient<R>),
    #[cfg(feature = "salzburg-provider")]
    #[provider(hafas = rhafas::profile::salzburg::SalzburgProfile {})]
    Salzburg(rhafas::client::HafasClient<R>),
    #[cfg(feature = "verbundlinie-provider")]
    #[provider(hafas = rhafas::profile::verbundlinie::VerbundlinieProfile {})]
    Verbundlinie(rhafas::client::HafasClient<R>),
    #[cfg(feature = "svv-provider")]
    #[provider(hafas = rhafas::profile::svv::SvvProfile {})]
    Svv(rhafas::client::HafasClient<R>),
    #[cfg(feature = "vor-provider")]
    #[provider(hafas = rhafas::profile::vor::VorProfile {})]
    Vor(rhafas::client::HafasClient<R>),
    #[cfg(feature = "vkg-provider")]
    #[provider(hafas = rhafas::profile::vkg::VkgProfile {})]
    Vkg(rhafas::client::HafasClient<R>),
    #[cfg(feature = "vvv-provider")]
    #[provider(hafas = rhafas::profile::vvv::VvvProfile {})]
    Vvv(rhafas::client::HafasClient<R>),
    #[cfg(feature = "bls-provider")]
    #[provider(hafas = rhafas::profile::bls::BlsProfile {})]
    Bls(rhafas::client::HafasClient<R>),
    #[cfg(feature = "kvb-provider")]
    #[provider(hafas = rhafas::profile::kvb::KvbProfile {})]
    Kvb(rhafas::client::HafasClient<R>),
    #[cfg(feature = "bart-provider")]
    #[provider(hafas = rhafas::profile::bart::BartProfile {})]
    Bart(rhafas::client::HafasClient<R>),
    #[cfg(feature = "ivb-provider")]
    #[provider(hafas = rhafas::profile::ivb::IvbProfile {})]
    Ivb(rhafas::client::HafasClient<R>),
    #[cfg(feature = "resrobot-provider")]
    #[provider(hafas = rhafas::profile::resrobot::ResrobotProfile {})]
    Resrobot(rhafas::client::HafasClient<R>),

    // Search.ch
    #[cfg(feature = "search-ch-provider")]
    #[provider(constructor = rsearchch::SearchChClient::new)]
    SearchCh(rsearchch::SearchChClient<R>),

    // Transitous
    #[cfg(feature = "transitous-provider")]
    #[provider(constructor = |r| rmotis::MotisClient::new(url::Url::parse(rmotis::TRANSITOUS_URL).expect("Failed to parse TRANSITOUS_URL"), r))]
    Transitous(rmotis::MotisClient),

    // DB Movas
    #[cfg(feature = "db-provider")]
    #[provider(constructor = rdb::DbMovasClient::new)]
    Db(rdb::DbMovasClient<R>),
}

#[cfg(test)]
mod api_test {
    use super::*;

    #[test]
    #[cfg(feature = "db-provider")]
    fn macro_creates_enum_types() {
        let _ = RailwayProviderType::Db;
    }

    #[test]
    #[cfg(feature = "db-provider")]
    fn macro_creates_enum_type_variants() {
        let list = RailwayProviderType::variants();
        assert!(list.len() >= 1);
    }

    #[test]
    #[cfg(feature = "db-provider")]
    fn macro_creates_enum_from_to_string() {
        use std::str::FromStr;
        assert_eq!(
            RailwayProviderType::from_str("db").unwrap(),
            RailwayProviderType::Db
        );
        assert_eq!(RailwayProviderType::Db.to_string(), "db".to_owned());
    }

    #[test]
    #[cfg(feature = "db-provider")]
    fn macro_creates_constructor() {
        let _ = RailwayProvider::new(
            RailwayProviderType::Db,
            rcore::ReqwestRequesterBuilder::default(),
        );
    }

    #[test]
    #[cfg(feature = "db-provider")]
    fn macro_implements_provider() {
        use rcore::Provider;

        let client = RailwayProvider::new(
            RailwayProviderType::Db,
            rcore::ReqwestRequesterBuilder::default(),
        );
        let _ = Box::new(client) as Box<dyn Provider<rcore::ReqwestRequester, Error = BoxedError>>;
    }

    #[test]
    #[cfg(feature = "all-providers")]
    fn readme_supported_up_to_date() {
        use std::collections::HashSet;

        let supported: HashSet<_> = RailwayProviderType::variants()
            .into_iter()
            .map(|p| p.to_string().to_lowercase().replace(&['-', '_'], ""))
            .collect();
        let readme = include_str!("../../README.md").lines();

        let entries: HashSet<_> = readme
            // Go to profiles-section.
            .skip_while(|l| l != &"## Profiles")
            // Go to first table.
            .skip_while(|l| !l.starts_with("|"))
            // Skip header.
            .skip(2)
            // Take rows
            .take_while(|l| l.starts_with("|"))
            // Take first column
            .flat_map(|r| r.split('|').nth(1))
            .map(|s| s.trim().to_lowercase().replace(&['-', '_', '.'], ""))
            .collect();

        let extra: HashSet<_> = entries.difference(&supported).collect();
        let missing: HashSet<_> = supported.difference(&entries).collect();

        if !(extra.is_empty() && missing.is_empty()) {
            println!("Extra Providers: {:#?}", extra);
            println!("Missing Providers: {:#?}", missing);
            panic!("Mismatched README and available providers");
        }
    }

    #[test]
    #[cfg(feature = "all-providers")]
    fn hafas_all_providers_up_to_date() {
        use serde::Deserialize;
        use std::collections::{HashMap, HashSet};
        use toml;

        #[derive(Deserialize)]
        struct CrateCargo {
            features: HashMap<String, Vec<String>>,
        }

        let hafas_cargo = include_str!("../../railway-provider-hafas/Cargo.toml");
        let cargo: CrateCargo = toml::from_str(hafas_cargo).unwrap();

        let hafas_supported_profiles: HashSet<_> = cargo
            .features
            .keys()
            .filter_map(|s| s.strip_suffix("-profile"))
            .map(|p| p.to_string().to_lowercase().replace(['-', '_'], ""))
            .collect();
        let hafas_all_profiles: HashSet<_> = cargo.features["all-profiles"]
            .iter()
            .map(|p| {
                p.strip_suffix("-profile")
                    .unwrap_or_default()
                    .to_string()
                    .to_lowercase()
                    .replace(['-', '_'], "")
            })
            .collect();

        let supported: HashSet<_> = RailwayProviderType::variants()
            .iter()
            .map(|p| p.to_string().to_lowercase().replace(['-', '_'], ""))
            .collect();

        let supported_hafas = supported
            .intersection(&hafas_supported_profiles)
            .map(|s| s.to_string())
            .collect();

        let extra: HashSet<_> = hafas_all_profiles.difference(&supported_hafas).collect();
        let missing: HashSet<_> = supported_hafas.difference(&hafas_all_profiles).collect();

        if !(extra.is_empty() && missing.is_empty()) {
            println!("Extra Providers: {:#?}", extra);
            println!("Missing Providers: {:#?}", missing);
            panic!("Mismatched README and available providers");
        }
    }
}