italo_api/
lib.rs

1use std::collections::HashMap;
2
3use anyhow::{Context, Ok};
4
5use journey::InternalJourneyRequest;
6pub use journey::{
7    Journey, JourneyRequest, JourneyResults, JourneySegment, JourneysSolution, Stop,
8};
9use login::{LoginRequestBody, LoginResponse};
10use reqwest::Client;
11pub use station::{Station, StationRealtime, StationTrainRealtime};
12use station::{StationCode, StationLabel};
13pub use train::{Disruption, TrainRealtime, TrainSchedule, TrainStation};
14
15static LOGIN_ENDPOINT: &str = "https://big.ntvspa.it/BIG/v7/Rest/SessionManager.svc/Login";
16static STATION_LIST_ENDPOINT: &str = "https://italoinviaggio.italotreno.it/it/stazione";
17static STATION_REALTIME_ENDPOINT: &str =
18    "https://italoinviaggio.italotreno.it/api/RicercaStazioneService?&CodiceStazione=";
19static TRAIN_REALTIME_ENDPOINT: &str =
20    "https://italoinviaggio.italotreno.it/api/RicercaTrenoService?&TrainNumber=";
21
22static SEARCH_SOLUTIONS: &str =
23    "https://big.ntvspa.it/BIG/v7/Rest/BookingManager.svc/GetAvailableTrains";
24
25mod journey;
26mod login;
27mod station;
28mod train;
29
30/// Use this struct to access italotreno API.
31///
32/// Use [`Self::default()`] to instantiate the interface.
33///
34#[derive(Default)]
35pub struct ItaloApi {
36    signature: Option<LoginResponse>,
37    client: Client,
38}
39
40impl ItaloApi {
41    fn is_initialized(&self) -> bool {
42        self.signature.is_some()
43    }
44
45    async fn init(&mut self) -> anyhow::Result<()> {
46        self.signature = Some(
47            self.client
48                .post(LOGIN_ENDPOINT)
49                .json(&LoginRequestBody::default())
50                .send()
51                .await?
52                .json()
53                .await?,
54        );
55        Ok(())
56    }
57
58    /// Retrieves stations recognized by the italotreno information system.
59    ///
60    /// The struct contains internal Ids used by [`Self::station_realtime()`]
61    pub async fn station_list(&self) -> anyhow::Result<Vec<Station>> {
62        let res = self
63            .client
64            .get(STATION_LIST_ENDPOINT)
65            .send()
66            .await?
67            .text()
68            .await?;
69
70        let raw_lists = res
71            .split_once("ItaloInViaggio.Resources.stationList = ")
72            .context("stationList not found")?
73            .1
74            .split_once("ItaloInViaggio.Resources.stationCoding = ")
75            .context("stationCoding not found")?;
76
77        let label_list: Vec<StationLabel> =
78            serde_json::from_str(raw_lists.0.trim_end().trim_end_matches(';'))?;
79
80        let code_list: Vec<StationCode> = serde_json::from_str(
81            raw_lists
82                .1
83                .split_once("ItaloInViaggio.Resources.localizzation")
84                .context("localization not found")?
85                .0
86                .trim_end()
87                .trim_end_matches(';'),
88        )?;
89
90        let label_map = label_list
91            .iter()
92            .map(|elem| (elem.value(), elem.label()))
93            .collect::<HashMap<_, _>>();
94
95        Ok(code_list
96            .iter()
97            .map(|elem| {
98                Station::new(
99                    elem.code().to_owned(),
100                    elem.url_coding().to_owned(),
101                    label_map
102                        .get(elem.code())
103                        .unwrap_or(&&"".to_string())
104                        .to_string(),
105                )
106            })
107            .filter(|elem| !elem.name().is_empty())
108            .collect())
109    }
110
111    /// Retrieve the departure and arrival boards for a station using [`Self::station_realtime()`]
112    pub async fn station_realtime(&self, station: Station) -> anyhow::Result<StationRealtime> {
113        Ok(self
114            .client
115            .get(STATION_REALTIME_ENDPOINT.to_string() + station.code())
116            .send()
117            .await?
118            .json()
119            .await?)
120    }
121
122    /// Retrieve realtime data on a moving train
123    pub async fn train_realtime(&self, train_code: &str) -> anyhow::Result<TrainRealtime> {
124        Ok(self
125            .client
126            .get(TRAIN_REALTIME_ENDPOINT.to_string() + train_code)
127            .send()
128            .await?
129            .json()
130            .await?)
131    }
132
133    /// Search journey solutions between stations
134    pub async fn find_journeys(
135        &mut self,
136        journey: &JourneyRequest,
137    ) -> anyhow::Result<JourneyResults> {
138        match self.is_initialized() {
139            true => Ok(()),
140            false => self.init().await,
141        }?;
142
143        Ok(self
144            .client
145            .post(SEARCH_SOLUTIONS)
146            .json(&InternalJourneyRequest::new(
147                self.signature.as_deref().unwrap(),
148                2,
149                journey,
150            ))
151            .send()
152            .await?
153            .json()
154            .await?)
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use std::ops::Add;
161
162    use chrono::{Duration, Utc};
163
164    use super::*;
165
166    #[tokio::test]
167    async fn it_works() {
168        let mut api = ItaloApi::default();
169        assert!(!api.is_initialized());
170
171        assert!(api.init().await.is_ok());
172        assert!(api.is_initialized());
173
174        let stations = api.station_list().await;
175        println!("{:?}", stations);
176        println!();
177        assert!(stations.is_ok_and(|f| f.len() > 0));
178
179        let station_realtime = api
180            .station_realtime(Station::new(
181                "MC_".to_string(),
182                "milano-centrale".to_string(),
183                "Milano Centrale".to_string(),
184            ))
185            .await;
186        println!("{:?}", station_realtime);
187        println!();
188        assert!(station_realtime
189            .is_ok_and(|f| f.arrival_board().len() > 0 && f.departure_board().len() > 0));
190
191        let train_realtime = api.train_realtime("8158").await;
192        println!("{:?}", train_realtime);
193        println!();
194
195        let start_station = Station::new(
196            "NAC".to_string(),
197            "napoli-centrale".to_string(),
198            "Napoli Centrale".to_string(),
199        );
200        let end_station = Station::new(
201            "MC_".to_string(),
202            "milano-centrale".to_string(),
203            "Milano Centrale".to_string(),
204        );
205
206        let solutions = api
207            .find_journeys(
208                &&JourneyRequest::default()
209                    .set_departure_station(start_station)
210                    .set_arrival_station(end_station)
211                    .set_interval_start_date_time(Utc::now())
212                    .set_interval_end_date_time(Utc::now().add(Duration::hours(5)))
213                    .set_override_interval_time_restriction(true),
214            )
215            .await;
216
217        println!("{:?}", solutions);
218        println!();
219        assert!(solutions.is_ok());
220    }
221}