septa_api/
client.rs

1use crate::{errors, requests, responses};
2use serde::de::DeserializeOwned;
3
4const BASE_API_URL: &str = "https://www3.septa.org/api";
5
6pub type Result<T> = std::result::Result<T, errors::Error>;
7
8#[derive(Debug, Clone)]
9pub struct Client {
10    base_url: String,
11}
12
13impl Default for Client {
14    fn default() -> Self {
15        Self::new()
16    }
17}
18
19impl Client {
20    pub fn new() -> Self {
21        Self {
22            base_url: BASE_API_URL.to_string(),
23        }
24    }
25
26    pub fn with_base_url(base_url: &str) -> Self {
27        Self {
28            base_url: base_url.to_string(),
29        }
30    }
31
32    async fn get<R: DeserializeOwned>(&self, endpoint: &str) -> Result<R> {
33        let url = format!("{}{}", self.base_url, endpoint);
34
35        let response = reqwest::Client::new()
36            .get(url)
37            .send()
38            .await?
39            .json::<responses::ApiResponse<R>>()
40            .await?;
41
42        match response {
43            responses::ApiResponse::Error(error) => Err(errors::Error::ApiErrorResponse(error)),
44            responses::ApiResponse::Response(response) => Ok(response),
45        }
46    }
47
48    async fn get_request<T: requests::Request, R: DeserializeOwned>(
49        &self,
50        endpoint: &str,
51        request: T,
52    ) -> Result<R> {
53        let url = format!("{}{}", self.base_url, endpoint);
54
55        let response = reqwest::Client::new()
56            .get(url)
57            .query(&request.into_params())
58            .send()
59            .await?
60            .json::<responses::ApiResponse<R>>()
61            .await?;
62
63        match response {
64            responses::ApiResponse::Error(error) => Err(errors::Error::ApiErrorResponse(error)),
65            responses::ApiResponse::Response(response) => Ok(response),
66        }
67    }
68
69    /// Returns a list of regional rail trains to arrive at a given station
70    ///
71    /// This function calls into the `/Arrivals/index.php` endpoint.
72    ///
73    /// The returned list is split into northbound and southbound trains. The definition of "Northbound" and "Southbound"
74    /// is defined by SEPTA as the following:
75    /// The direction are obviously not geographical references, but rather a reference to the old Reading and
76    /// Pennsy Railroads. The key to understanding the direction is by using Suburban Station as a starting
77    /// point: Any trains that move eastbound towards Market East are all considered Northbound; trains going
78    /// from suburban to 30th St are all Southbound. The path field describes more accurately the path of travel
79    /// along various branches.
80    ///
81    /// # Arguments
82    ///
83    /// * `request` - A struct containing the request parameters
84    ///
85    /// # Example
86    ///
87    /// ```
88    /// use septa_api::Client;
89    /// use septa_api::requests::ArrivalsRequest;
90    /// use septa_api::types::RegionalRailStop;
91    ///
92    /// #[tokio::main]
93    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
94    ///     let client = Client::new();
95    ///     let request = ArrivalsRequest {
96    ///         station: RegionalRailStop::SuburbanStation,
97    ///         results: Some(5),
98    ///         direction: None
99    ///     };
100    ///     let response = client.arrivals(request).await?;
101    ///
102    ///     // Loop through the northbound arrivals
103    ///     for train in response.northbound {
104    ///         println!("Train {} is currently {} on the {} line", train.train_id, train.status, train.line.to_string());
105    ///     }
106    ///
107    ///     // Loop through the southbound arrivals
108    ///     for train in response.southbound {
109    ///         println!("Train {} is currently {} on the {} line", train.train_id, train.status, train.line.to_string());
110    ///     }
111    ///
112    ///     Ok(())
113    /// }
114    /// ```
115    pub async fn arrivals(
116        &self,
117        request: requests::ArrivalsRequest,
118    ) -> Result<responses::ArrivalsResponse> {
119        self.get_request("/Arrivals/index.php", request).await
120    }
121
122    /// Returns a list of all active regional rail trains
123    ///
124    /// This function calls into the `/TrainView/index.php` endpoint.
125    ///
126    /// # Example
127    ///
128    /// ```
129    /// use septa_api::Client;
130    ///
131    /// #[tokio::main]
132    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
133    ///     let client = Client::new();
134    ///     let response = client.train_view().await?;
135    ///
136    ///     for train in response {
137    ///         println!("Train {} is currently {} mins late on the {} line", train.train_number, train.late, train.line.to_string());
138    ///     }
139    ///
140    ///     Ok(())
141    /// }
142    pub async fn train_view(&self) -> Result<responses::TrainResponse> {
143        self.get("/TrainView/index.php").await
144    }
145
146    /// Returns departure and arrival times between two different stations
147    ///
148    /// This function calls into the `/NextToArrive/index.php` endpoint.
149    ///
150    /// # Arguments
151    ///
152    /// * `request` - A struct containing the request parameters
153    ///
154    /// # Example
155    ///
156    /// ```
157    /// use septa_api::Client;
158    /// use septa_api::requests::NextToArriveRequest;
159    /// use septa_api::types::RegionalRailStop;
160    ///
161    /// #[tokio::main]
162    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
163    ///     let client = Client::new();
164    ///     let request = NextToArriveRequest {
165    ///         starting_station: RegionalRailStop::SuburbanStation,
166    ///         ending_station: RegionalRailStop::Downingtown,
167    ///         results: None,
168    ///     };
169    ///     let response = client.next_to_arrive(request).await?;
170    ///
171    ///     for next_to_arrive in response {
172    ///        println!("Train {} is scheduled to arrive {}", next_to_arrive.orig_train, next_to_arrive.orig_departure_time);
173    ///     }
174    ///
175    ///    Ok(())
176    /// }
177    pub async fn next_to_arrive(
178        &self,
179        request: requests::NextToArriveRequest,
180    ) -> Result<responses::NextToArriveResponse> {
181        self.get_request("/NextToArrive/index.php", request).await
182    }
183
184    /// Returns the schedule for a train by the train's number
185    ///
186    /// This function calls into the `/RRSchedules/index.php` endpoint.
187    ///
188    /// # Arguments
189    ///
190    /// * `request` - A struct containing the request parameters
191    ///
192    /// # Example
193    ///
194    /// ```
195    /// use septa_api::Client;
196    /// use septa_api::requests::RailScheduleRequest;
197    ///
198    /// #[tokio::main]
199    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
200    ///     let client = Client::new();
201    ///     let request = RailScheduleRequest {
202    ///         train_number: "514".to_string()
203    ///     };
204    ///     let response = client.rail_schedule(request).await?;
205    ///
206    ///     for schedule in response {
207    ///        println!("The train is scheduled to arrive at {} at {}", schedule.station.to_string(), schedule.scheduled_time);
208    ///     }
209    ///
210    ///    Ok(())
211    /// }
212    pub async fn rail_schedule(
213        &self,
214        request: requests::RailScheduleRequest,
215    ) -> Result<responses::RailScheduleResponse> {
216        self.get_request("/RRSchedules/index.php", request).await
217    }
218}