use deserialize::{DepartureOrArrivalData, Direction, GetDepartureOrArrivalError}; use reqwest::Url; use crate::bahn_api::transport_modes::TransportMode; use super::{ basic_types::{ route_ids::RouteId, station_ids::{AsStationId, StationIdRef}, }, transport_modes::TransportModesSet, }; pub async fn departures( station: impl AsStationId<'_>, transport_modes: TransportModesSet, ) -> Result { departures_or_arrivals(station, transport_modes).await } pub async fn arrivals( station: impl AsStationId<'_>, transport_modes: TransportModesSet, ) -> Result { departures_or_arrivals(station, transport_modes).await } pub async fn departures_or_arrivals( station: impl AsStationId<'_>, transport_modes: TransportModesSet, ) -> Result { let StationIdRef { eva_number, db_station_id, } = station.into(); let response = reqwest::get( Url::parse_with_params( match T::DIRECTION { Direction::Departure => "https://www.bahn.de/web/api/reiseloesung/abfahrten", Direction::Arrival => "https://www.bahn.de/web/api/reiseloesung/ankuenfte", }, [ ("ortExtId", eva_number.as_str()), ("ortId", db_station_id.as_str()), ("mitVias", "true"), ("maxVias", "8"), ] .into_iter() .chain( transport_modes .into_iter() .map(|v| ("verkehrsmittel[]", v.as_str_for_bahn_api())), ), ) .expect("hardcoded url and generated parameters should always be valid"), ) .await .map_err(T::Err::network_error)? .error_for_status() .map_err(T::Err::network_error)? .text() .await .map_err(T::Err::network_error)?; let data: T::De = serde_json::from_str(&response).map_err(move |e| T::Err::parse_error(response, e))?; T::convert(data) } #[derive(Debug)] pub struct Departures { pub departures: Vec, } #[derive(Debug)] pub struct Arrivals { pub arrivals: Vec, } #[derive(Debug)] pub struct Departure { pub id: RouteId, pub route: String, pub category: Option, pub stops: Vec, } #[derive(Debug)] pub struct Arrival { pub id: RouteId, pub route: String, pub category: Option, pub stops: Vec, } #[derive(Debug)] pub struct NamedStop { pub name: String, } #[derive(Debug)] pub enum GetDeparturesError { NetworkError(reqwest::Error), ParseError(String, serde_json::Error), } #[derive(Debug)] pub enum GetArrivalsError { NetworkError(reqwest::Error), ParseError(String, serde_json::Error), } impl GetDepartureOrArrivalError for GetDeparturesError { fn network_error(err: reqwest::Error) -> Self { Self::NetworkError(err) } fn parse_error(str: String, err: serde_json::Error) -> Self { Self::ParseError(str, err) } } impl GetDepartureOrArrivalError for GetArrivalsError { fn network_error(err: reqwest::Error) -> Self { Self::NetworkError(err) } fn parse_error(str: String, err: serde_json::Error) -> Self { Self::ParseError(str, err) } } pub mod deserialize { use serde::Deserialize; use crate::bahn_api::{basic_types::route_ids::RouteId, transport_modes::TransportMode}; use super::{ Arrival, Arrivals, Departure, Departures, GetArrivalsError, GetDeparturesError, NamedStop, }; pub enum Direction { Departure, Arrival, } pub trait DepartureOrArrivalData: Sized { const DIRECTION: Direction; type De: for<'de> Deserialize<'de>; type Err: GetDepartureOrArrivalError; fn convert(data: Self::De) -> Result; } pub trait GetDepartureOrArrivalError { fn network_error(err: reqwest::Error) -> Self; fn parse_error(str: String, err: serde_json::Error) -> Self; } impl DepartureOrArrivalData for Departures { const DIRECTION: Direction = Direction::Departure; type De = DepData; type Err = GetDeparturesError; fn convert(mut data: Self::De) -> Result { for departure in &mut data.departures { // add terminus to the stops list if it isn't the last stop already if let Some(terminus) = departure.terminus.take() { if departure.stops.last().is_none_or(|stop| *stop != terminus) { departure.stops.push(terminus); } } } Ok(Self { departures: data .departures .into_iter() .map(|dep| Departure { id: RouteId(dep.journey_id), route: dep.vehicle.route, category: dep .vehicle .category .and_then(|cat| TransportMode::from_str_from_bahn_api(cat.trim())), stops: dep .stops .into_iter() .map(|name| NamedStop { name }) .collect(), }) .collect(), }) } } impl DepartureOrArrivalData for Arrivals { const DIRECTION: Direction = Direction::Arrival; type De = ArrData; type Err = GetArrivalsError; fn convert(mut data: Self::De) -> Result { for departure in &mut data.arrivals { // add terminus to the stops list if it isn't the first or last stop already // NOTE: the bahn.de api currently sets `terminus` to the *first* station when arrivals are requested if let Some(terminus) = departure.terminus.take() { if [departure.stops.first(), departure.stops.last()] .into_iter() .flatten() .all(|stop| *stop != terminus) { departure.stops.insert(0, terminus); } } } Ok(Self { arrivals: data .arrivals .into_iter() .map(|arr| Arrival { id: RouteId(arr.journey_id), route: arr.vehicle.route, category: arr .vehicle .category .and_then(|cat| TransportMode::from_str_from_bahn_api(cat.trim())), stops: arr .stops .into_iter() .map(|name| NamedStop { name }) .collect(), }) .collect(), }) } } #[derive(Deserialize)] pub struct DepData { #[serde(rename = "entries")] departures: Vec, } #[derive(Deserialize)] pub struct ArrData { #[serde(rename = "entries")] arrivals: Vec, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct DepDeparture { #[serde(rename = "ueber")] stops: Vec, journey_id: String, #[serde(default)] terminus: Option, #[serde(rename = "verkehrmittel")] vehicle: DepVehicle, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct ArrArrival { #[serde(rename = "ueber")] stops: Vec, journey_id: String, #[serde(default)] terminus: Option, #[serde(rename = "verkehrmittel")] vehicle: ArrVehicle, } #[derive(Deserialize)] struct DepVehicle { #[serde(rename = "mittelText")] route: String, #[serde(default, rename = "produktGattung")] category: Option, } #[derive(Deserialize)] struct ArrVehicle { #[serde(rename = "mittelText")] route: String, #[serde(default, rename = "produktGattung")] category: Option, } }