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, &params))
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}