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 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}