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}