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 …
Diffstat (limited to '')
| -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) -  } -}  | 
