at_api_rs/realtime.rs
1use std::collections::HashMap;
2
3use crate::{
4 error::Result,
5 types::{gtfs::Entity, ATResponse, Header},
6 BASE_API_URL,
7};
8use reqwest::{Client, Method};
9
10/// A client for interacting with the Auckland Transport GTFS realtime API.
11pub struct Realtime<'a> {
12 client: Client,
13 api_key: &'a str,
14}
15
16impl<'a> Realtime<'a> {
17 /// Creates a new Auckland Transport GTFS realtime client.
18 ///
19 /// # Parameters
20 ///
21 /// * `api_key` - The API key to use when interacting with the API.
22 pub fn new(api_key: &'a str) -> Self {
23 Self {
24 client: Client::new(),
25 api_key,
26 }
27 }
28
29 /// Fetches both trip updates and vehicle positions from the AT API.
30 ///
31 /// AT sends the trip updates and vehicle positions seperate, these are joined together upon
32 /// collection in the function, joined by trip ID.
33 ///
34 /// Parameters can be used to query for specific vehicles or trips. If [`None`] is given for
35 /// both fields, all vehicles will be returned.
36 ///
37 /// # Parameters
38 ///
39 /// * `trip_ids` - A list of trip IDs to search for.
40 /// * `vehicle_ids` - A list of vehicle IDs to search for.
41 ///
42 /// # Returns
43 ///
44 /// Returns a tuple where the first item is the response header received from AT, and the
45 /// second item is a vector of AT vehicles.
46 ///
47 /// [`None`]: std::option::Option::None
48 pub async fn fetch_combined<'b>(
49 &self,
50 trip_ids: Option<&Vec<&'b str>>,
51 vehicle_ids: Option<&Vec<&'b str>>,
52 ) -> Result<(Header, Vec<Entity>)> {
53 let url = format!("{}/public/realtime", BASE_API_URL);
54 let mut params = vec![];
55
56 if let Some(trips) = trip_ids {
57 params.push(("tripid", trips.join(",")));
58 }
59
60 if let Some(vehicles) = vehicle_ids {
61 params.push(("vehicleid", vehicles.join(",")));
62 }
63
64 let resp = self
65 .request(Method::GET, Self::build_query(url, ¶ms))
66 .send()
67 .await?
68 .json::<ATResponse>()
69 .await?;
70
71 let mut merged = vec![];
72 let entities: HashMap<_, _> = resp
73 .response
74 .entity
75 .into_iter()
76 .map(|e| (e.id.clone(), e))
77 .collect();
78
79 fn merge(ent: &Entity, hm: &HashMap<String, Entity>) -> Option<Entity> {
80 let trip_id = ent.vehicle.as_ref()?.trip.as_ref()?.trip_id.as_ref()?;
81 let tu_ent = hm.get(trip_id)?;
82 let mut entity = ent.clone();
83
84 if let Some(trip_update) = tu_ent.trip_update.as_ref() {
85 entity.trip_update = Some(trip_update.clone());
86 }
87
88 Some(entity)
89 }
90
91 for (_, ent) in entities.iter() {
92 if let Some(ent) = merge(ent, &entities) {
93 merged.push(ent);
94 }
95 }
96
97 Ok((resp.response.header, merged))
98 }
99
100 /// Creates a new Reqwest request builder with the given method and URL, with the
101 /// authentication header preset.
102 ///
103 /// # Parameters
104 ///
105 /// * `method` - The HTTP method to build the request with.
106 /// * `url` - The URL to send the request to.
107 fn request(&self, method: Method, url: String) -> reqwest::RequestBuilder {
108 self.client
109 .request(method, url)
110 .header("Ocp-Apim-Subscription-Key", self.api_key)
111 }
112
113 /// Builds a query string.
114 ///
115 /// This is used instead of `RequestBuilder::query` as the AT API requires commas to seperate
116 /// the vehicle and trip IDs, but reqwest escapes commas which AT does not support.
117 fn build_query(mut url: String, params: &[(&str, String)]) -> String {
118 let mut queries = vec![];
119 for (k, v) in params {
120 queries.push(format!("{}={}", k, v));
121 }
122
123 if !queries.is_empty() {
124 let queries = format!("?{}", queries.join("&"));
125 url += queries.as_str();
126 }
127
128 url
129 }
130}