From f74c9c2ae9df747af3c75657734bf39cf759ce6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emma=20B=C3=B6cker?= Date: Mon, 5 Feb 2024 22:32:57 +0100 Subject: [PATCH] big refactor --- Cargo.lock | 4 +- iris-client/src/endpoints/station_board.rs | 2 +- iris-client/src/lib.rs | 2 +- railboard-api/src/cache.rs | 32 ++- railboard-api/src/custom.rs | 24 +- railboard-api/src/custom/station_board.rs | 82 +++--- railboard-api/src/error.rs | 5 + railboard-api/src/iris.rs | 21 +- railboard-api/src/iris/station_board.rs | 64 +++-- railboard-api/src/main.rs | 236 +++++++++-------- railboard-api/src/ris.rs | 35 +-- railboard-api/src/ris/journey_details.rs | 233 ++--------------- railboard-api/src/ris/journey_search.rs | 12 +- railboard-api/src/ris/station_board.rs | 238 ++---------------- railboard-api/src/ris/station_information.rs | 104 ++------ .../src/ris/station_search_by_name.rs | 19 +- railboard-api/src/vendo.rs | 20 +- railboard-api/src/vendo/journey_details.rs | 166 ++---------- railboard-api/src/vendo/location_search.rs | 18 +- railboard-api/src/vendo/station_board.rs | 183 ++------------ ris-client/Cargo.toml | 2 +- ris-client/src/endpoints/journey_details.rs | 143 ++++++++++- .../endpoints/journey_details/transformed.rs | 130 ++++++++++ ris-client/src/endpoints/journey_search.rs | 28 +-- ris-client/src/endpoints/station_board.rs | 134 +++++++++- .../src/endpoints/station_board/response.rs | 4 +- .../endpoints/station_board/transformed.rs | 63 +++++ .../src/endpoints/station_information.rs | 26 +- .../endpoints/station_information/response.rs | 2 +- .../station_information/transformed.rs | 61 +++++ ris-client/src/endpoints/station_search.rs | 19 +- .../src/endpoints/station_search/response.rs | 29 ++- ris-client/src/error.rs | 2 + {railboard-api => ris-client}/src/helpers.rs | 0 ris-client/src/lib.rs | 5 +- ris-client/src/request.rs | 11 + ris-client/tests/journey_details.rs | 5 +- ris-client/tests/station_information.rs | 6 +- ris-client/tests/station_search.rs | 6 +- vendo-client/Cargo.toml | 2 +- vendo-client/src/endpoints/journey_details.rs | 77 +++++- .../src/endpoints/journey_details/response.rs | 3 +- .../endpoints/journey_details/transformed.rs | 74 ++++++ vendo-client/src/endpoints/location_search.rs | 5 +- .../src/endpoints/location_search/response.rs | 6 +- vendo-client/src/endpoints/station_board.rs | 110 +++++++- .../endpoints/station_board/transformed.rs | 44 ++++ vendo-client/src/lib.rs | 8 +- .../types.rs => vendo-client/src/shared.rs | 0 vendo-client/tests/journey_details.rs | 11 +- vendo-client/tests/station_board.rs | 2 +- 51 files changed, 1297 insertions(+), 1221 deletions(-) create mode 100644 ris-client/src/endpoints/journey_details/transformed.rs create mode 100644 ris-client/src/endpoints/station_board/transformed.rs create mode 100644 ris-client/src/endpoints/station_information/transformed.rs rename {railboard-api => ris-client}/src/helpers.rs (100%) create mode 100644 ris-client/src/request.rs create mode 100644 vendo-client/src/endpoints/journey_details/transformed.rs create mode 100644 vendo-client/src/endpoints/station_board/transformed.rs rename railboard-api/src/types.rs => vendo-client/src/shared.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index fb48a45..67b2e67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1195,7 +1195,7 @@ dependencies = [ [[package]] name = "ris-client" -version = "0.1.0" +version = "0.2.0" dependencies = [ "async-lock", "chrono", @@ -1820,7 +1820,7 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "vendo-client" -version = "0.1.0" +version = "0.2.0" dependencies = [ "async-lock", "chrono", diff --git a/iris-client/src/endpoints/station_board.rs b/iris-client/src/endpoints/station_board.rs index da6c0f1..af8bfdf 100644 --- a/iris-client/src/endpoints/station_board.rs +++ b/iris-client/src/endpoints/station_board.rs @@ -112,7 +112,7 @@ impl IrisClient { /// Get all planned information IRIS has for a specific station at the specified date + hour. /// - /// From experience IRIS does not have any more planned data than the current day + maybe a bit of the early hours of the next day. + /// From experience IRIS does not have more planned data than the current day + maybe a bit of the early hours of the next day. /// /// **Consider using [`station_board`](IrisClient::station_board) instead.** \ /// diff --git a/iris-client/src/lib.rs b/iris-client/src/lib.rs index 103ad0c..6cb52d5 100644 --- a/iris-client/src/lib.rs +++ b/iris-client/src/lib.rs @@ -31,7 +31,7 @@ impl IrisClient { concurrent_requests: Option, ) -> Self { Self { - client: client.unwrap_or_else(reqwest::Client::new), + client: client.unwrap_or_default(), base_url: base_url.unwrap_or_else(|| String::from("https://iris.noncd.db.de")), semaphore: Semaphore::new(concurrent_requests.unwrap_or(100)), } diff --git a/railboard-api/src/cache.rs b/railboard-api/src/cache.rs index fb81d07..236af4e 100644 --- a/railboard-api/src/cache.rs +++ b/railboard-api/src/cache.rs @@ -5,21 +5,16 @@ use chrono_tz::Europe::Berlin; use iris_client::station_board::response::TimeTable; use redis::JsonAsyncCommands; use ris_client::{ - journey_search::RisJourneySearchResponse, station_search::RisStationSearchResponse, + journey_search::RisJourneySearchResponse, station_search::RisStationSearchElement, }; use serde::{de::DeserializeOwned, Serialize}; use thiserror::Error; - -use crate::{ - ris::{ - journey_details::RisJourneyDetails, station_board::RisStationBoard, - station_information::StationInformation, - }, - vendo::{ - journey_details::JourneyDetails, location_search::LocationSearchCache, - station_board::StationBoard, - }, -}; +use ris_client::journey_details::RisJourneyDetails; +use ris_client::station_board::RisStationBoard; +use ris_client::station_information::RisStationInformation; +use vendo_client::journey_details::VendoJourneyDetails; +use vendo_client::station_board::VendoStationBoard; +use crate::vendo::location_search::LocationSearchCache; #[async_trait::async_trait] pub trait Cache: Sync + Send { @@ -43,6 +38,7 @@ pub enum CacheInsertError { RedisError(#[from] redis::RedisError), } +#[derive(Clone)] pub struct RedisCache { pub redis_client: Arc, } @@ -132,7 +128,7 @@ pub trait CachableObject { } #[async_trait::async_trait] -impl CachableObject for StationBoard { +impl CachableObject for VendoStationBoard { async fn insert_to_cache( &self, cache: &C, @@ -158,7 +154,7 @@ impl CachableObject for LocationSearchCache { } #[async_trait::async_trait] -impl CachableObject for JourneyDetails { +impl CachableObject for VendoJourneyDetails { async fn insert_to_cache( &self, cache: &C, @@ -253,7 +249,7 @@ impl CachableObject for RisStationBoard { } #[async_trait::async_trait] -impl CachableObject for StationInformation { +impl CachableObject for RisStationInformation { async fn insert_to_cache( &self, cache: &C, @@ -266,14 +262,14 @@ impl CachableObject for StationInformation { } #[async_trait::async_trait] -impl CachableObject for RisStationSearchResponse { +impl CachableObject for Vec { async fn insert_to_cache( &self, cache: &C, information: Option<&str>, ) -> Result<(), CacheInsertError> { - let key = format!("ris.station_search_by_name.{}", information.unwrap_or("")); + let key = format!("ris.station-search-by-name.{}", information.unwrap_or("")); - cache.insert_to_cache(key, &self, 180).await + cache.insert_to_cache(key, &self, 60 * 60).await } } diff --git a/railboard-api/src/custom.rs b/railboard-api/src/custom.rs index 5a4088f..8279b46 100644 --- a/railboard-api/src/custom.rs +++ b/railboard-api/src/custom.rs @@ -1,30 +1,12 @@ use std::sync::Arc; -use axum::{routing::get, Router}; -use iris_client::IrisClient; -use ris_client::RisClient; +use axum::{Router, routing::get}; -use crate::cache::{self, RedisCache}; +use crate::SharedState; pub mod station_board; -#[derive(Clone)] -pub struct CustomEndpointState { - iris_client: Arc, - ris_client: Arc, - cache: Arc, -} - -pub fn router(redis: Arc, db_client_id: &str, db_api_key: &str) -> Router { - let ris_client = Arc::new(RisClient::new(None, None, None, db_client_id, db_api_key)); - - let shared_state = Arc::new(CustomEndpointState { - iris_client: Arc::new(IrisClient::default()), - ris_client, - cache: Arc::new(cache::RedisCache::new(redis)), - }); - +pub fn router() -> Router> { Router::new() .route("/station_board/:id", get(station_board::station_board)) - .with_state(shared_state) } diff --git a/railboard-api/src/custom/station_board.rs b/railboard-api/src/custom/station_board.rs index 785757c..00e35b3 100644 --- a/railboard-api/src/custom/station_board.rs +++ b/railboard-api/src/custom/station_board.rs @@ -4,18 +4,14 @@ use axum::{ extract::{Path, Query, State}, Json, }; -use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Utc}; +use chrono::{Datelike, DateTime, FixedOffset, TimeZone, Utc}; use chrono_tz::Europe::Berlin; -use iris_client::station_board::{message::Message, IrisStationBoard, RouteStop}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::{ - error::RailboardResult, iris::station_board::iris_station_board, - ris::station_board::ris_station_board, -}; +use iris_client::station_board::{IrisStationBoard, message::Message, RouteStop}; -use super::CustomEndpointState; +use crate::{error::RailboardResult, iris::station_board::iris_station_board, SharedState}; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -25,24 +21,24 @@ pub struct StationBoardQuery { } #[utoipa::path( - get, - path = "/v1/station_board/{eva}", - params( - ("eva" = String, Path, description = "The Eva Number of the Station you are requesting"), - ("timeStart" = Option>, Query, description = "The Start Time of the Time Range you are requesting"), - ("timeEnd" = Option>, Query, description = "The End Time of the Time Range you are requesting") - ), - tag = "Custom", - responses( - (status = 200, description = "The requested Station Board", body = StationBoard), - (status = 400, description = "The Error returned by the Ris or Iris, will be Variant 2 or Variant 5", body = RailboardApiError), - (status = 500, description = "The Error returned if the request or deserialization fails, will be domain Request", body = RailboardApiError) - ) +get, +path = "/v1/station_board/{eva}", +params( +("eva" = String, Path, description = "The Eva Number of the Station you are requesting"), +("timeStart" = Option < DateTime < FixedOffset >>, Query, description = "The Start Time of the Time Range you are requesting"), +("timeEnd" = Option < DateTime < FixedOffset >>, Query, description = "The End Time of the Time Range you are requesting") +), +tag = "Custom", +responses( +(status = 200, description = "The requested Station Board", body = StationBoard), +(status = 400, description = "The Error returned by the Ris or Iris, will be Variant 2 or Variant 5", body = RailboardApiError), +(status = 500, description = "The Error returned if the request or deserialization fails, will be domain Request", body = RailboardApiError) +) )] pub async fn station_board( Path(eva): Path, Query(query): Query, - State(state): State>, + State(state): State>, ) -> RailboardResult> { let time_start = if let Some(time_start) = query.time_start { Berlin.from_utc_datetime(&time_start.naive_utc()) @@ -57,19 +53,17 @@ pub async fn station_board( }; let (ris_station_board, iris_station_board) = tokio::join!( - ris_station_board( + state.ris_client.station_board( &eva, Some(time_start), Some(time_end), - state.ris_client.clone(), - state.cache.clone() ), iris_station_board( &eva, time_end, time_start, state.iris_client.clone(), - state.cache.clone() + &state.cache ) ); @@ -92,18 +86,18 @@ pub async fn station_board( iris_item.train_number == item.train_number.to_string() && iris_item.train_type == item.category && (iris_item - .arrival - .clone() - .map(|arrival| arrival.planned_time.naive_utc().date().day()) - == item - .arrival - .clone() - .map(|arrival| arrival.time_scheduled.naive_utc().date().day()) - || iris_item.departure.clone().map(|departure| { - departure.planned_time.naive_utc().date().day() - }) == item.departure.clone().map(|departure| { - departure.time_scheduled.naive_utc().date().day() - })) + .arrival + .clone() + .map(|arrival| arrival.planned_time.naive_utc().date().day()) + == item + .arrival + .clone() + .map(|arrival| arrival.time_scheduled.naive_utc().date().day()) + || iris_item.departure.clone().map(|departure| { + departure.planned_time.naive_utc().date().day() + }) == item.departure.clone().map(|departure| { + departure.time_scheduled.naive_utc().date().day() + })) }); let iris_item = iris_item.cloned(); @@ -176,13 +170,13 @@ pub async fn station_board( }) .unwrap_or(false) || stop - .departure - .as_ref() - .map(|departure| { - departure.planned_time.naive_utc() >= time_start.naive_utc() - && departure.planned_time.naive_utc() <= time_end.naive_utc() - }) - .unwrap_or(false) + .departure + .as_ref() + .map(|departure| { + departure.planned_time.naive_utc() >= time_start.naive_utc() + && departure.planned_time.naive_utc() <= time_end.naive_utc() + }) + .unwrap_or(false) }) { if !items .iter() diff --git a/railboard-api/src/error.rs b/railboard-api/src/error.rs index 048bd01..d8ffb1f 100644 --- a/railboard-api/src/error.rs +++ b/railboard-api/src/error.rs @@ -129,6 +129,11 @@ impl From for RailboardApiError { message: format!("Failed to get from Ris (through Zugportal): {err}"), error: Some(UnderlyingApiError::ZugportalError(err)), }, + RisOrRequestError::NotFoundError => RailboardApiError { + domain: ErrorDomain::Input, + message: "There was nothing found with these parameters".to_string(), + error: None, + } } } } diff --git a/railboard-api/src/iris.rs b/railboard-api/src/iris.rs index 774f8c2..8279b46 100644 --- a/railboard-api/src/iris.rs +++ b/railboard-api/src/iris.rs @@ -1,27 +1,12 @@ use std::sync::Arc; -use axum::{routing::get, Router}; -use iris_client::IrisClient; +use axum::{Router, routing::get}; -use crate::cache::{self, RedisCache}; +use crate::SharedState; pub mod station_board; -#[derive(Clone)] -pub struct IrisState { - iris_client: Arc, - cache: Arc, -} - -pub fn router(redis: Arc) -> Router { - let iris_client = Arc::new(IrisClient::default()); - - let shared_state = Arc::new(IrisState { - iris_client, - cache: Arc::new(cache::RedisCache::new(redis)), - }); - +pub fn router() -> Router> { Router::new() .route("/station_board/:id", get(station_board::station_board)) - .with_state(shared_state) } diff --git a/railboard-api/src/iris/station_board.rs b/railboard-api/src/iris/station_board.rs index 7a14326..f518109 100644 --- a/railboard-api/src/iris/station_board.rs +++ b/railboard-api/src/iris/station_board.rs @@ -4,21 +4,17 @@ use axum::{ extract::{Path, Query, State}, Json, }; -use chrono::{DateTime, Duration, FixedOffset, TimeZone, Timelike}; +use chrono::{DateTime, Duration, FixedOffset, Timelike, TimeZone}; use chrono_tz::{Europe::Berlin, Tz}; -use iris_client::{ - station_board::{from_iris_timetable, response::TimeTable, IrisStationBoard}, - IrisClient, IrisOrRequestError, -}; use serde::Deserialize; use utoipa::IntoParams; -use crate::{ - cache::{self, CachableObject, Cache}, - error::RailboardResult, +use iris_client::{ + IrisClient, + IrisOrRequestError, station_board::{from_iris_timetable, IrisStationBoard, response::TimeTable}, }; -use super::IrisState; +use crate::{cache::{self, CachableObject, Cache}, error::RailboardResult, SharedState}; #[derive(Deserialize, IntoParams)] pub struct IrisStationBoardQuery { @@ -31,24 +27,24 @@ pub struct IrisStationBoardQuery { } #[utoipa::path( - get, - path = "/iris/v1/station_board/{eva}", - params( - ("eva" = String, Path, description = "The eva number of the Station you are requesting"), - IrisStationBoardQuery - ), - tag = "Iris", - responses( - (status = 200, description = "The requested Station Board", body = IrisStationBoard), - (status = 400, description = "The Error returned by Iris, will be the Iris Domain with UnderlyingApiError Variant 2, which has no Information because Iris doesn't return errors", body = RailboardApiError), - (status = 500, description = "The Error returned if the request or deserialization fails", body = RailboardApiError) - ) +get, +path = "/iris/v1/station_board/{eva}", +params( +("eva" = String, Path, description = "The eva number of the Station you are requesting"), +IrisStationBoardQuery +), +tag = "Iris", +responses( +(status = 200, description = "The requested Station Board", body = IrisStationBoard), +(status = 400, description = "The Error returned by Iris, will be the Iris Domain with UnderlyingApiError Variant 2, which has no Information because Iris doesn't return errors", body = RailboardApiError), +(status = 500, description = "The Error returned if the request or deserialization fails", body = RailboardApiError) +) )] pub async fn station_board( Path(eva): Path, Query(params): Query, - State(state): State>, -) -> RailboardResult> { + State(state): State>, +) -> RailboardResult> { let lookbehind = params.lookbehind.unwrap_or(20); let lookahead = params.lookahead.unwrap_or(180); @@ -66,9 +62,9 @@ pub async fn station_board( lookahead, lookbehind, state.iris_client.clone(), - state.cache.clone(), + &state.cache, ) - .await?; + .await?; Ok(Json(station_board)) } @@ -78,8 +74,8 @@ pub async fn iris_station_board( lookahead: DateTime, lookbehind: DateTime, iris_client: Arc, - cache: Arc, -) -> RailboardResult { + cache: &cache::RedisCache, +) -> RailboardResult { let mut dates = Vec::new(); for current_date in DateRange(lookbehind, lookahead) { @@ -87,11 +83,10 @@ pub async fn iris_station_board( } let (realtime, timetables) = tokio::join!( - get_realtime(iris_client.clone(), cache.clone(), eva), + get_realtime(iris_client.clone(), cache, eva), futures::future::join_all(dates.iter().map(|date| async { if let Some(cached) = cache - .as_ref() - .get_from_id::(&format!( + .get_from_id::(&format!( "iris.station-board.plan.{}.{}.{}", eva, date.format("%Y-%m-%d"), @@ -119,7 +114,7 @@ pub async fn iris_station_board( ); let cache = cache.clone(); tokio::spawn(async move { - cache_timetable.insert_to_cache(cache.as_ref(), None).await + cache_timetable.insert_to_cache(&cache, None).await }); Ok(timetable) } @@ -186,11 +181,11 @@ impl Iterator for DateRange { async fn get_realtime( iris_client: Arc, - cache: Arc, + cache: &cache::RedisCache, id: &str, ) -> Result { if let Some(cached) = &cache - .get_from_id::(&format!( + .get_from_id::(&format!( "iris.station-board.realtime.{}", id.to_owned() )) @@ -204,9 +199,10 @@ async fn get_realtime( Ok(realtime) => { let realtime = realtime; let cache_realtime = (realtime.clone(), id.to_owned()); + let cache = cache.clone(); tokio::spawn(async move { cache_realtime - .insert_to_cache(cache.clone().as_ref(), None) + .insert_to_cache(&cache, None) .await }); Ok(realtime) diff --git a/railboard-api/src/main.rs b/railboard-api/src/main.rs index 97003dd..540d242 100644 --- a/railboard-api/src/main.rs +++ b/railboard-api/src/main.rs @@ -2,110 +2,110 @@ use std::sync::Arc; use axum::{Router, Server}; use dotenvy::dotenv; +use reqwest::{Certificate, Client, Proxy}; +#[cfg(unix)] +use tokio::signal::unix::SignalKind; use tracing::metadata::LevelFilter; -use tracing_subscriber::{fmt, prelude::*, EnvFilter}; +use tracing_subscriber::{EnvFilter, fmt, prelude::*}; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; -#[cfg(unix)] -use tokio::signal::unix::SignalKind; + +use iris_client::IrisClient; +use ris_client::RisClient; +use vendo_client::VendoClient; + +use crate::cache::RedisCache; pub mod cache; pub mod error; -pub mod types; pub mod iris; -pub mod ris; +pub mod ris; pub mod vendo; -pub mod custom; - -mod helpers; - -pub use helpers::*; +pub mod custom; #[derive(OpenApi)] #[openapi( - paths( - vendo::station_board::station_board, - vendo::location_search::location_search, - vendo::journey_details::journey_details, - iris::station_board::station_board, - ris::journey_search::journey_search, - ris::journey_details::journey_details, - ris::station_board::station_board, - ris::station_information::station_information, - ris::station_search_by_name::station_search_by_name, - custom::station_board::station_board, - ), - components(schemas( - error::RailboardApiError, - error::ErrorDomain, - error::UnderlyingApiError, - // Vendo stuff - types::Time, - types::Notice, - types::HimNotice, - types::Attribute, - vendo_client::VendoError, - vendo::station_board::StationBoard, - vendo::station_board::StationBoardElement, - vendo::station_board::StationBoardArrival, - vendo::station_board::StationBoardDeparture, - vendo_client::location_search::LocationSearchResult, - vendo_client::location_search::LocationSearchCoordinates, - vendo::journey_details::JourneyDetails, - vendo::journey_details::TrainSchedule, - vendo::journey_details::Stop, - // Iris stuff - iris_client::station_board::IrisStationBoard, - iris_client::station_board::StationBoardStop, - iris_client::station_board::StationBoardStopArrival, - iris_client::station_board::StationBoardStopDeparture, - iris_client::station_board::RouteStop, - iris_client::station_board::ReplacedTrain, - iris_client::station_board::message::Message, - iris_client::station_board::message::MessageStatus, - iris_client::station_board::message::MessagePriority, - // Ris stuff - ris_client::RisError, - ris_client::RisUnauthorizedError, - ris_client::ZugportalError, - ris_client::journey_search::RisJourneySearchElement, - ris_client::journey_search::RisJourneySearchSchedule, - ris_client::journey_search::RisJourneySearchTransport, - ris::journey_details::RisJourneyDetails, - ris::journey_details::RisJourneyStop, - ris_client::journey_details::JourneyDetailsMessage, - ris_client::journey_details::Transport, - ris_client::journey_details::ReplacementTransport, - ris::journey_details::JourneyStopEvent, - ris::journey_details::JourneyStopAdministration, - ris::journey_details::JourneyStopDisruption, - ris::station_board::RisStationBoard, - ris::station_board::RisStationBoardItem, - ris::station_board::RisStationBoardItemAdministration, - ris::station_board::DepartureArrival, - ris::station_information::StationInformation, - ris::station_information::StationNameContent, - ris::station_information::Position, - ris::station_information::Position, - ris_client::station_search::RisStationSearchResponse, - ris_client::station_search::StationSearchResponse, - ris_client::station_information::Translatable, - ris_client::station_information::StationNameContent, - // Custom stuff - custom::station_board::StationBoard, - custom::station_board::StationBoardItem, - custom::station_board::StationBoardItemAdministration, - custom::station_board::DepartureArrival, - custom::station_board::IrisInformation, - - )), - tags( - (name = "Iris", description = "API using the Iris API as Backend"), - (name = "Ris", description = "API using the Ris API as Backend"), - (name = "Custom", description = "API not using a single API as Backend, but rather a combination of multiple sources"), - (name = "Vendo", description = "API using the Vendo API as Backend"), - ) +paths( +vendo::station_board::station_board, +vendo::location_search::location_search, +vendo::journey_details::journey_details, +iris::station_board::station_board, +ris::journey_search::journey_search, +ris::journey_details::journey_details, +ris::station_board::station_board, +ris::station_information::station_information, +ris::station_search_by_name::station_search_by_name, +custom::station_board::station_board, +), +components(schemas( +error::RailboardApiError, +error::ErrorDomain, +error::UnderlyingApiError, +// Vendo stuff +vendo_client::VendoError, +vendo_client::shared::Time, +vendo_client::shared::Notice, +vendo_client::shared::HimNotice, +vendo_client::shared::Attribute, +vendo_client::station_board::VendoStationBoard, +vendo_client::station_board::StationBoardElement, +vendo_client::station_board::StationBoardArrival, +vendo_client::station_board::StationBoardDeparture, +vendo_client::location_search::VendoLocationSearchResult, +vendo_client::location_search::VendoLocationSearchCoordinates, +vendo_client::journey_details::VendoJourneyDetails, +vendo_client::journey_details::VendoTrainSchedule, +vendo_client::journey_details::VendoStop, +// Iris stuff +iris_client::station_board::IrisStationBoard, +iris_client::station_board::StationBoardStop, +iris_client::station_board::StationBoardStopArrival, +iris_client::station_board::StationBoardStopDeparture, +iris_client::station_board::RouteStop, +iris_client::station_board::ReplacedTrain, +iris_client::station_board::message::Message, +iris_client::station_board::message::MessageStatus, +iris_client::station_board::message::MessagePriority, +// Ris stuff +ris_client::RisError, +ris_client::RisUnauthorizedError, +ris_client::ZugportalError, +ris_client::journey_search::RisJourneySearchElement, +ris_client::journey_search::RisJourneySearchSchedule, +ris_client::journey_search::RisJourneySearchTransport, +ris_client::journey_details::RisJourneyDetails, +ris_client::journey_details::RisJourneyStop, +ris_client::journey_details::RisJourneyStopEvent, +ris_client::journey_details::RisJourneyStopAdministration, +ris_client::journey_details::RisJourneyStopDisruption, +ris_client::journey_details::RisTransport, +ris_client::journey_details::RisReplacementTransport, +ris_client::journey_details::RisJourneyDetailsMessage, +ris_client::station_board::RisStationBoard, +ris_client::station_board::RisStationBoardItem, +ris_client::station_board::RisStationBoardItemAdministration, +ris_client::station_board::DepartureArrival, +ris_client::station_information::RisStationInformation, +ris_client::station_information::RisStationNameContent, +ris_client::station_information::RisPosition, +ris_client::station_search::RisStationSearchResponse, +ris_client::station_search::RisStationSearchElement, +ris_client::station_search::RisStationSearchTranslatable, +ris_client::station_search::RisStationSearchNameContent, +// Custom stuff +custom::station_board::StationBoard, +custom::station_board::StationBoardItem, +custom::station_board::StationBoardItemAdministration, +custom::station_board::DepartureArrival, +custom::station_board::IrisInformation, +)), +tags( +(name = "Iris", description = "API using the Iris API as Backend"), +(name = "Ris", description = "API using the Ris API as Backend"), +(name = "Custom", description = "API not using a single API as Backend, but rather a combination of multiple sources"), +(name = "Vendo", description = "API using the Vendo API as Backend"), +) )] struct ApiDoc; @@ -123,13 +123,10 @@ async fn main() { .init(); let redis_client = { - let redis_url = match std::env::var("REDIS_URL") { - Ok(url) => url, - Err(_) => { - tracing::warn!("caching is enabled and REDIS_URL env variable is not set. Using default \"redis://127.0.0.1/\""); - String::from("redis://127.0.0.1/") - } - }; + let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| { + tracing::warn!("REDIS_URL env variable is not set. Using default \"redis://127.0.0.1/\""); + String::from("redis://127.0.0.1/") + }); redis::Client::open(redis_url).expect("Failed create redis client, check redis url") }; @@ -138,14 +135,38 @@ async fn main() { let ris_api_key = std::env::var("RIS_API_KEY").expect("RIS_API_KEY env variable is not set"); let ris_client_id = std::env::var("RIS_CLIENT_ID").expect("RIS_CLIENT_ID env variable is not set"); + let http_client = + Client::builder() + // .add_root_certificate(Certificate::from_pem(include_bytes!("../../mitm.pem")).unwrap()) + // .proxy(Proxy::all("http://localhost:8080").unwrap()) + .build() + .unwrap(); + + let ris_client = Arc::new(RisClient::new( + Some(http_client.clone()), + None, + None, + &ris_client_id, + &ris_api_key, + )); + + let iris_client = Arc::new(IrisClient::new(Some(http_client.clone()), None, None)); + + let vendo_client = Arc::new(VendoClient::new(Some(http_client.clone()), None, None)); + let app = Router::new() .merge(SwaggerUi::new("/docs").url("/openapi.json", ApiDoc::openapi())) - .nest("/vendo/v1", vendo::router(redis_client.clone())) - .nest("/iris/v1", iris::router(redis_client.clone())) - .nest("/ris/v1", ris::router(redis_client.clone(), &ris_client_id, &ris_api_key)) - .nest("/v1", custom::router(redis_client.clone(), &ris_client_id, &ris_api_key)) + .nest("/vendo/v1", vendo::router()) + .nest("/iris/v1", iris::router()) + .nest("/ris/v1", ris::router()) + .nest("/v1", custom::router()).with_state(Arc::new(SharedState { + vendo_client, + ris_client, + iris_client, + cache: RedisCache::new(redis_client), + })) .fallback(|| async { "Nothing here :/" }); - + let bind_addr = std::env::var("API_URL").unwrap_or_else(|_| String::from("0.0.0.0:8080")); let server = Server::bind(&bind_addr.parse().unwrap()).serve(app.into_make_service()).with_graceful_shutdown(shutdown_hook()); @@ -153,6 +174,13 @@ async fn main() { server.await.unwrap(); } +pub struct SharedState { + vendo_client: Arc, + ris_client: Arc, + iris_client: Arc, + cache: RedisCache, +} + async fn shutdown_hook() { #[cfg(unix)] tokio::select! { diff --git a/railboard-api/src/ris.rs b/railboard-api/src/ris.rs index d9873b5..bbb0132 100644 --- a/railboard-api/src/ris.rs +++ b/railboard-api/src/ris.rs @@ -1,12 +1,8 @@ use std::sync::Arc; -use axum::{routing::get, Router}; -use reqwest::Client; -// use reqwest::Certificate; -// use reqwest::Proxy; -use ris_client::RisClient; +use axum::{Router, routing::get}; -use crate::cache::RedisCache; +use crate::SharedState; pub mod journey_details; pub mod journey_search; @@ -14,31 +10,7 @@ pub mod station_board; pub mod station_information; pub mod station_search_by_name; -pub struct RisState { - ris_client: Arc, - cache: Arc, -} - -pub fn router(redis: Arc, db_client_id: &str, db_api_key: &str) -> Router { - let client = Client::builder() - // .add_root_certificate(Certificate::from_pem(include_bytes!("../../mitm.pem")).unwrap()) - // .proxy(Proxy::all("http://localhost:8080").unwrap()) - .build() - .unwrap(); - - let ris_client = Arc::new(RisClient::new( - Some(client), - None, - None, - db_client_id, - db_api_key, - )); - - let shared_state = Arc::new(RisState { - ris_client, - cache: Arc::new(RedisCache::new(redis)), - }); - +pub fn router() -> Router> { Router::new() .route( "/journey_search/:category/:number", @@ -57,5 +29,4 @@ pub fn router(redis: Arc, db_client_id: &str, db_api_key: &str) - "/station_search/:query", get(station_search_by_name::station_search_by_name), ) - .with_state(shared_state) } diff --git a/railboard-api/src/ris/journey_details.rs b/railboard-api/src/ris/journey_details.rs index 4e5d598..4009599 100644 --- a/railboard-api/src/ris/journey_details.rs +++ b/railboard-api/src/ris/journey_details.rs @@ -1,37 +1,30 @@ -use std::{collections::HashSet, sync::Arc}; +use std::sync::Arc; use axum::{ extract::{Path, State}, Json, }; -use chrono::{DateTime, FixedOffset}; -use ris_client::journey_details::{JourneyDetailsEvent, JourneyDetailsMessage, Transport}; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -use crate::{ - cache::{CachableObject, Cache}, - error::RailboardResult, -}; +use ris_client::journey_details::RisJourneyDetails; -use super::RisState; +use crate::{cache::{CachableObject, Cache}, error::RailboardResult, SharedState}; #[utoipa::path( - get, - path = "/ris/v1/journey_details/{id}", - params( - ("id" = String, Path, description = "The id of this journey (can be optained e.G. through the journey search endpoint)") - ), - tag = "Ris", - responses( - (status = 200, description = "The requested Journey Details", body = RisJourneyDetails), - (status = 400, description = "The Error returned by Ris, will be the Ris Domain with UnderlyingApiError Variant 3 or 4", body = RailboardApiError), - (status = 500, description = "The Error returned if the request or deserialization fails, will be domain Request", body = RailboardApiError) - ) +get, +path = "/ris/v1/journey_details/{id}", +params( +("id" = String, Path, description = "The id of this journey (can be optained e.G. through the journey search endpoint)") +), +tag = "Ris", +responses( +(status = 200, description = "The requested Journey Details", body = RisJourneyDetails), +(status = 400, description = "The Error returned by Ris, will be the Ris Domain with UnderlyingApiError Variant 3 or 4", body = RailboardApiError), +(status = 500, description = "The Error returned if the request or deserialization fails, will be domain Request", body = RailboardApiError) +) )] pub async fn journey_details( Path(id): Path, - state: State>, + state: State>, ) -> RailboardResult> { if let Some(cached) = state .cache @@ -43,205 +36,11 @@ pub async fn journey_details( let response = state.ris_client.journey_details(&id).await?; - let mut stops: Vec<(Option, Option)> = Vec::new(); - - 'outer: for event in response.events { - match event.r#type { - ris_client::journey_details::EventType::Arrival => { - for stop in stops.iter_mut() { - if stop - .1 - .as_ref() - .map(|departure| { - stop.0.is_none() - && departure.station.eva_number == event.station.eva_number - && departure.time_schedule >= event.time_schedule - }) - .unwrap_or(false) - { - stop.0 = Some(event); - continue 'outer; - } - } - stops.push((Some(event), None)) - } - ris_client::journey_details::EventType::Departure => { - for stop in stops.iter_mut() { - if stop - .0 - .as_ref() - .map(|arrival| { - stop.1.is_none() - && arrival.station.eva_number == event.station.eva_number - && arrival.time_schedule <= event.time_schedule - }) - .unwrap_or(false) - { - stop.1 = Some(event); - continue 'outer; - } - } - stops.push((None, Some(event))) - } - } - } - - let stops = stops - .into_iter() - .map(|stop| { - let arrival = stop.0; - let departure = stop.1; - - let departure_arrival = departure - .clone() - .unwrap_or_else(|| arrival.clone().unwrap()); - - let mut messages: HashSet = HashSet::new(); - - if let Some(arrival) = arrival.clone() { - for message in arrival.messages { - messages.insert(message); - } - } - - if let Some(departure) = departure.clone() { - for message in departure.messages { - messages.insert(message); - } - } - - let custom_operator_name = - match departure_arrival.administration.administration_id.as_str() { - "80" => "DB Fernverkehr AG", - "82" => "CFL", - "87" => "SNCF", - "88" => "SNCB", - _ => &departure_arrival.administration.operator_name, - }; - - RisJourneyStop { - stop_id: departure_arrival.station.eva_number, - stop_name: departure_arrival.station.name, - arrival: arrival.map(|arrival| JourneyStopEvent { - cancelled: arrival.canceled, - additional: arrival.additional, - on_demand: arrival.on_demand, - scheduled: arrival.time_schedule, - realtime: arrival.time, - time_type: arrival.time_type, - }), - departure: departure.map(|departure| JourneyStopEvent { - cancelled: departure.canceled, - additional: departure.additional, - on_demand: departure.on_demand, - scheduled: departure.time_schedule, - realtime: departure.time, - time_type: departure.time_type, - }), - transport: departure_arrival.transport, - messages: messages.into_iter().collect(), - disruptions: departure_arrival - .disruptions - .into_iter() - .map(|disruption| JourneyStopDisruption { - id: disruption.disruption_id, - communication_id: disruption.disruption_communication_id, - text: disruption.descriptions.de.text, - text_short: disruption.descriptions.de.text_short, - priority: disruption.display_priority, - }) - .collect(), - scheduled_platform: departure_arrival.platform_schedule, - real_platform: departure_arrival.platform, - administration: JourneyStopAdministration { - id: departure_arrival.administration.administration_id, - name: custom_operator_name.to_string(), - operator_code: departure_arrival.administration.operator_code, - ris_name: departure_arrival.administration.operator_name, - }, - } - }) - .collect(); - - let response = RisJourneyDetails { - id: response.journey_id, - destination_id: response.destination_schedule.eva_number, - destination_name: response.destination_schedule.name, - origin_id: response.origin_schedule.eva_number, - origin_name: response.origin_schedule.name, - journey_type: response.r#type, - cancelled: response.journey_canceled, - stops, - }; - { let response = response.clone(); - tokio::spawn(async move { response.insert_to_cache(state.cache.as_ref(), None).await }); + tokio::spawn(async move { response.insert_to_cache(&state.cache, None).await }); } Ok(Json(response)) } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct RisJourneyDetails { - pub id: String, - pub journey_type: String, - pub origin_name: String, - pub origin_id: String, - pub destination_name: String, - pub destination_id: String, - pub cancelled: bool, - pub stops: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct RisJourneyStop { - pub stop_id: String, - pub stop_name: String, - #[schema(nullable)] - pub arrival: Option, - #[schema(nullable)] - pub departure: Option, - pub messages: Vec, - pub disruptions: Vec, - pub transport: Transport, - #[schema(nullable)] - pub scheduled_platform: Option, - #[schema(nullable)] - pub real_platform: Option, - pub administration: JourneyStopAdministration, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct JourneyStopEvent { - pub cancelled: bool, - pub additional: bool, - pub on_demand: bool, - pub scheduled: DateTime, - #[schema(nullable)] - pub realtime: Option>, - pub time_type: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct JourneyStopAdministration { - pub id: String, - pub name: String, - pub operator_code: String, - pub ris_name: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct JourneyStopDisruption { - pub id: String, - pub communication_id: Option, - pub priority: i32, - pub text: String, - #[schema(nullable)] - pub text_short: Option, -} diff --git a/railboard-api/src/ris/journey_search.rs b/railboard-api/src/ris/journey_search.rs index 1964276..2b8ef95 100644 --- a/railboard-api/src/ris/journey_search.rs +++ b/railboard-api/src/ris/journey_search.rs @@ -6,15 +6,11 @@ use axum::{ }; use chrono::{NaiveDate, TimeZone}; use chrono_tz::Europe::Berlin; -use ris_client::journey_search::RisJourneySearchElement; use serde::Deserialize; -use crate::{ - cache::{CachableObject, Cache}, - error::RailboardResult, -}; +use ris_client::journey_search::RisJourneySearchElement; -use super::RisState; +use crate::{cache::{CachableObject, Cache}, error::RailboardResult, SharedState}; #[derive(Deserialize)] pub struct JounreySearchPath { @@ -45,7 +41,7 @@ pub struct JounreySearchQuery { pub async fn journey_search( Path(path): Path, Query(query): Query, - State(state): State>, + State(state): State>, ) -> RailboardResult>> { let category = path.category; let number = path.number; @@ -81,7 +77,7 @@ pub async fn journey_search( let category = category.clone(); let number = number.clone(); (category, number, response) - .insert_to_cache(cache.clone().as_ref(), None) + .insert_to_cache(&cache, None) .await }); } diff --git a/railboard-api/src/ris/station_board.rs b/railboard-api/src/ris/station_board.rs index cf8e044..284cb23 100644 --- a/railboard-api/src/ris/station_board.rs +++ b/railboard-api/src/ris/station_board.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, sync::Arc}; +use std::sync::Arc; use axum::{ extract::{Path, Query, State}, @@ -6,17 +6,11 @@ use axum::{ }; use chrono::{DateTime, FixedOffset, TimeZone}; use chrono_tz::Europe::Berlin; -use ris_client::{station_board::StationBoardItem, RisClient}; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; +use serde::Deserialize; -use crate::{ - cache::{self, CachableObject, Cache}, - error::RailboardResult, - name_from_administation_code, -}; +use ris_client::station_board::RisStationBoard; -use super::RisState; +use crate::{cache::{CachableObject, Cache}, error::RailboardResult, SharedState}; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -26,24 +20,24 @@ pub struct StationBoardQuery { } #[utoipa::path( - get, - path = "/ris/v1/station_board/{eva}", - params( - ("eva" = String, Path, description = "The Eva Number of the Station you are requesting"), - ("timeStart" = Option, Query, description = "The Start Time of the Time Range you are requesting"), - ("timeEnd" = Option, Query, description = "The End Time of the Time Range you are requesting") - ), - tag = "Ris", - responses( - (status = 200, description = "The requested Station Board", body = RisStationBoard), - (status = 400, description = "The Error returned by the Zugportal API (Ris), will be the Ris Domain with UnderlyingApiError Variant 5", body = RailboardApiError), - (status = 500, description = "The Error returned if the request or deserialization fails, will be domain Request", body = RailboardApiError) - ) +get, +path = "/ris/v1/station_board/{eva}", +params( +("eva" = String, Path, description = "The Eva Number of the Station you are requesting"), +("timeStart" = Option < String >, Query, description = "The Start Time of the Time Range you are requesting"), +("timeEnd" = Option < String >, Query, description = "The End Time of the Time Range you are requesting") +), +tag = "Ris", +responses( +(status = 200, description = "The requested Station Board", body = RisStationBoard), +(status = 400, description = "The Error returned by the Zugportal API (Ris), will be the Ris Domain with UnderlyingApiError Variant 5", body = RailboardApiError), +(status = 500, description = "The Error returned if the request or deserialization fails, will be domain Request", body = RailboardApiError) +) )] pub async fn station_board( Path(eva): Path, Query(query): Query, - State(state): State>, + State(state): State>, ) -> RailboardResult> { let time_start = query .time_start @@ -53,27 +47,8 @@ pub async fn station_board( .time_end .map(|time_end| Berlin.from_utc_datetime(&time_end.naive_utc())); - let result = ris_station_board( - &eva, - time_start, - time_end, - state.ris_client.clone(), - state.cache.clone(), - ) - .await?; - - Ok(Json(result)) -} - -pub async fn ris_station_board( - eva: &str, - time_start: Option>, - time_end: Option>, - ris_client: Arc, - cache: Arc, -) -> RailboardResult { if let (Some(time_start), Some(time_end)) = (time_start, time_end) { - if let Some(cached) = cache + if let Some(cached) = state.cache .get_from_id(&format!( "ris.station-board.{}.{}.{}", eva, @@ -82,185 +57,22 @@ pub async fn ris_station_board( )) .await { - return Ok(cached); + return Ok(Json(cached)); } } - let (arrivals, departures) = tokio::join!( - ris_client.station_board_arrivals(eva, time_start, time_end), - ris_client.station_board_departures(eva, time_start, time_end) - ); - - let arrivals = arrivals?; - let departures = departures?; - - let mut trains: BTreeMap, Option)> = - BTreeMap::new(); - - for train in arrivals.items { - let id = train.train.journey_id.to_owned(); - trains.entry(id).or_insert_with(|| (None, None)).0 = Some(train); - } - - for train in departures.items { - let id = train.train.journey_id.to_owned(); - trains.entry(id).or_insert_with(|| (None, None)).1 = Some(train); - } - - let station_board = RisStationBoard { - eva: departures.eva_no, - name: departures.station_name, - time_start: departures.time_start, - time_end: departures.time_end, - items: trains - .into_iter() - .map(|(id, (arrival, departure))| { - let departure_arrival = departure - .clone() - .unwrap_or_else(|| arrival.clone().unwrap()); - - let scheduled_platform = if departure_arrival.platform.is_empty() { - None - } else { - Some(departure_arrival.platform) - }; - let realtime_platform = if departure_arrival.platform_predicted.is_empty() { - None - } else { - Some(departure_arrival.platform_predicted) - }; - - RisStationBoardItem { - journey_id: id, - station_eva: departure_arrival.station.eva_no.clone(), - station_name: departure_arrival.station.name.clone(), - cancelled: departure_arrival.canceled || departure_arrival.station.canceled, - category: departure_arrival.train.category, - train_type: departure_arrival.train.r#type, - train_number: departure_arrival.train.no, - line_indicator: departure_arrival.train.line_name, - departure: departure.as_ref().map(|departure| DepartureArrival { - delay: departure.diff, - time_realtime: departure.time_predicted, - time_scheduled: departure.time, - time_type: departure.time_type.clone(), - }), - arrival: arrival.as_ref().map(|arrival| DepartureArrival { - delay: arrival.diff, - time_realtime: arrival.time_predicted, - time_scheduled: arrival.time, - time_type: arrival.time_type.clone(), - }), - destination_eva: departure - .as_ref() - .and_then(|departure| { - departure - .destination - .as_ref() - .map(|destination| destination.eva_no.clone()) - }) - .unwrap_or(departure_arrival.station.eva_no.clone()), - destination_name: departure - .and_then(|departure| { - departure.destination.map(|destination| destination.name) - }) - .unwrap_or(departure_arrival.station.name.clone()), - origin_eva: arrival - .as_ref() - .and_then(|arrival| { - arrival.origin.as_ref().map(|origin| origin.eva_no.clone()) - }) - .unwrap_or(departure_arrival.station.eva_no), - origin_name: arrival - .as_ref() - .and_then(|arrival| { - arrival.origin.as_ref().map(|origin| origin.name.clone()) - }) - .unwrap_or(departure_arrival.station.name), - platform_scheduled: scheduled_platform, - platform_realtime: realtime_platform, - administation: RisStationBoardItemAdministration { - id: departure_arrival.administration.id, - operator_code: departure_arrival.administration.operator_code, - operator_name: String::from( - name_from_administation_code( - &departure_arrival.administration.operator_name, - ) - .unwrap_or(&departure_arrival.administration.operator_name), - ), - ris_operator_name: departure_arrival.administration.operator_name, - }, - } - }) - .collect(), - }; + let station_board = state.ris_client + .station_board(&eva, time_start, time_end) + .await?; { let station_board = station_board.clone(); tokio::spawn(async move { - let _ = station_board.insert_to_cache(cache.as_ref(), None).await; + let _ = station_board.insert_to_cache(&state.cache, None).await; }); } - Ok(station_board) -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, ToSchema, Clone)] -#[serde(rename_all = "camelCase")] -pub struct RisStationBoard { - pub eva: String, - pub name: String, - pub time_start: DateTime, - pub time_end: DateTime, - pub items: Vec, + Ok(Json(station_board)) } -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, ToSchema, Clone)] -#[serde(rename_all = "camelCase")] -pub struct RisStationBoardItem { - pub journey_id: String, - - pub station_eva: String, - pub station_name: String, - - pub category: String, - pub train_type: String, - pub train_number: u32, - pub line_indicator: String, - - pub cancelled: bool, - - pub arrival: Option, - pub departure: Option, - - #[schema(nullable)] - pub platform_scheduled: Option, - #[schema(nullable)] - pub platform_realtime: Option, - - pub origin_eva: String, - pub origin_name: String, - pub destination_eva: String, - pub destination_name: String, - pub administation: RisStationBoardItemAdministration, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, ToSchema, Clone)] -#[serde(rename_all = "camelCase")] -pub struct RisStationBoardItemAdministration { - pub id: String, - pub operator_code: String, - pub operator_name: String, - pub ris_operator_name: String, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, ToSchema, Clone)] -#[serde(rename_all = "camelCase")] -pub struct DepartureArrival { - /// Since ris returns dates with seconds it also rounds up this number if the seconds are 50 for example - pub delay: i32, - pub time_scheduled: DateTime, - pub time_realtime: DateTime, - pub time_type: String, -} diff --git a/railboard-api/src/ris/station_information.rs b/railboard-api/src/ris/station_information.rs index 21da751..cf23f0d 100644 --- a/railboard-api/src/ris/station_information.rs +++ b/railboard-api/src/ris/station_information.rs @@ -5,33 +5,27 @@ use axum::{ Json, }; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; +use ris_client::station_information::RisStationInformation; -use crate::{ - cache::{CachableObject, Cache}, - error::{RailboardApiError, RailboardResult}, -}; - -use super::RisState; +use crate::{cache::{CachableObject, Cache}, error::{RailboardApiError, RailboardResult}, SharedState}; #[utoipa::path( - get, - path = "/ris/v1/station/{eva}", - params( - ("eva" = String, Path, description = "The Eva Number of the Station you are requesting"), - ), - tag = "Ris", - responses( - (status = 200, description = "The requested Station Information", body = StationInformation), - (status = 400, description = "The Error returned by the Ris, will be the Ris Domain with UnderlyingApiError Variant 3, 4 or none if there was no Station found", body = RailboardApiError), - (status = 500, description = "The Error returned if the request or deserialization fails, will be domain Request", body = RailboardApiError) - ) +get, +path = "/ris/v1/station/{eva}", +params( +("eva" = String, Path, description = "The Eva Number of the Station you are requesting"), +), +tag = "Ris", +responses( +(status = 200, description = "The requested Station Information", body = RisStationInformation), +(status = 400, description = "The Error returned by the Ris, will be the Ris Domain with UnderlyingApiError Variant 3, 4 or none if there was no Station found", body = RailboardApiError), +(status = 500, description = "The Error returned if the request or deserialization fails, will be domain Request", body = RailboardApiError) +) )] pub async fn station_information( Path(eva): Path, - State(state): State>, -) -> RailboardResult> { + State(state): State>, +) -> RailboardResult> { if let Some(cached) = state .cache .get_from_id(&format!("ris.station-information.{}", &eva)) @@ -42,9 +36,7 @@ pub async fn station_information( let response = state.ris_client.station_information(&eva).await?; - let first = response.stations.into_iter().next(); - - if first.is_none() { + if response.is_none() { return Err(RailboardApiError { domain: crate::error::ErrorDomain::Ris, message: "No Station found".to_string(), @@ -52,75 +44,15 @@ pub async fn station_information( }); } - let response = first.unwrap(); - - let response: StationInformation = response.into(); + let response = response.unwrap(); { let response = response.clone(); tokio::spawn(async move { - let _ = response.insert_to_cache(state.cache.clone().as_ref(), None).await; + let _ = response.insert_to_cache(&state.cache, None).await; }); } Ok(Json(response)) } -impl From for StationInformation { - fn from(value: ris_client::station_information::StationInformation) -> Self { - Self { - eva: value.eva_number, - names: StationNameContent { - name_long: value.names.de.name_long, - speech_long: value.names.de.speech_long, - speech_short: value.names.de.speech_short, - }, - station_id: value.station_id, - available_transports: value.available_transports, - transport_associations: value.transport_associations, - country_code: value.country_code, - state: value.state, - municipality_key: value.municipality_key, - time_zone: value.time_zone, - metropolis: value.metropolis.map(|m| m.de), - position: Position { - longitude: value.position.longitude, - latitude: value.position.latitude, - }, - } - } -} - -#[derive(Serialize, Deserialize, Debug, ToSchema, Clone)] -#[serde(rename_all = "camelCase")] -pub struct StationInformation { - pub eva: String, - #[schema(nullable)] - pub station_id: Option, - pub names: StationNameContent, - pub metropolis: Option, - pub available_transports: Vec, - pub transport_associations: Vec, - pub country_code: String, - pub state: String, - pub municipality_key: String, - pub time_zone: String, - pub position: Position, -} - -#[derive(Serialize, Deserialize, Debug, ToSchema, Clone)] -#[serde(rename_all = "camelCase")] -pub struct StationNameContent { - pub name_long: String, - #[schema(nullable)] - pub speech_long: Option, - #[schema(nullable)] - pub speech_short: Option, -} - -#[derive(Serialize, Deserialize, Debug, ToSchema, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Position { - pub longitude: f64, - pub latitude: f64, -} diff --git a/railboard-api/src/ris/station_search_by_name.rs b/railboard-api/src/ris/station_search_by_name.rs index bd8120a..e80a20b 100644 --- a/railboard-api/src/ris/station_search_by_name.rs +++ b/railboard-api/src/ris/station_search_by_name.rs @@ -4,15 +4,12 @@ use axum::{ extract::{Path, Query, State}, Json, }; -use ris_client::station_search::RisStationSearchResponse; use serde::Deserialize; -use crate::{ - cache::{CachableObject, Cache}, - error::RailboardResult, -}; +use ris_client::station_search::RisStationSearchElement; + +use crate::{cache::{CachableObject, Cache}, error::RailboardResult, SharedState}; -use super::RisState; #[derive(Deserialize)] pub struct RisStationSearchQuery { @@ -28,7 +25,7 @@ pub struct RisStationSearchQuery { ), tag = "Ris", responses( - (status = 200, description = "The requested Station Search Information", body = RisStationSearchResponse), + (status = 200, description = "The requested Station Search Information", body = [RisStationSearchElement]), (status = 400, description = "The Error returned by Ris, will be the Ris Domain with UnderlyingApiError Variant 3 or 4", body = RailboardApiError), (status = 500, description = "The Error returned if the request or deserialization fails, will be domain Request", body = RailboardApiError) ) @@ -36,8 +33,8 @@ pub struct RisStationSearchQuery { pub async fn station_search_by_name( Path(query): Path, Query(query_params): Query, - state: State>, -) -> RailboardResult> { + state: State>, +) -> RailboardResult>> { if let Some(cached) = state .cache .get_from_id(&format!("ris.journey-details.{}", &query)) @@ -61,8 +58,8 @@ pub async fn station_search_by_name( tokio::spawn(async move { response .insert_to_cache( - state.cache.as_ref(), - Some(&format!("{}.{}", query, limit.to_string())), + &state.cache, + Some(&format!("{}.{}", query, limit)), ) .await }); diff --git a/railboard-api/src/vendo.rs b/railboard-api/src/vendo.rs index 602d590..a6acb52 100644 --- a/railboard-api/src/vendo.rs +++ b/railboard-api/src/vendo.rs @@ -1,27 +1,14 @@ use std::sync::Arc; -use axum::{routing::get, Router}; -use vendo_client::VendoClient; +use axum::{Router, routing::get}; -use crate::cache::{self, RedisCache}; +use crate::SharedState; pub mod journey_details; pub mod location_search; pub mod station_board; -pub struct VendoState { - vendo_client: VendoClient, - cache: RedisCache, -} - -pub fn router(redis: Arc) -> Router { - let vendo_client = VendoClient::default(); - - let shared_state = Arc::new(VendoState { - vendo_client, - cache: cache::RedisCache::new(redis), - }); - +pub fn router() -> Router> { Router::new() .route("/station_board/:id", get(station_board::station_board)) .route( @@ -32,5 +19,4 @@ pub fn router(redis: Arc) -> Router { "/location_search/:query", get(location_search::location_search), ) - .with_state(shared_state) } diff --git a/railboard-api/src/vendo/journey_details.rs b/railboard-api/src/vendo/journey_details.rs index fe70457..64433b3 100644 --- a/railboard-api/src/vendo/journey_details.rs +++ b/railboard-api/src/vendo/journey_details.rs @@ -4,33 +4,26 @@ use axum::{ extract::{Path, State}, Json, }; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; -use vendo_client::journey_details::{JourneyDetailsAttribute, JourneyDetailsHimNotice}; -use crate::{ - cache::{CachableObject, Cache}, - error::RailboardResult, - types::{Attribute, HimNotice, Time}, -}; +use vendo_client::journey_details::VendoJourneyDetails; -use super::VendoState; +use crate::{cache::{CachableObject, Cache}, error::RailboardResult, SharedState}; #[utoipa::path( - get, - path = "/vendo/v1/journey_details/{id}", - params(("id" = String, Path, description = "The Vendo-ID of the Journey you want to get details for")), - tag = "Vendo", - responses( - (status = 200, description = "The requested Journey Details", body = JourneyDetails), - (status = 400, description = "The Error returned by Vendo", body = RailboardApiError), - (status = 500, description = "The Error returned if the request or deserialization fails", body = RailboardApiError) - ) +get, +path = "/vendo/v1/journey_details/{id}", +params(("id" = String, Path, description = "The Vendo-ID of the Journey you want to get details for")), +tag = "Vendo", +responses( +(status = 200, description = "The requested Journey Details", body = VendoJourneyDetails), +(status = 400, description = "The Error returned by Vendo", body = RailboardApiError), +(status = 500, description = "The Error returned if the request or deserialization fails", body = RailboardApiError) +) )] pub async fn journey_details( Path(id): Path, - State(state): State>, -) -> RailboardResult> { + State(state): State>, +) -> RailboardResult> { if let Some(cached) = state .cache .get_from_id(&format!("vendo.journey-details.{}", &id)) @@ -39,140 +32,13 @@ pub async fn journey_details( return Ok(Json(cached)); } - let response = state.vendo_client.journey_details(&id).await?; - - let mapped = JourneyDetails { - short_name: response.short_name, - name: response.name, - long_name: response.long_name, - destination: response.destination, - - journey_id: id, - - stops: response - .stops - .into_iter() - .map(|stop| Stop { - name: stop.name, - arrival: stop.arrival.map(|arrival| Time { - scheduled: arrival, - realtime: stop.realtime_arrival, - }), - departure: stop.departure.map(|departure| Time { - scheduled: departure, - realtime: stop.realtime_departure, - }), - platform: stop.platform, - realtime_platform: stop.realtime_platform, - notes: stop.notes.into_iter().map(|note| note.text).collect(), - him_notices: stop - .him_notices - .into_iter() - .map(|from| from.into()) - .collect(), - attributes: stop - .attributes - .into_iter() - .map(|from| from.into()) - .collect(), - service_note: stop.service_note.map(|service| service.into()), - }) - .collect(), - - transport_number: response.transport_number, - product_type: response.product_type, - notes: response.notes.into_iter().map(|note| note.text).collect(), - him_notices: response - .him_notices - .into_iter() - .map(|from| from.into()) - .collect(), - attributes: response - .attributes - .into_iter() - .map(|from| from.into()) - .collect(), - schedule: TrainSchedule { - regular_schedule: response.schedule.regular_schedule, - days_of_operation: response.schedule.days_of_operation, - }, - journey_day: response.journey_day, - }; + let journey_details = state.vendo_client.journey_details(&id).await?; { - let cached = mapped.clone(); + let cached = journey_details.clone(); tokio::spawn(async move { cached.insert_to_cache(&state.cache, None).await }); } - Ok(Json(mapped)) + Ok(Json(journey_details)) } -#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct JourneyDetails { - pub short_name: String, - pub name: String, - pub long_name: String, - pub destination: String, - - pub journey_id: String, - - pub stops: Vec, - - #[schema(nullable)] - pub transport_number: Option, - pub product_type: String, - - pub notes: Vec, - pub him_notices: Vec, - pub attributes: Vec, - - pub schedule: TrainSchedule, - pub journey_day: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct TrainSchedule { - pub regular_schedule: String, - #[schema(nullable)] - pub days_of_operation: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct Stop { - pub name: String, - #[schema(nullable)] - pub arrival: Option