amtrak_api/client.rs
1//! Amtrak API Client
2//!
3//! The client allows the user to call the various different endpoints provided
4//! by the API.
5
6use crate::{errors, responses};
7
8/// Default endpoint for Amtrak API
9const BASE_API_URL: &str = "https://api-v3.amtraker.com/v3";
10
11pub type Result<T> = std::result::Result<T, errors::Error>;
12
13/// A client instance
14///
15/// Note: This does not represent an active connection. Connections are
16/// established when making an endpoint call and are not persistent after.
17#[derive(Debug, Clone)]
18pub struct Client {
19 base_url: String,
20}
21
22impl Default for Client {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl Client {
29 /// Creates a new instance with the default Amtrak API endpoint
30 ///
31 /// # Example
32 ///
33 /// ```rust
34 /// use amtrak_api::Client;
35 ///
36 /// #[tokio::main]
37 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
38 /// let client = Client::new();
39 /// Ok(())
40 /// }
41 /// ```
42 pub fn new() -> Self {
43 Self {
44 base_url: BASE_API_URL.to_string(),
45 }
46 }
47
48 /// Creates a new instance with the provided Amtrak endpoint
49 ///
50 /// This function is useful for testing since Mockito will create a local
51 /// endpoint
52 ///
53 /// # Arguments
54 ///
55 /// * `base_url` - The base url of the endpoint that this client will query
56 /// when making API calls.
57 ///
58 /// # Example
59 ///
60 /// ```rust
61 /// use amtrak_api::Client;
62 ///
63 /// #[tokio::main]
64 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
65 /// let client = Client::with_base_url("https://api-v3.amtraker.com/v3");
66 /// Ok(())
67 /// }
68 /// ```
69 pub fn with_base_url(base_url: &str) -> Self {
70 Self {
71 base_url: base_url.to_string(),
72 }
73 }
74
75 /// Returns all trains being tracked by Amtrak
76 ///
77 /// This function calls into the `/trains` endpoint.
78 ///
79 /// This function will list all current trains being tracked by the Amtrak
80 /// API. Check the [`TrainResponse`] struct for the schema and data that
81 /// this endpoint returns.
82 ///
83 /// # Example
84 ///
85 /// ```rust
86 /// use amtrak_api::{Client, TrainStatus};
87 /// use chrono::{Local, Utc};
88 ///
89 /// #[tokio::main]
90 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
91 /// Client::new()
92 /// .trains()
93 /// .await?
94 /// .into_iter()
95 /// .flat_map(|(_, trains)| {
96 /// trains
97 /// .into_iter()
98 /// .filter(|train| train.route_name == "Keystone")
99 /// })
100 /// .map(|train| {
101 /// let enroute_information = train
102 /// .stations
103 /// .iter()
104 /// .find(|station| station.status == TrainStatus::Enroute)
105 /// .map(|station| (station.name.clone(), station.arrival));
106 ///
107 /// (train, enroute_information)
108 /// })
109 /// .for_each(|(train, enroute_information)| {
110 /// if let Some((station_name, arrival)) = enroute_information {
111 /// let time_till_arrival = if let Some(arrival) = arrival {
112 /// let local_now = Local::now().with_timezone(&Utc);
113 /// let arrival_utc = arrival.with_timezone(&Utc);
114 ///
115 /// format!(
116 /// "{} minutes",
117 /// arrival_utc.signed_duration_since(local_now).num_minutes()
118 /// )
119 /// } else {
120 /// "N/A".to_string()
121 /// };
122 ///
123 /// println!(
124 /// "{} train is heading to {}, currently enroute to {} with an ETA of {}",
125 /// train.train_id, train.destination_name, station_name, time_till_arrival
126 /// );
127 /// } else {
128 /// println!(
129 /// "{} train is heading to {}",
130 /// train.train_id, train.destination_code
131 /// );
132 /// }
133 /// });
134 ///
135 /// Ok(())
136 /// }
137 /// ```
138 ///
139 /// [`TrainResponse`]: responses::TrainResponse
140 pub async fn trains(&self) -> Result<responses::TrainResponse> {
141 let url = format!("{}/trains", self.base_url);
142
143 let response = reqwest::Client::new()
144 .get(url)
145 .send()
146 .await?
147 .json::<responses::TrainResponseWrapper>()
148 .await?;
149
150 Ok(response.0)
151 }
152
153 /// Returns the specified train(s) being tracked by Amtrak
154 ///
155 /// This function calls into the `/trains/{:train_id}` endpoint.
156 ///
157 /// This function will list the specified train being tracked by the Amtrak
158 /// API. Check the [`TrainResponse`] struct for the schema and data that
159 /// this endpoint returns.
160 ///
161 /// # Arguments
162 ///
163 /// * `train_identifier` - Can either be the [`train_id`] or the
164 /// [`train_num`] of the train the caller wants to query.
165 ///
166 /// # Example
167 ///
168 /// ```rust
169 /// use amtrak_api::{Client, TrainStatus};
170 ///
171 /// const TRAIN_ID: &str = "612-5";
172 ///
173 /// #[tokio::main]
174 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
175 /// let client = Client::new();
176 ///
177 /// // Attempt to query the status of the "612-5" train
178 /// let response = client.train(TRAIN_ID).await?;
179 /// let train_612_5 = response.get(TRAIN_ID);
180 ///
181 /// match train_612_5 {
182 /// Some(trains) => match trains.len() {
183 /// 1 => {
184 /// let phl_station = trains
185 /// .get(0)
186 /// .unwrap()
187 /// .stations
188 /// .iter()
189 /// .find(|station| station.code == "PHL");
190 ///
191 /// match phl_station {
192 /// Some(phl_station) => match phl_station.status {
193 /// TrainStatus::Enroute => {
194 /// println!("Train is enroute to Philadelphia station")
195 /// }
196 /// TrainStatus::Station => {
197 /// println!("Train is current at Philadelphia station")
198 /// }
199 /// TrainStatus::Departed => {
200 /// println!("Train has departed Philadelphia station")
201 /// }
202 /// TrainStatus::Unknown => println!("The train status is unknown"),
203 /// },
204 /// None => println!(
205 /// "Philadelphia station was not found in the \"{}\" route",
206 /// TRAIN_ID
207 /// ),
208 /// }
209 /// }
210 /// 0 => println!("Train \"{}\" response was empty", TRAIN_ID),
211 /// _ => println!("More than one train returned for \"{}\"", TRAIN_ID),
212 /// },
213 /// None => println!(
214 /// "Train \"{}\" is not currently in the Amtrak network",
215 /// TRAIN_ID
216 /// ),
217 /// }
218 ///
219 /// Ok(())
220 /// }
221 /// ```
222 ///
223 /// [`TrainResponse`]: responses::TrainResponse
224 /// [`train_id`]: responses::Train::train_id
225 /// [`train_num`]: responses::Train::train_num
226 pub async fn train<S: AsRef<str>>(
227 &self,
228 train_identifier: S,
229 ) -> Result<responses::TrainResponse> {
230 let url = format!("{}/trains/{}", self.base_url, train_identifier.as_ref());
231
232 let response = reqwest::Client::new()
233 .get(url)
234 .send()
235 .await?
236 .json::<responses::TrainResponseWrapper>()
237 .await?;
238
239 Ok(response.0)
240 }
241
242 /// Returns all the stations in the Amtrak network
243 ///
244 /// This function calls into the `/stations` endpoint.
245 ///
246 /// This function will list all the stations in the Amtrak network. Check
247 /// the [`StationResponse`] struct for the schema and data that this
248 /// endpoint returns.
249 ///
250 /// # Example
251 ///
252 /// ```rust
253 /// use amtrak_api::Client;
254 ///
255 /// #[tokio::main]
256 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
257 /// Client::new()
258 /// .stations()
259 /// .await?
260 /// .values()
261 /// .filter(|station| station.state == "PA")
262 /// .for_each(|station| {
263 /// println!("Station \"{}\" is in PA", station.name);
264 /// });
265 ///
266 /// Ok(())
267 /// }
268 /// ```
269 ///
270 /// [`StationResponse`]: responses::StationResponse
271 pub async fn stations(&self) -> Result<responses::StationResponse> {
272 let url = format!("{}/stations", self.base_url);
273
274 let response = reqwest::Client::new()
275 .get(url)
276 .send()
277 .await?
278 .json::<responses::StationResponseWrapper>()
279 .await?;
280
281 Ok(response.0)
282 }
283
284 /// Returns the specified station in the Amtrak network
285 ///
286 /// This function calls into the `/stations/{:station_code}` endpoint.
287 ///
288 /// This function will query the station with the provided `station_code`.
289 /// Check the [`StationResponse`] struct for the schema and data that this
290 /// endpoint returns.
291 ///
292 /// # Arguments
293 ///
294 /// * `station_code` - The station [`code`] the caller wants to query.
295 ///
296 /// # Example
297 ///
298 /// ```rust
299 /// use amtrak_api::Client;
300 ///
301 /// const STATION_CODE: &str = "PHL";
302 ///
303 /// #[tokio::main]
304 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
305 /// Client::new()
306 /// .station(STATION_CODE)
307 /// .await?
308 /// .values()
309 /// .for_each(|station| {
310 /// println!(
311 /// "Current train scheduled for station \"{}\": {}",
312 /// station.name,
313 /// station.trains.join(", ")
314 /// );
315 /// });
316 ///
317 /// Ok(())
318 /// }
319 /// ```
320 ///
321 /// [`StationResponse`]: responses::StationResponse
322 /// [`code`]: responses::TrainStation::code
323 pub async fn station<S: AsRef<str>>(
324 &self,
325 station_code: S,
326 ) -> Result<responses::StationResponse> {
327 let url = format!("{}/stations/{}", self.base_url, station_code.as_ref());
328
329 let response = reqwest::Client::new()
330 .get(url)
331 .send()
332 .await?
333 .json::<responses::StationResponseWrapper>()
334 .await?;
335
336 Ok(response.0)
337 }
338}