bahnreise/src/bahn_api/departures_arrivals.rs
2025-08-19 23:42:39 +02:00

286 lines
8.4 KiB
Rust

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, GetDeparturesError> {
departures_or_arrivals(station, transport_modes).await
}
pub async fn arrivals(
station: impl AsStationId<'_>,
transport_modes: TransportModesSet,
) -> Result<Arrivals, GetArrivalsError> {
departures_or_arrivals(station, transport_modes).await
}
pub async fn departures_or_arrivals<T: DepartureOrArrivalData>(
station: impl AsStationId<'_>,
transport_modes: TransportModesSet,
) -> Result<T, T::Err> {
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<Departure>,
}
#[derive(Debug)]
pub struct Arrivals {
pub arrivals: Vec<Arrival>,
}
#[derive(Debug)]
pub struct Departure {
pub id: RouteId,
pub route: String,
pub category: Option<TransportMode>,
pub stops: Vec<NamedStop>,
}
#[derive(Debug)]
pub struct Arrival {
pub id: RouteId,
pub route: String,
pub category: Option<TransportMode>,
pub stops: Vec<NamedStop>,
}
#[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<Self, Self::Err>;
}
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<Self, Self::Err> {
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<Self, Self::Err> {
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<DepDeparture>,
}
#[derive(Deserialize)]
pub struct ArrData {
#[serde(rename = "entries")]
arrivals: Vec<ArrArrival>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DepDeparture {
#[serde(rename = "ueber")]
stops: Vec<String>,
journey_id: String,
#[serde(default)]
terminus: Option<String>,
#[serde(rename = "verkehrmittel")]
vehicle: DepVehicle,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ArrArrival {
#[serde(rename = "ueber")]
stops: Vec<String>,
journey_id: String,
#[serde(default)]
terminus: Option<String>,
#[serde(rename = "verkehrmittel")]
vehicle: ArrVehicle,
}
#[derive(Deserialize)]
struct DepVehicle {
#[serde(rename = "mittelText")]
route: String,
#[serde(default, rename = "produktGattung")]
category: Option<String>,
}
#[derive(Deserialize)]
struct ArrVehicle {
#[serde(rename = "mittelText")]
route: String,
#[serde(default, rename = "produktGattung")]
category: Option<String>,
}
}