Flesh out api and add tests

This commit is contained in:
Joey Hines 2025-04-20 14:45:31 -06:00
parent e47099f492
commit 24cf670465
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
8 changed files with 189 additions and 8 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
.env

13
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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<RouteResp, Error> {
let req = RouteRequest {
rt: route.to_string(),
key: self.key.clone(),
};
pub async fn make_request<T: Serialize, V: for<'a> Deserialize<'a>>(
&mut self,
endpoint: &str,
req: T,
) -> Result<V, Error> {
let url = self.url.join(endpoint)?;
let url = self.url.join("ttpositions.aspx")?;
let resp: Response<RouteResp> = self
let resp: Response<V> = 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<RouteResp, Error> {
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<EtaResp, Error> {
self.make_request("ttarrivals.aspx", eta_request).await
}
pub async fn fetch_train_schedule(&mut self, run_number: u32) -> Result<FollowResp, Error> {
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();
}
}

50
src/models/eta.rs Normal file
View File

@ -0,0 +1,50 @@
use crate::models::Ctatt;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)]
pub struct EtaRequest {
pub mapid: Option<u32>,
pub stpid: Option<u32>,
pub max: Option<u32>,
pub rt: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct EtaResp {
#[serde(flatten)]
pub header: Ctatt,
pub eta: Vec<Eta>,
}
#[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<String>,
pub lat: String,
pub lon: String,
pub heading: String,
}

53
src/models/follow.rs Normal file
View File

@ -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<String>,
}
#[derive(Deserialize, Debug)]
pub struct FollowResp {
#[serde(flatten)]
pub header: Ctatt,
pub position: Position,
pub eta: Vec<ScheduledStop>,
}

View File

@ -1,3 +1,5 @@
pub mod eta;
pub mod follow;
pub mod route;
pub mod train;

View File

@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)]
pub struct RouteRequest {
pub rt: String,
pub key: String,
}
#[derive(Deserialize, Debug)]