diff options
author | stuebinm | 2022-02-09 02:51:59 +0100 |
---|---|---|
committer | stuebinm | 2022-02-09 02:51:59 +0100 |
commit | cd13c85c69cf761b2da84ad91af64d23a3568aa5 (patch) | |
tree | 4cbdbf6f348508ad7f70aa11450dccc8148ae0cb | |
parent | cf88935b5245daea51d2b513709b61a0e43483d6 (diff) |
existential types in rust are weird
… lots and lots of traits …
-rw-r--r-- | src/lib.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 89 | ||||
-rw-r--r-- | src/onboard/iceportal.rs (renamed from src/iceportal.rs) | 41 | ||||
-rw-r--r-- | src/onboard/mod.rs | 76 | ||||
-rw-r--r-- | src/onboard/zugportal.rs | 109 | ||||
-rw-r--r-- | src/traits.rs | 34 | ||||
-rw-r--r-- | src/types.rs | 64 | ||||
-rw-r--r-- | src/zugportal.rs | 97 |
8 files changed, 312 insertions, 202 deletions
@@ -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::<TripInfo>( - "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::<traveltext::zugportal::Journey>( - // "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/iceportal.rs b/src/onboard/iceportal.rs index 3cb5e6f..04ec291 100644 --- a/src/iceportal.rs +++ b/src/onboard/iceportal.rs @@ -1,8 +1,14 @@ +/// 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::{serde::*, travelynx::TrainRef, types::IsStation}; +use crate::onboard; +use crate::onboard::{OnBoardAPI, OnBoardInfo}; +use crate::{serde::*, traits::*, travelynx::TrainRef}; + +pub struct Iceportal {} #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -72,8 +78,8 @@ impl IsStation for Stop { } } -impl TripInfo { - pub fn guess_last_station(&self) -> Option<String> { +impl OnBoardInfo for TripInfo { + fn guess_last_station(&self) -> Option<&dyn IsStation> { let current_pos = self.trip.actual_position; self .trip @@ -83,17 +89,38 @@ impl TripInfo { .map(|stop| (stop.info.distance_from_start, stop)) .filter(|(dist, _)| dist <= ¤t_pos) .next() - .map(|(_, stop)| stop.station.name.clone()) + .map(|(_, stop)| stop as &dyn IsStation) } - pub fn get_train_ref(&self) -> TrainRef { + 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) + fn stops<'a>( + &'a self + ) -> Box<dyn std::iter::Iterator<Item = &'a dyn IsStation> + '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<Box<dyn OnBoardInfo>, 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<Box<dyn OnBoardInfo>, serde_json::Error>; +} + +pub fn request<Api, I>( + api: &Api, + debug: bool +) -> Result<Box<dyn OnBoardInfo>, serde_json::Error> +where + Api: OnBoardAPI, + I: OnBoardInfo + DeserializeOwned + 'static +{ + let url: &'static str = api.apiurl(); + match get_request::<I>(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<dyn std::iter::Iterator<Item = &'a dyn IsStation> + 'a>; +} + +fn get_request<R>(uri: &str) -> Result<R, (serde_json::Error, String)> +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::<R>(&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<Stop> +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Stop { + station: Station, + status: String, // one of "Normal", ...? + track: Track, + messages: Vec<String>, + arrival_time: Option<DepartureTime>, + departure_time: Option<DepartureTime> +} + +#[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<Utc>, + predicted: DateTime<Utc>, + 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<Utc>> { + self.arrival_time.as_ref().map(|t| &t.target) + } + + fn real_arrival(&self) -> Option<&chrono::DateTime<Utc>> { + 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<dyn std::iter::Iterator<Item = &'a dyn IsStation> + '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<Box<dyn OnBoardInfo>, 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<Utc>>; + fn real_arrival(&self) -> Option<&DateTime<Utc>>; + 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| <DateTime<Local>>::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<DateTime<Utc>> } -pub trait IsStation { - fn name(&self) -> &str; - fn scheduled_arrival(&self) -> Option<&DateTime<Utc>>; - fn real_arrival(&self) -> Option<&DateTime<Utc>>; - 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| <DateTime<Local>>::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<S>); +pub struct Trip<'a, 'i, S: IsStation>( + pub &'i mut dyn std::iter::Iterator<Item = &'a S> +); 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<S: IsStation> std::fmt::Display for Trip<'_, S> { +// #[allow(unstable_name_collisions)] +impl<S: IsStation> 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<Stop> -} - - -#[derive(Deserialize, Debug)] -#[serde(rename_all="camelCase")] -pub struct Stop { - station: Station, - status: String, // one of "Normal", ...? - track: Track, - messages: Vec<String>, - arrival_time: Option<DepartureTime>, - departure_time: Option<DepartureTime> -} - -#[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<Utc>, - predicted: DateTime<Utc>, - 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<Utc>> { - self.arrival_time.as_ref().map(|t| &t.target) - } - - fn real_arrival(&self) -> Option<&chrono::DateTime<Utc>> { - self.arrival_time.as_ref().map(|t| &t.predicted) - } - - fn ds100(&self) -> &str { - "??" - } -} - -impl Journey { - pub fn guess_last_station(&self) -> Option<String> { - 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) - } -} |