From 24cf67046589f07bf5bf283316dc5e3355644bc7 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sun, 20 Apr 2025 14:45:31 -0600 Subject: [PATCH] Flesh out api and add tests --- .gitignore | 1 + Cargo.lock | 13 ++++++++ Cargo.toml | 1 + src/lib.rs | 76 ++++++++++++++++++++++++++++++++++++++++---- src/models/eta.rs | 50 +++++++++++++++++++++++++++++ src/models/follow.rs | 53 ++++++++++++++++++++++++++++++ src/models/mod.rs | 2 ++ src/models/route.rs | 1 - 8 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 src/models/eta.rs create mode 100644 src/models/follow.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..0b745e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 54971dd..373b091 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,7 @@ dependencies = [ "serde", "serde_json", "thiserror", + "tokio", "url", ] @@ -1093,9 +1094,21 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 15bae83..2b46328 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,5 @@ reqwest = { version = "0.12.15", features = ["json"] } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" thiserror = "2.0.12" +tokio = { version = "1.44.2", features = ["macros", "rt"] } url = "2.5.4" diff --git a/src/lib.rs b/src/lib.rs index 1037a72..0618fcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ use crate::models::Response; +use crate::models::eta::{EtaRequest, EtaResp}; +use crate::models::follow::{FollowRequest, FollowResp}; use crate::models::route::{RouteRequest, RouteResp}; use reqwest::{Client, Url}; +use serde::{Deserialize, Serialize}; use thiserror::Error; pub mod models; @@ -32,18 +35,19 @@ impl CTAClient { } } - pub async fn fetch_route(&mut self, route: &str) -> Result { - let req = RouteRequest { - rt: route.to_string(), - key: self.key.clone(), - }; + pub async fn make_request Deserialize<'a>>( + &mut self, + endpoint: &str, + req: T, + ) -> Result { + let url = self.url.join(endpoint)?; - let url = self.url.join("ttpositions.aspx")?; - let resp: Response = self + let resp: Response = self .client .get(url) .query(&req) .query(&[("outputType", "JSON")]) + .query(&[("key", &self.key)]) .send() .await? .json() @@ -51,4 +55,62 @@ impl CTAClient { Ok(resp.ctatt) } + + pub async fn fetch_route(&mut self, route: &str) -> Result { + let req = RouteRequest { + rt: route.to_string(), + }; + + self.make_request("ttpositions.aspx", req).await + } + + pub async fn fetch_eta(&mut self, eta_request: EtaRequest) -> Result { + self.make_request("ttarrivals.aspx", eta_request).await + } + + pub async fn fetch_train_schedule(&mut self, run_number: u32) -> Result { + self.make_request("ttfollow.aspx", FollowRequest { run_number }) + .await + } +} + +#[cfg(test)] +mod tests { + use crate::CTAClient; + use crate::models::eta::EtaRequest; + + pub fn client() -> CTAClient { + let token = std::env::var("CTA_TOKEN").expect("Missing CTA_TOKEN"); + + CTAClient::new(token) + } + + #[tokio::test] + pub async fn test_fetch_eta() { + let mut client = client(); + + let _eta = client + .fetch_eta(EtaRequest { + mapid: None, + stpid: Some(30111), + max: Some(1), + rt: None, + }) + .await + .unwrap(); + } + + #[tokio::test] + pub async fn test_fetch_route() { + let mut client = client(); + + let _eta = client.fetch_route("blue").await.unwrap(); + } + + #[tokio::test] + pub async fn test_fetch_train_schedule() { + let mut client = client(); + + let _train = client.fetch_train_schedule(109).await.unwrap(); + } } diff --git a/src/models/eta.rs b/src/models/eta.rs new file mode 100644 index 0000000..b2d45a6 --- /dev/null +++ b/src/models/eta.rs @@ -0,0 +1,50 @@ +use crate::models::Ctatt; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Debug)] +pub struct EtaRequest { + pub mapid: Option, + pub stpid: Option, + pub max: Option, + pub rt: Option, +} + +#[derive(Deserialize, Debug)] +pub struct EtaResp { + #[serde(flatten)] + pub header: Ctatt, + pub eta: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct Eta { + #[serde(rename = "staId")] + pub sta_id: String, + #[serde(rename = "stpId")] + pub stp_id: String, + #[serde(rename = "staNm")] + pub sta_nm: String, + #[serde(rename = "stpDe")] + pub stp_de: String, + pub rn: String, + pub rt: String, + #[serde(rename = "destSt")] + pub dest_st: String, + #[serde(rename = "destNm")] + pub tr_dr: String, + pub prdt: String, + #[serde(rename = "arrT")] + pub arr_t: String, + #[serde(rename = "isApp")] + pub is_app: String, + #[serde(rename = "isSch")] + pub is_sch: String, + #[serde(rename = "isDly")] + pub is_dly: String, + #[serde(rename = "isFlt")] + pub is_flt: String, + pub flags: Option, + pub lat: String, + pub lon: String, + pub heading: String, +} diff --git a/src/models/follow.rs b/src/models/follow.rs new file mode 100644 index 0000000..6ccb696 --- /dev/null +++ b/src/models/follow.rs @@ -0,0 +1,53 @@ +use crate::models::Ctatt; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Debug)] +pub struct FollowRequest { + #[serde(rename = "runnumber")] + pub run_number: u32, +} + +#[derive(Deserialize, Debug)] +pub struct Position { + pub lat: String, + pub lon: String, + pub heading: String, +} + +#[derive(Deserialize, Debug)] +pub struct ScheduledStop { + #[serde(rename = "staId")] + pub sta_id: String, + #[serde(rename = "stpId")] + pub stp_id: String, + #[serde(rename = "staNm")] + pub sta_nm: String, + #[serde(rename = "stpDe")] + pub stp_de: String, + pub rn: String, + pub rt: String, + #[serde(rename = "destSt")] + pub dest_st: String, + #[serde(rename = "destNm")] + pub tr_dr: String, + pub prdt: String, + #[serde(rename = "arrT")] + pub arr_t: String, + #[serde(rename = "isApp")] + pub is_app: String, + #[serde(rename = "isSch")] + pub is_sch: String, + #[serde(rename = "isDly")] + pub is_dly: String, + #[serde(rename = "isFlt")] + pub is_flt: String, + pub flags: Option, +} + +#[derive(Deserialize, Debug)] +pub struct FollowResp { + #[serde(flatten)] + pub header: Ctatt, + pub position: Position, + pub eta: Vec, +} diff --git a/src/models/mod.rs b/src/models/mod.rs index d4da7f6..ebf10b2 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,5 @@ +pub mod eta; +pub mod follow; pub mod route; pub mod train; diff --git a/src/models/route.rs b/src/models/route.rs index 816eade..3faa38e 100644 --- a/src/models/route.rs +++ b/src/models/route.rs @@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Debug)] pub struct RouteRequest { pub rt: String, - pub key: String, } #[derive(Deserialize, Debug)]