ojp_rs/
requests.rs

1use std::fmt::Display;
2
3use chrono::{DateTime, Local, NaiveDateTime, SecondsFormat, Utc};
4use reqwest::Client;
5use secrecy::{ExposeSecret, SecretString};
6use thiserror::Error;
7use tracing::{Level, span};
8
9const URL: &str = "https://api.opentransportdata.swiss/ojp20";
10
11#[derive(Debug)]
12pub enum RequestType {
13    LocationInformation,
14    Trip,
15    StopEvent,
16    Unknown,
17}
18
19#[derive(Debug, Error)]
20pub enum RequestError {
21    #[error("Missing authetification token")]
22    MissingAuthToken,
23    #[error("Missing location name")]
24    MissingLocationName,
25    #[error("Missing from and to ids")]
26    MissingFromAndToId,
27    #[error("Missing from id")]
28    MissingFromId,
29    #[error("Missing to id")]
30    MissingToId,
31    #[error("Unknown request type: must be LocationInformation, Trip, or StopEvent")]
32    UnknownRequestType,
33    #[error("Events type is not implemented")]
34    EventsRequestTypeNotImplemented,
35    #[error("Invalid number of results, got {0}, should be > 0.")]
36    InvalidNumberResults(u32),
37    #[error("Http request error: {0}")]
38    ReqwestError(#[from] reqwest::Error),
39}
40
41impl TryFrom<RequestType> for String {
42    type Error = RequestError;
43    fn try_from(value: RequestType) -> Result<Self, Self::Error> {
44        match value {
45            RequestType::LocationInformation => Ok("OJPLocationInformationRequest".to_string()),
46            RequestType::Trip => Ok("OJPTripRequest".to_string()),
47            RequestType::StopEvent => Ok("OJPStopEventRequest".to_string()),
48            RequestType::Unknown => Err(RequestError::UnknownRequestType),
49        }
50    }
51}
52
53#[derive(Debug)]
54pub struct RequestBuilder {
55    token: Option<SecretString>,
56    date_time: DateTime<Utc>,
57    request_type: RequestType,
58    number_results: u32,
59    from: Option<i32>,
60    to: Option<i32>,
61    name: Option<String>,
62    requestor_ref: String,
63}
64
65impl RequestBuilder {
66    pub fn new(date_time: NaiveDateTime) -> Self {
67        // We convert NaiveDateTime to Utc through Local (for the offset)
68        // First we get the "now" local time (used for the offset)
69        // and add it to the NaiveDateTime
70        let date_time = date_time
71            .and_local_timezone(*Local::now().offset())
72            .unwrap();
73
74        let date_time = date_time.to_utc();
75        RequestBuilder {
76            date_time,
77            token: None,
78            request_type: RequestType::Unknown,
79            number_results: 0,
80            from: None,
81            to: None,
82            name: None,
83            requestor_ref: String::new(),
84        }
85    }
86
87    pub fn set_from(mut self, from: i32) -> Self {
88        self.from = Some(from);
89        self
90    }
91
92    pub fn set_to(mut self, to: i32) -> Self {
93        self.to = Some(to);
94        self
95    }
96
97    pub fn set_token(mut self, token: SecretString) -> Self {
98        self.token = Some(token);
99        self
100    }
101
102    pub fn set_request_type(mut self, request_type: RequestType) -> Self {
103        self.request_type = request_type;
104        self
105    }
106
107    pub fn set_number_results(mut self, number_results: u32) -> Self {
108        self.number_results = number_results;
109        self
110    }
111
112    pub fn set_name(mut self, name: &str) -> Self {
113        self.name = Some(name.to_string());
114        self
115    }
116
117    pub fn set_requestor_ref(mut self, requestor_ref: &str) -> Self {
118        self.requestor_ref = requestor_ref.to_string();
119        self
120    }
121
122    pub fn try_request_body(&self) -> Result<String, RequestError> {
123        let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
124        let date_time = self.date_time.to_rfc3339_opts(SecondsFormat::Millis, true);
125
126        let number_results = self.number_results;
127        match self.request_type {
128            RequestType::Unknown => Err(RequestError::UnknownRequestType),
129            RequestType::LocationInformation => {
130                if number_results == 0 {
131                    return Err(RequestError::InvalidNumberResults(number_results));
132                }
133                if self.name.is_none() {
134                    return Err(RequestError::MissingLocationName);
135                }
136                let req = format!(
137"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
138                            <OJP xmlns=\"http://www.vdv.de/ojp\" xmlns:siri=\"http://www.siri.org.uk/siri\" version=\"2.0\">
139                             	<OJPRequest>
140                                    <siri:ServiceRequest>
141                                        <siri:RequestTimestamp>{now}</siri:RequestTimestamp>
142                                        <siri:RequestorRef>{}</siri:RequestorRef>
143                                        <OJPLocationInformationRequest>
144                                        <siri:RequestTimestamp>{now}</siri:RequestTimestamp>
145                                        <siri:MessageIdentifier>LIR-1a</siri:MessageIdentifier>
146                                        <InitialInput>
147                                            <Name>{}</Name>
148                                        </InitialInput>
149                                        <Restrictions>
150                                            <Type>stop</Type>
151                                            <NumberOfResults>{number_results}</NumberOfResults>
152                                        </Restrictions>
153                                    </OJPLocationInformationRequest>
154                                    </siri:ServiceRequest>
155                                </OJPRequest>
156                            </OJP>", self.requestor_ref, self.name.as_ref().unwrap());
157                Ok(req)
158            }
159            RequestType::StopEvent => Err(RequestError::EventsRequestTypeNotImplemented),
160            RequestType::Trip => {
161                if number_results == 0 {
162                    return Err(RequestError::InvalidNumberResults(number_results));
163                }
164                let (from, to) = match (self.from, self.to) {
165                    (Some(from), Some(to)) => (from, to),
166                    (None, None) => return Err(RequestError::MissingFromAndToId),
167                    (Some(_), None) => return Err(RequestError::MissingToId),
168                    (None, Some(_)) => return Err(RequestError::MissingFromId),
169                };
170                let req = format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>
171                            <OJP xmlns=\"http://www.vdv.de/ojp\" xmlns:siri=\"http://www.siri.org.uk/siri\" version=\"2.0\">
172                             	<OJPRequest>
173                                    <siri:ServiceRequest>
174                                        <siri:RequestTimestamp>{now}</siri:RequestTimestamp>
175                                        <siri:RequestorRef>{}</siri:RequestorRef>
176                                        <OJPTripRequest>
177                                            <siri:RequestTimestamp>{now}</siri:RequestTimestamp>
178                                            <siri:MessageIdentifier>TR-1r1</siri:MessageIdentifier>
179                                            <Origin>
180                                                <PlaceRef>
181                                                    <siri:StopPointRef>{from}</siri:StopPointRef>
182                                                </PlaceRef>
183                                                <DepArrTime>{date_time}</DepArrTime>
184                                            </Origin>
185                                            <Destination>
186                                                <PlaceRef>
187                                                    <siri:StopPointRef>{to}</siri:StopPointRef>
188                                                </PlaceRef>
189                                            </Destination>
190                                            <Params>
191                                                <NumberOfResults>{number_results}</NumberOfResults>
192                                            </Params>
193                                        </OJPTripRequest>
194                                    </siri:ServiceRequest>
195                                </OJPRequest>
196                            </OJP>", self.requestor_ref);
197                Ok(req)
198            }
199        }
200    }
201
202    pub fn build_request(self) -> Result<reqwest::RequestBuilder, RequestError> {
203        let id_request = self.try_request_body()?;
204
205        if self.token.is_none() {
206            return Err(RequestError::MissingAuthToken);
207        }
208        {
209            let span = span!(Level::INFO, "Performing OJP request");
210            let _guard = span.enter();
211            tracing::info!("{}", self);
212        }
213        let token = self.token.ok_or(RequestError::MissingAuthToken)?;
214
215        let req = Client::new()
216            .post(URL)
217            .header("Content-Type", "application/xml")
218            .header("accept", "*/*")
219            .bearer_auth(token.expose_secret())
220            .body(id_request);
221
222        Ok(req)
223    }
224
225    pub async fn send_request(self) -> Result<String, RequestError> {
226        let respone = self.build_request()?.send().await?.text().await?;
227        Ok(respone)
228    }
229}
230
231impl Display for RequestBuilder {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        let token = self
234            .token
235            .as_ref()
236            .map(|s| format!("{:?}", s))
237            .unwrap_or("Undefined".to_string());
238
239        match self.request_type {
240            RequestType::LocationInformation => {
241                write!(f, "Location Information Request: ")?;
242                write!(
243                    f,
244                    "Location: {}, ",
245                    self.name
246                        .as_ref()
247                        .map(|i| i.to_string())
248                        .unwrap_or("Undefined".to_string()),
249                )?;
250            }
251            RequestType::Trip => {
252                write!(f, "Trip Request: ")?;
253                write!(
254                    f,
255                    "From: {}, To: {}, ",
256                    self.from
257                        .map(|i| format!("{i}"))
258                        .unwrap_or("Undefined".to_string()),
259                    self.to
260                        .map(|i| format!("{i}"))
261                        .unwrap_or("Undefined".to_string()),
262                )?;
263            }
264            RequestType::StopEvent => {
265                write!(f, "Stop Envent Request Not Implement. ")?;
266            }
267            RequestType::Unknown => {
268                write!(f, "RequestType is unknown. ")?;
269            }
270        }
271        write!(
272            f,
273            "NumberResults: {}, DateTime: {}, RequestorRef: {}, Token: {token}",
274            self.number_results, self.date_time, self.requestor_ref
275        )
276    }
277}