summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuebinm2022-02-09 02:51:59 +0100
committerstuebinm2022-02-09 02:51:59 +0100
commitcd13c85c69cf761b2da84ad91af64d23a3568aa5 (patch)
tree4cbdbf6f348508ad7f70aa11450dccc8148ae0cb
parentcf88935b5245daea51d2b513709b61a0e43483d6 (diff)
existential types in rust are weird
… lots and lots of traits …
-rw-r--r--src/lib.rs4
-rw-r--r--src/main.rs89
-rw-r--r--src/onboard/iceportal.rs (renamed from src/iceportal.rs)41
-rw-r--r--src/onboard/mod.rs76
-rw-r--r--src/onboard/zugportal.rs109
-rw-r--r--src/traits.rs34
-rw-r--r--src/types.rs64
-rw-r--r--src/zugportal.rs97
8 files changed, 312 insertions, 202 deletions
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::<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 <= &current_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 <= &current_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)
- }
-}