From cd13c85c69cf761b2da84ad91af64d23a3568aa5 Mon Sep 17 00:00:00 2001 From: stuebinm Date: Wed, 9 Feb 2022 02:51:59 +0100 Subject: existential types in rust are weird … lots and lots of traits … --- src/iceportal.rs | 99 ------------------------------------- src/lib.rs | 4 +- src/main.rs | 89 ++++++++++++++------------------- src/onboard/iceportal.rs | 126 +++++++++++++++++++++++++++++++++++++++++++++++ src/onboard/mod.rs | 76 ++++++++++++++++++++++++++++ src/onboard/zugportal.rs | 109 ++++++++++++++++++++++++++++++++++++++++ src/traits.rs | 34 +++++++++++++ src/types.rs | 64 +++++++----------------- src/zugportal.rs | 97 ------------------------------------ 9 files changed, 404 insertions(+), 294 deletions(-) delete mode 100644 src/iceportal.rs create mode 100644 src/onboard/iceportal.rs create mode 100644 src/onboard/mod.rs create mode 100644 src/onboard/zugportal.rs create mode 100644 src/traits.rs delete mode 100644 src/zugportal.rs diff --git a/src/iceportal.rs b/src/iceportal.rs deleted file mode 100644 index 3cb5e6f..0000000 --- a/src/iceportal.rs +++ /dev/null @@ -1,99 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::Deserialize; -use serde_json::Value; - -use crate::{serde::*, travelynx::TrainRef, types::IsStation}; - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct TripInfo { - trip: Trip, - connection: Option, - selected_route: Option, - active: Option -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Trip { - train_type: String, - vzn: String, // train number - // some position info here - actual_position: u64, // distance along track, presumably - stops: Vec -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Stop { - info: StopInfo, - station: Station, - timetable: Timetable -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct StopInfo { - distance_from_start: u64, - position_status: String // one of "departed", "future", ... ? -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Station { - eva_nr: String, - name: String -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Timetable { - #[serde(deserialize_with = "option_naive_read_unixtime_db")] - scheduled_arrival_time: Option>, - #[serde(deserialize_with = "option_naive_read_unixtime_db")] - actual_arrival_time: Option> -} - -impl IsStation for Stop { - fn name(&self) -> &str { - &self.station.name - } - - fn scheduled_arrival(&self) -> Option<&chrono::DateTime> { - self.timetable.scheduled_arrival_time.as_ref() - } - - fn real_arrival(&self) -> Option<&chrono::DateTime> { - self.timetable.scheduled_arrival_time.as_ref() - } - - fn ds100(&self) -> &str { - "??" - } -} - -impl TripInfo { - pub fn guess_last_station(&self) -> Option { - let current_pos = self.trip.actual_position; - self - .trip - .stops - .iter() - .rev() - .map(|stop| (stop.info.distance_from_start, stop)) - .filter(|(dist, _)| dist <= ¤t_pos) - .next() - .map(|(_, stop)| stop.station.name.clone()) - } - - pub fn get_train_ref(&self) -> TrainRef { - TrainRef { - _type: self.trip.train_type.clone(), - no: self.trip.vzn.clone() - } - } - - pub fn trip(&self) -> crate::types::Trip<'_, Stop> { - crate::types::Trip(&self.trip.stops) - } -} diff --git a/src/lib.rs b/src/lib.rs index 4107efe..6bb173d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ -pub mod iceportal; +pub mod onboard; pub(crate) mod serde; +pub mod traits; pub mod travelynx; pub mod types; -pub mod zugportal; diff --git a/src/main.rs b/src/main.rs index 7e1c85a..9bb2f9e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,9 @@ use clap::{Parser, Subcommand}; use colored::*; use serde::Deserialize; -use traveltext::types::*; -use traveltext::{iceportal::*, travelynx::*}; +use traveltext::onboard::{choose_api, OnBoardAPI}; +use traveltext::{traits::*, travelynx::*, types::*}; + #[derive(Parser)] struct Cli { @@ -34,15 +35,21 @@ enum Command { Destination { to: String }, + Station { + station: String + }, Arewethereyet, /// If iceportal.de is available, ask it which train we're in and /// check in - Autocheckin, + Autocheckin { + train: String + }, /// Undo the last checkin (if any). Undo, - /// Query iceportal.de (for testing) - ICEPortal, - Zugportal + /// Query a train's on-board API + Query { + train: String + } } #[derive(Deserialize)] @@ -119,11 +126,9 @@ fn main() -> Result<(), ureq::Error> { None => println!("{}: I have no idea", traveltext) } } - Some(_to) => println!( - "{}: {}", - traveltext, - "you're not checked in".red() - ) + Some(_to) => { + println!("{}: {}", traveltext, "you're not checked in".red()) + } } } Command::Checkin { from, to, train } => { @@ -165,23 +170,25 @@ fn main() -> Result<(), ureq::Error> { println!("{}: {}", traveltext, resp); } - Command::Autocheckin => { - let iceportal: TripInfo = exiting_get_request( - "https://iceportal.de/api1/rs/tripInfo/trip", - cli.debug - ); - - let last_stop = iceportal.guess_last_station().unwrap(); - let train = iceportal.get_train_ref(); + Command::Autocheckin { train } => { + let onboard = match choose_api(&train).request(cli.debug) { + Ok(resp) => resp, + Err(e) => exit_err(&e.to_string(), "failed to parse api response") + }; + let last_stop = onboard.guess_last_station().unwrap(); + let train = onboard.get_train_ref(); println!( "{}: guessing you got onto {} {} in {}, checking in …", - traveltext, train._type, train.no, last_stop + traveltext, + train._type, + train.no, + last_stop.to_fancy_string() ); let resp: Response = exiting_post_request( &format!("{}/api/v1/travel", cli.baseurl), Action::CheckIn { train, - from_station: last_stop, + from_station: last_stop.name().to_owned(), to_station: None, comment: None, token: format!("{}", config.token_travel) @@ -192,51 +199,31 @@ fn main() -> Result<(), ureq::Error> { // eprintln!("{:?}", resp); println!("{}: {}", traveltext, resp); } - Command::ICEPortal => { - match get_request::( - "https://iceportal.de/api1/rs/tripInfo/trip" - ) { + Command::Query { train } => { + let api: &dyn OnBoardAPI = choose_api(&train); + match api.request(cli.debug) { Ok(resp) => { println!( "{}: Currently in {}\n", traveltext, resp.get_train_ref().to_string().green() ); - println!("guessing last stop was: {:?}\n", resp.guess_last_station()); - println!("Stops:\n{}", resp.trip()) - } - Err(err) => { - if cli.debug { - eprintln!("{:?}", err); - } - println!("either this tool or the iceportal broke or you're not actually on an ICE\n\ - (get a response but couldn't parse it)"); - } - } - } - Command::Zugportal => { - match get_request::( - // "https://iceportal.de/api1/rs/tripInfo/trip" - "https://zugportal.de/prd/zupo-travel-information/api/public/ri/journey" - ) { - Ok(resp) => { println!( - "{}: Currently in {}\n", - traveltext, - resp.get_train_ref().to_string().green() + "guessing last stop was: {:?}\n", + resp.guess_last_station().unwrap().name() ); - // println!("guessing last stop was: {:?}\n", resp.guess_last_station()); - println!("Stops:\n{}", resp.trip()) + // println!("Stops:\n{}", resp.stops()) } Err(err) => { - if cli.debug { - eprintln!("{:?}", err); - } println!("either this tool or the zugportal broke or you're not actually on an ICE\n\ (get a response but couldn't parse it)"); } } } + Command::Station { station } => { + // let c = HafasClient::new(DbProfile, HyperRustlsRequester::new()); + // println!("{:#?}", c.suggestions("München Hbf", None).await.unwrap()); + } } Ok(()) } diff --git a/src/onboard/iceportal.rs b/src/onboard/iceportal.rs new file mode 100644 index 0000000..04ec291 --- /dev/null +++ b/src/onboard/iceportal.rs @@ -0,0 +1,126 @@ +/// implementation of traits to query the iceportal.de +/// (available in high speed trains in DE) +use chrono::{DateTime, Utc}; +use serde::Deserialize; +use serde_json::Value; + +use crate::onboard; +use crate::onboard::{OnBoardAPI, OnBoardInfo}; +use crate::{serde::*, traits::*, travelynx::TrainRef}; + +pub struct Iceportal {} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct TripInfo { + trip: Trip, + connection: Option, + selected_route: Option, + active: Option +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Trip { + train_type: String, + vzn: String, // train number + // some position info here + actual_position: u64, // distance along track, presumably + stops: Vec +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Stop { + info: StopInfo, + station: Station, + timetable: Timetable +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct StopInfo { + distance_from_start: u64, + position_status: String // one of "departed", "future", ... ? +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Station { + eva_nr: String, + name: String +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Timetable { + #[serde(deserialize_with = "option_naive_read_unixtime_db")] + scheduled_arrival_time: Option>, + #[serde(deserialize_with = "option_naive_read_unixtime_db")] + actual_arrival_time: Option> +} + +impl IsStation for Stop { + fn name(&self) -> &str { + &self.station.name + } + + fn scheduled_arrival(&self) -> Option<&chrono::DateTime> { + self.timetable.scheduled_arrival_time.as_ref() + } + + fn real_arrival(&self) -> Option<&chrono::DateTime> { + self.timetable.scheduled_arrival_time.as_ref() + } + + fn ds100(&self) -> &str { + "??" + } +} + +impl OnBoardInfo for TripInfo { + fn guess_last_station(&self) -> Option<&dyn IsStation> { + let current_pos = self.trip.actual_position; + self + .trip + .stops + .iter() + .rev() + .map(|stop| (stop.info.distance_from_start, stop)) + .filter(|(dist, _)| dist <= ¤t_pos) + .next() + .map(|(_, stop)| stop as &dyn IsStation) + } + + fn get_train_ref(&self) -> TrainRef { + TrainRef { + _type: self.trip.train_type.clone(), + no: self.trip.vzn.clone() + } + } + + fn stops<'a>( + &'a self + ) -> Box + 'a> { + Box::new(self.trip.stops.iter().map(|s| s as &dyn IsStation)) + } +} + +impl std::fmt::Display for Stop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.station.name) + } +} + +impl OnBoardAPI for Iceportal { + fn apiurl(&self) -> &'static str { + "https://iceportal.de/api1/rs/tripInfo/trip" + } + + fn request( + &self, + debug: bool + ) -> Result, serde_json::Error> { + onboard::request::<_, TripInfo>(self, debug) + } +} diff --git a/src/onboard/mod.rs b/src/onboard/mod.rs new file mode 100644 index 0000000..56bd144 --- /dev/null +++ b/src/onboard/mod.rs @@ -0,0 +1,76 @@ +use crate::traits::IsStation; +use crate::travelynx::TrainRef; +use serde::de::DeserializeOwned; + +pub mod iceportal; +pub mod zugportal; + +pub fn choose_api(name: &str) -> &dyn OnBoardAPI { + match name { + "iceportal" => &iceportal::Iceportal {} as &dyn OnBoardAPI, + "zugportal" => &zugportal::Zugportal {} as &dyn OnBoardAPI, + _ => panic!("no such API known") + } +} + +pub trait OnBoardAPI { + fn apiurl(&self) -> &'static str; + + fn request( + &self, + debug: bool + ) -> Result, serde_json::Error>; +} + +pub fn request( + api: &Api, + debug: bool +) -> Result, serde_json::Error> +where + Api: OnBoardAPI, + I: OnBoardInfo + DeserializeOwned + 'static +{ + let url: &'static str = api.apiurl(); + match get_request::(url) { + Ok(resp) => Ok(Box::new(resp)), + Err((err, resp)) => { + if debug { + eprintln!("{:?}\n\nError was:{:?}", resp, err); + } + Err(err) + } + } +} + +pub trait OnBoardInfo { + fn guess_last_station(&self) -> Option<&dyn IsStation>; + + fn get_train_ref(&self) -> TrainRef; + + fn stops<'a>( + &'a self + ) -> Box + 'a>; +} + +fn get_request(uri: &str) -> Result +where + R: serde::de::DeserializeOwned +{ + let resp: String = ureq::get(uri) + .call() + .unwrap_or_else(|err| exit_err(&err.to_string(), "get request failed")) + .into_string() + .unwrap_or_else(|err| { + exit_err(&err.to_string(), "get request response failed") + }); + + match serde_json::from_str::(&resp) { + Ok(obj) => Ok(obj), + Err(err) => Err((err, resp)) + } +} + +fn exit_err(msg: &str, scope: &str) -> ! { + eprintln!("{}: {}", scope, msg); + std::process::exit(1) +} diff --git a/src/onboard/zugportal.rs b/src/onboard/zugportal.rs new file mode 100644 index 0000000..0ad6cfd --- /dev/null +++ b/src/onboard/zugportal.rs @@ -0,0 +1,109 @@ +/// implementation of traits to query zugportal.de +/// (available at least in the Munich S-Bahn, maybe other trains) +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +use crate::onboard; +use crate::onboard::{OnBoardAPI, OnBoardInfo}; +use crate::{traits::*, travelynx::TrainRef}; + +pub struct Zugportal {} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Journey { + name: String, // the line's name, e.g. S 8 + no: i64, + stops: Vec +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Stop { + station: Station, + status: String, // one of "Normal", ...? + track: Track, + messages: Vec, + arrival_time: Option, + departure_time: Option +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Station { + eva_no: String, + name: String +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct Track { + target: String, + prediction: String +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +struct DepartureTime { + target: DateTime, + predicted: DateTime, + time_type: String, // one of REAL, PREVIEW, ..? + diff: i64 // diff in minutes? + // NOTE: also sends predictedTimeInMs and targetTimeInMs; these might be unix times +} + +impl IsStation for Stop { + fn name(&self) -> &str { + &self.station.name + } + + fn scheduled_arrival(&self) -> Option<&chrono::DateTime> { + self.arrival_time.as_ref().map(|t| &t.target) + } + + fn real_arrival(&self) -> Option<&chrono::DateTime> { + self.arrival_time.as_ref().map(|t| &t.predicted) + } + + fn ds100(&self) -> &str { + "??" + } +} + +impl std::fmt::Display for Stop { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.station.name) + } +} + +impl OnBoardInfo for Journey { + fn guess_last_station(&self) -> Option<&dyn IsStation> { + todo!() + } + + fn get_train_ref(&self) -> TrainRef { + TrainRef { + _type: self.name.clone(), + no: self.no.to_string().clone() + } + } + + fn stops<'a>( + &'a self + ) -> Box + 'a> { + Box::new(self.stops.iter().map(|s| s as &dyn IsStation)) + } +} + +impl OnBoardAPI for Zugportal { + fn apiurl(&self) -> &'static str { + "https://zugportal.de/prd/zupo-travel-information/api/public/ri/journey" + } + + fn request( + &self, + debug: bool + ) -> Result, serde_json::Error> { + onboard::request::<_, Journey>(self, debug) + } +} diff --git a/src/traits.rs b/src/traits.rs new file mode 100644 index 0000000..a24a689 --- /dev/null +++ b/src/traits.rs @@ -0,0 +1,34 @@ +use chrono::{DateTime, Local, Utc}; +use colored::Colorize; + +pub trait IsStation { + fn name(&self) -> &str; + fn scheduled_arrival(&self) -> Option<&DateTime>; + fn real_arrival(&self) -> Option<&DateTime>; + fn ds100(&self) -> &str; + + fn to_fancy_string(&self) -> String { + format!( + "{} {} – {} ({})", + self + .real_arrival() // chrono's API for timezones is expressive, but reads like c++ … + .map(|t| >::from(*t).time().to_string()) + .unwrap_or("??:??:??".to_string()) + .blue(), + { + let delay = match (self.real_arrival(), self.scheduled_arrival()) { + (Some(a), Some(s)) => (a.time() - s.time()).num_minutes(), + _ => 0 + }; + let text = format!("({:+})", delay); + if delay > 0 { + text.red() + } else { + text.green() + } + }, + self.ds100().red(), + self.name() + ) + } +} diff --git a/src/types.rs b/src/types.rs index c1c949a..482025a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,9 +1,10 @@ use serde::{Deserialize, Deserializer}; -use chrono::{DateTime, Local, Utc}; +use chrono::{DateTime, Utc}; use colored::*; use crate::serde::*; +use crate::traits::IsStation; #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -44,38 +45,6 @@ pub struct Stop { real_departure: Option> } -pub trait IsStation { - fn name(&self) -> &str; - fn scheduled_arrival(&self) -> Option<&DateTime>; - fn real_arrival(&self) -> Option<&DateTime>; - fn ds100(&self) -> &str; - - fn to_fancy_string(&self) -> String { - format!( - "{} {} – {} ({})", - self - .real_arrival() // chrono's API for timezones is expressive, but reads like c++ … - .map(|t| >::from(*t).time().to_string()) - .unwrap_or("??:??:??".to_string()) - .blue(), - { - let delay = match (self.real_arrival(), self.scheduled_arrival()) { - (Some(a), Some(s)) => (a.time() - s.time()).num_minutes(), - _ => 0 - }; - let text = format!("({:+})", delay); - if delay > 0 { - text.red() - } else { - text.green() - } - }, - self.ds100().red(), - self.name() - ) - } -} - impl IsStation for Station { fn name(&self) -> &str { &self.name @@ -136,7 +105,9 @@ pub struct Ds100 { inner: String } -pub struct Trip<'a, S: IsStation>(pub &'a Vec); +pub struct Trip<'a, 'i, S: IsStation>( + pub &'i mut dyn std::iter::Iterator +); impl std::fmt::Display for Train { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -144,17 +115,20 @@ impl std::fmt::Display for Train { } } -#[allow(unstable_name_collisions)] -impl std::fmt::Display for Trip<'_, S> { +// #[allow(unstable_name_collisions)] +impl std::fmt::Display for Trip<'_, '_, S> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.0.len() != 0 { - self - .0 - .iter() - .map(|stop| stop.to_fancy_string()) - // .intersperse(" ↓".to_string()) - .for_each(|l| writeln!(f, " {}\n ↓", l).unwrap()); - } + todo!(); + // self.0.map(|stop| stop.to_fancy_string()) + // .for_each(|l| writeln!(f, " {}\n ↓", l).unwrap()); + // if self.0.len() != 0 { + // self + // .0 + // .iter() + // .map(|stop| stop.to_fancy_string()) + // // .intersperse(" ↓".to_string()) + // .for_each(|l| writeln!(f, " {}\n ↓", l).unwrap()); + // } Ok(()) } } @@ -180,7 +154,7 @@ impl std::fmt::Display for Status { .unwrap_or("".to_string()) .green(), self.from_station.to_fancy_string(), - Trip(&self.intermediate_stops), + Trip(&mut self.intermediate_stops.iter()), self .to_station .as_ref() diff --git a/src/zugportal.rs b/src/zugportal.rs deleted file mode 100644 index f00cd04..0000000 --- a/src/zugportal.rs +++ /dev/null @@ -1,97 +0,0 @@ -/// implementation of traits to query zugportal.de -/// (available at least in the Munich S-Bahn, maybe other trains) - -use chrono::{DateTime, Utc}; -use serde::Deserialize; -use serde_json::Value; - -use crate::{serde::*, travelynx::TrainRef, types::IsStation}; - -#[derive(Deserialize, Debug)] -#[serde(rename_all="camelCase")] -pub struct Journey { - name: String, // the line's name, e.g. S 8 - no: i64, - stops: Vec -} - - -#[derive(Deserialize, Debug)] -#[serde(rename_all="camelCase")] -pub struct Stop { - station: Station, - status: String, // one of "Normal", ...? - track: Track, - messages: Vec, - arrival_time: Option, - departure_time: Option -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all="camelCase")] -struct Station { - eva_no: String, - name: String -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all="camelCase")] -struct Track { - target: String, - prediction: String -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all="camelCase")] -struct DepartureTime { - target: DateTime, - predicted: DateTime, - time_type: String, // one of REAL, PREVIEW, ..? - diff: i64, // diff in minutes? - // NOTE: also sends predictedTimeInMs and targetTimeInMs; these might be unix times -} - -impl IsStation for Stop { - fn name(&self) -> &str { - &self.station.name - } - - fn scheduled_arrival(&self) -> Option<&chrono::DateTime> { - self.arrival_time.as_ref().map(|t| &t.target) - } - - fn real_arrival(&self) -> Option<&chrono::DateTime> { - self.arrival_time.as_ref().map(|t| &t.predicted) - } - - fn ds100(&self) -> &str { - "??" - } -} - -impl Journey { - pub fn guess_last_station(&self) -> Option { - todo!() - // let current_pos = self.trip.actual_position; - // self - // .trip - // .stops - // .iter() - // .rev() - // .map(|stop| (stop.info.distance_from_start, stop)) - // .filter(|(dist, _)| dist <= ¤t_pos) - // .next() - // .map(|(_, stop)| stop.station.name.clone()) - } - - pub fn get_train_ref(&self) -> TrainRef { - TrainRef { - _type: self.name.clone(), - no: self.no.to_string().clone() - } - } - - pub fn trip(&self) -> crate::types::Trip<'_, Stop> { - crate::types::Trip(&self.stops) - } -} -- cgit v1.2.3