From 7de36af3c6ffcc25832a6ff2303ba6c4c1101de5 Mon Sep 17 00:00:00 2001 From: stuebinm Date: Sun, 4 Dec 2022 22:03:43 +0100 Subject: a couple more features (filtering, read from file & pipe) --- Readme.md | 33 ++++++++------- src/main.rs | 130 ++++++++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 123 insertions(+), 40 deletions(-) diff --git a/Readme.md b/Readme.md index a34e921..f754362 100644 --- a/Readme.md +++ b/Readme.md @@ -3,33 +3,36 @@ a tool to answer to the question "how to check that the gtfs realtime feed says what it's supposed to if for some unfortunate reason you can't read binary?". -At 188 crates, it might be suspected that it gives a *slightly* over-engineered +At 197 crates, it might be suspected that it gives a *slightly* over-engineered answer. ~~~ -Usage: showrt [OPTIONS] +Usage: showrt [OPTIONS] [URL_OR_FILE] Arguments: - uri of the GTFS RT feed to fetch & display + [URL_OR_FILE] Either a file or an URI containing the gtfs realtime feed. + Omit this to read from stdin instead Options: - --json emit the feed as json - -i, --ignore-nonfatal ignore things that look wrong as long as possible - --no-colors don't do terminal colours - -h, --help Print help information - -V, --version Print version information + --json emit the feed as json + -f, --filter-trip filter for FeedEntities affecting the given trip id + -i, --ignore-content-type ignore the Content-Type header (default is to + abort if it's not application/octet-string) + --no-colors don't do terminal colours in output + -h, --help Print help information + -V, --version Print version information ~~~ ### Features: - - fetch feeds from some url - - display feeds in protobuf pseudo-format, optionally with fancy colours - - dump the entire feed as json to process it with `jq` & friends + - read feeds from an url, a file, or from stdin + - display feeds in fancy colours (or without them) and with timestamps in a + more human-readable format than seconds since the unix epoch + - dump the entire feed as json + - be useful with pipes, e.g. fetch with `curl` or process with `jq` + - filter for a specific trip (works well with `watch` to observe a single trip) - error messages with entirely too many arrows in them ### Still Todo: - - open local files (why do people even have these?) - - play nice with (input-)pipes - - optionally convert all dates into something more human-friendly than seconds - since the unix epoch + - disable colours in case of pipes - optionally collapse translations that contain only one language in the output diff --git a/src/main.rs b/src/main.rs index 315d093..e96bbd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,31 @@ mod protos; mod fancy; -use protos::protos::gtfs_realtime::FeedMessage; +use protos::protos::gtfs_realtime::{FeedMessage, TripUpdate, VehiclePosition, TripDescriptor, EntitySelector, Alert, FeedEntity}; +use std::io::Read; use protobuf::Message; use clap::Parser; // use anyhow::Context; use miette::{WrapErr, IntoDiagnostic, miette}; -use reqwest::header::{HeaderValue, CONTENT_TYPE}; +use reqwest::{header::{HeaderValue, CONTENT_TYPE}, Url}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - /// uri of the GTFS RT feed to fetch & display - url: String, + /// Either a file or an URI containing the gtfs realtime feed. + /// Omit this to read from stdin instead. + url_or_file: Option, /// emit the feed as json #[arg(long)] json: bool, - /// ignore things that look wrong as long as possible - #[arg(long="ignore-nonfatal", short='i')] - ignore_nonfatal: bool, - /// don't do terminal colours + /// filter for FeedEntities affecting the given trip id + #[arg(short='f', long="filter-trip")] + filter_trip: Option, + /// ignore the Content-Type header (default is to abort if it's not application/octet-string) + #[arg(long="ignore-content-type", short='i')] + ignore_content_type: bool, + /// don't do terminal colours in output #[arg(long="no-colors")] no_colors: bool } @@ -30,29 +35,60 @@ struct Args { async fn main() -> miette::Result<()> { let args = Args::parse(); - let resp = reqwest::get(&args.url) - .await.into_diagnostic().wrap_err("Request failed")? - .error_for_status().into_diagnostic()?; - - if !args.ignore_nonfatal { - let ct = HeaderValue::from_static("application/octet-stream"); - match resp.headers().get(CONTENT_TYPE) { - Some(content_type) if ct == content_type => (), - Some(other_type) => - Err(miette!("should be {:?}", ct)) - .wrap_err(format!("Bad Content-Type {:?}", other_type))?, - None => - Err(miette!("Content-Type header is missing"))? + + let resp = match args.url_or_file { + Some(url_or_file) => match Url::parse(&url_or_file) { + Ok(url) => { + let resp = reqwest::get(url).await + .into_diagnostic() + .wrap_err("Request failed")? + .error_for_status().into_diagnostic()?; + + if !args.ignore_content_type { + let ct = HeaderValue::from_static("application/octet-stream"); + match resp.headers().get(CONTENT_TYPE) { + Some(content_type) if ct == content_type => (), + Some(other_type) => + Err(miette!("should be {:?}", ct)) + .wrap_err(format!("Bad Content-Type {:?}", other_type))?, + None => + Err(miette!("Content-Type header is missing"))? + } + } + + resp.bytes().await.into_diagnostic()?.into() + }, + Err(_) => { + std::fs::read(&url_or_file) + .into_diagnostic() + .wrap_err(format!("failed to read file: {}", url_or_file))? + } + }, + None => { + let mut buf = Vec::new(); + let mut stdin = std::io::stdin(); + stdin.read_to_end(&mut buf).into_diagnostic()?; + buf } - } + }; - let resp = resp - .bytes().await.into_diagnostic()?; - let proto = FeedMessage::parse_from_bytes(&resp[..]) + let mut proto = FeedMessage::parse_from_bytes(&resp[..]) .into_diagnostic() .wrap_err("Could not parse protobuf format")?; + if let Some(filter) = args.filter_trip { + proto = FeedMessage { + entity: proto + .entity + .iter() + .filter(|entity| entity.concerns(&filter)) + .map(|s| s.to_owned()) + .collect(), + ..proto + }; + } + match args.json { true => println!("{}", protobuf_json_mapping::print_to_string(&proto).into_diagnostic()?), @@ -65,3 +101,47 @@ async fn main() -> miette::Result<()> { Ok(()) } +trait Concerns { + fn concerns(&self, trip_id: &str) -> bool; +} + +impl Concerns for TripDescriptor { + fn concerns(&self, trip_id: &str) -> bool { + match &self.trip_id { + Some(id) if id == trip_id => true, + _ => false + } + } +} + +impl Concerns for TripUpdate { + fn concerns(&self, trip_id: &str) -> bool { + self.trip.concerns(trip_id) + } +} + +impl Concerns for VehiclePosition { + fn concerns(&self, trip_id: &str) -> bool { + self.trip.concerns(trip_id) + } +} + +impl Concerns for EntitySelector { + fn concerns(&self, trip_id: &str) -> bool { + self.trip.concerns(trip_id) + } +} + +impl Concerns for Alert { + fn concerns(&self, trip_id: &str) -> bool { + self.informed_entity.iter().any(|e| e.concerns(trip_id)) + } +} + +impl Concerns for FeedEntity { + fn concerns(&self, trip_id: &str) -> bool { + self.trip_update.concerns(trip_id) + || self.vehicle.concerns(trip_id) + || self.alert.concerns(trip_id) + } +} -- cgit v1.2.3