1#![allow(dead_code)]
2use std::fmt::Display;
3use std::num::ParseIntError;
4use std::{env::VarError, io::Write};
5
6use chrono::{DateTime, Duration, NaiveDateTime, TimeDelta, Utc};
7use futures::future::join_all;
8use quick_xml::DeError;
9use secrecy::SecretString;
10use serde::Deserialize;
11use thiserror::Error;
12use tracing::{Level, span};
13
14use crate::{RequestBuilder, RequestType, requests::RequestError};
15
16pub fn token(api_key: &str) -> Result<SecretString, OjpError> {
17 let t = std::env::var(api_key)?;
18 Ok(SecretString::new(t.into()))
19}
20
21fn iso_to_uic(iso: &str) -> Option<i32> {
22 match iso.to_lowercase().as_str() {
23 "fi" => Some(10),
24 "ru" => Some(20),
25 "by" => Some(21),
26 "ua" => Some(22),
27 "md" => Some(23),
28 "lt" => Some(24),
29 "lv" => Some(25),
30 "ee" => Some(26),
31 "kz" => Some(27),
32 "ge" => Some(28),
33 "uz" => Some(29),
34 "kp" => Some(30),
35 "mn" => Some(31),
36 "nn" => Some(32),
37 "cn" => Some(33),
38 "la" => Some(34),
39 "cu" => Some(40),
40 "al" => Some(41),
41 "jp" => Some(42),
42 "ba" => Some(44),
43 "pl" => Some(51),
44 "bg" => Some(52),
45 "ro" => Some(53),
46 "cz" => Some(54),
47 "hu" => Some(55),
48 "sk" => Some(56),
49 "az" => Some(57),
50 "am" => Some(58),
51 "kg" => Some(59),
52 "ie" => Some(60),
53 "kr" => Some(61),
54 "me" => Some(62),
55 "mk" => Some(65),
56 "tj" => Some(66),
57 "tm" => Some(67),
58 "af" => Some(68),
59 "gb" => Some(70),
60 "es" => Some(71),
61 "rs" => Some(72),
62 "gr" => Some(73),
63 "se" => Some(74),
64 "tr" => Some(75),
65 "no" => Some(76),
66 "hr" => Some(78),
67 "si" => Some(79),
68 "de" => Some(80),
69 "at" => Some(81),
70 "lu" => Some(82),
71 "it" => Some(83),
72 "nl" => Some(84),
73 "ch" => Some(85),
74 "dk" => Some(86),
75 "fr" => Some(87),
76 "be" => Some(88),
77 "tz" => Some(89),
78 "eg" => Some(90),
79 "tn" => Some(91),
80 "dz" => Some(92),
81 "ma" => Some(93),
82 "pt" => Some(94),
83 "il" => Some(95),
84 "ir" => Some(96),
85 "sy" => Some(97),
86 "lb" => Some(98),
87 "iq" => Some(99),
88 _ => None,
89 }
90}
91
92fn sloid_to_didok(sloid: &str) -> Result<i32, OjpError> {
93 let parts: Vec<&str> = sloid.split(':').collect();
95 if parts.len() < 4 {
96 return Err(OjpError::MalformedSloid(sloid.to_string())); }
98
99 let iso = parts[0].to_lowercase();
100 let uic = iso_to_uic(iso.as_str()).ok_or_else(|| OjpError::FailedToConvertIsoCode(iso))?;
101
102 let num_str = parts[3];
104 let num = num_str.parse::<i32>()?;
105
106 let didok = format!("{}{:05}", uic, num);
107 let didok = didok.parse::<i32>()?;
108
109 Ok(didok)
110}
111mod duration {
112 use chrono::Duration;
113 use serde::Deserialize;
114 use serde::de::{self, Deserializer};
115 use std::str::FromStr;
116
117 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
118 where
119 D: Deserializer<'de>,
120 {
121 let s = String::deserialize(deserializer)?;
122
123 let (sign, s) = if let Some(s) = s.strip_prefix("PT") {
124 (1, s)
125 } else if let Some(s) = s.strip_prefix("-PT") {
126 (-1, s)
127 } else {
128 return Err(de::Error::custom(format!(
129 "duration does not start with PT or -PT, but is {s}"
130 )));
131 };
132 let mut total_seconds = 0;
135 let mut current_number_str = String::new();
136
137 for c in s.chars() {
138 if c.is_ascii_digit() {
139 current_number_str.push(c);
140 } else {
141 if current_number_str.is_empty() {
142 return Err(de::Error::custom(format!(
143 "Expected a number before unit '{}'",
144 c
145 )));
146 }
147 let value = i64::from_str(¤t_number_str).map_err(de::Error::custom)?;
148 match c {
149 'H' => total_seconds += sign * value * 3600,
150 'M' => total_seconds += sign * value * 60,
151 'S' => total_seconds += sign * value,
152 _ => return Err(de::Error::custom(format!("Invalid duration unit: {}", c))),
153 }
154 current_number_str.clear();
155 }
156 }
157
158 if !current_number_str.is_empty() {
159 return Err(de::Error::custom(
160 "Duration string ends with a number but no unit",
161 ));
162 }
163
164 Ok(Duration::seconds(total_seconds))
165 }
166
167 #[cfg(test)]
170 mod tests {
171 use super::*; use chrono::Duration;
173 use quick_xml::DeError;
174 use serde::de::IntoDeserializer;
175
176 fn parse(s: &str) -> Result<Duration, DeError> {
177 let de = s.into_deserializer();
178 deserialize(de)
179 }
180
181 #[test]
182 fn parses_minutes_only() {
183 let d = parse("PT20M").unwrap();
184 assert_eq!(d, Duration::seconds(20 * 60));
185 }
186
187 #[test]
188 fn parses_hours_and_minutes() {
189 let d = parse("PT5H30M").unwrap();
190 assert_eq!(d, Duration::seconds(5 * 3600 + 30 * 60));
191 }
192
193 #[test]
194 fn parses_hours_and_minutes_and_seconds() {
195 let d = parse("PT5H30M05S").unwrap();
196 assert_eq!(d, Duration::seconds(5 * 3600 + 30 * 60 + 5));
197 }
198
199 #[test]
200 fn parses_seconds_with_fraction() {
201 let d = parse("PT1M30S").unwrap();
202 let expected = Duration::seconds(60 + 30);
203 assert_eq!(d, expected);
204 }
205
206 #[test]
207 fn zero_duration_is_valid() {
208 let d = parse("PT0S").unwrap();
209 assert_eq!(d, Duration::seconds(0));
210 }
211
212 #[test]
213 #[should_panic]
214 fn rejects_invalid_lexical_form() {
215 let _ = parse("P1D2H").unwrap();
216 }
217
218 #[test]
219 fn negative_duration_accepted() {
220 let d = parse("-PT10S").unwrap();
221 assert_eq!(d, Duration::seconds(-10));
222 }
223 }
224}
225
226#[derive(Debug, Error)]
227pub enum OjpError {
228 #[error("Failed to parse XML {0}")]
229 FailedToParseXml(DeError, String),
230 #[error("Failed to find trip from id: {dep_id}, to id: {arr_id}. Optional message: {msg}")]
231 FailedToFindTrip {
232 dep_id: i32,
233 arr_id: i32,
234 msg: String,
235 },
236 #[error("Unkown LegType")]
237 UnkownLegType,
238 #[error("Failed to parse {0}")]
239 ParseInt(#[from] ParseIntError),
240 #[error("Failed to convert to Simplified trip")]
241 FailedToConvertToSimplifiedTrip,
242 #[error("Failed to get API token: {0}")]
243 UnableToGetApiToken(#[from] VarError),
244 #[error("Request building error: {0}")]
245 RequestBuilderError(#[from] RequestError),
246 #[error("No place results found")]
247 PlaceResultsNotFound,
248 #[error("Malformed sloid: {0}")]
249 MalformedSloid(String),
250 #[error("Failed to convert ISO code to UIC: {0}")]
251 FailedToConvertIsoCode(String),
252}
253
254#[derive(Deserialize, Debug)]
255pub struct OJP {
256 #[serde(rename = "OJPResponse")]
257 ojp_response: OJPResponse,
258}
259
260impl OJP {
261 pub async fn find_location(
262 location: &str,
263 date_time: NaiveDateTime,
264 number_results: u32,
265 requestor_ref: &str,
266 api_key: &str,
267 ) -> Result<Vec<i32>, OjpError> {
268 let response = RequestBuilder::new(date_time)
269 .set_token(token(api_key)?)
270 .set_name(location)
271 .set_number_results(number_results)
272 .set_request_type(RequestType::LocationInformation)
273 .set_requestor_ref(requestor_ref)
274 .send_request()
275 .await?;
276
277 let ojp = OJP::try_from(response.as_str())?;
278 let place_result = ojp
279 .place_results()
280 .ok_or(OjpError::PlaceResultsNotFound)?
281 .into_iter()
282 .filter_map(|pr| pr.stop_place_ref())
283 .collect::<Vec<_>>();
284 Ok::<Vec<i32>, OjpError>(place_result)
285 }
286 pub async fn find_locations(
289 locations: &[&str],
290 date_time: NaiveDateTime,
291 number_results: u32,
292 requestor_ref: &str,
293 api_key: &str,
294 ) -> Result<Vec<i32>, OjpError> {
295 let point_ref = locations
296 .iter()
297 .map(|&tc| async move {
298 Self::find_location(tc, date_time, number_results, requestor_ref, api_key).await
299 })
300 .collect::<Vec<_>>();
301 join_all(point_ref)
302 .await
303 .into_iter()
304 .collect::<Result<_, _>>()
305 .map(|v: Vec<_>| v.into_iter().flatten().collect())
306 }
307
308 pub async fn find_trips(
312 departures: &[i32],
313 arrivals: &[i32],
314 date_time: NaiveDateTime,
315 number_results: u32,
316 requestor_ref: &str,
317 api_key: &str,
318 ) -> Vec<Result<SimplifiedTrip, OjpError>> {
319 let ref_trips: Vec<_> = departures
320 .iter()
321 .zip(arrivals.iter())
322 .map(|(&from_id, &to_id)| async move {
323 Self::find_trip(
324 from_id,
325 to_id,
326 date_time,
327 number_results,
328 requestor_ref,
329 api_key,
330 )
331 .await
332 })
333 .collect();
334 join_all(ref_trips).await
335 }
336
337 pub async fn find_trip(
340 from_id: i32,
341 to_id: i32,
342 date_time: NaiveDateTime,
343 number_results: u32,
344 requestor_ref: &str,
345 api_key: &str,
346 ) -> Result<SimplifiedTrip, OjpError> {
347 let response = RequestBuilder::new(date_time)
348 .set_token(token(api_key)?)
349 .set_from(from_id)
350 .set_to(to_id)
351 .set_number_results(number_results)
352 .set_request_type(RequestType::Trip)
353 .set_requestor_ref(requestor_ref)
354 .send_request()
355 .await?;
356
357 let ojp = OJP::try_from(response.as_str()).inspect_err(|e| {
358 let span = span!(Level::WARN, "From response error");
359 let _guard = span.enter();
360 tracing::error!("{e}");
361 let mut file = std::fs::File::create("debug.xml").unwrap();
362 file.write_all(response.as_bytes()).unwrap();
363 })?;
364 let ojp = if let Some(msg) = ojp.error() {
365 Err(OjpError::FailedToFindTrip {
366 dep_id: from_id,
367 arr_id: to_id,
368 msg: msg.to_string(),
369 })
370 } else {
371 Ok(ojp)
372 }?;
373
374 let ref_trip =
375 ojp.trip_departing_after(date_time, 0)
376 .ok_or(OjpError::FailedToFindTrip {
377 dep_id: from_id,
378 arr_id: to_id,
379 msg: format!("No trip departig after {date_time} was found."),
380 })?;
381
382 SimplifiedTrip::try_from(ref_trip).inspect_err(|e| {
383 let span = span!(Level::WARN, "From ref_trip error");
384 let _guard = span.enter();
385 tracing::error!("{e}");
386 let mut file = std::fs::File::create("debug_simplified.xml").unwrap();
387 file.write_all(response.as_bytes()).unwrap();
388 })
389 }
390
391 pub fn trips(&self) -> Option<Vec<&TripResult>> {
393 Some(
394 self.ojp_response
395 .service_delivery
396 .ojp_trip_delivery
397 .as_ref()?
398 .trip_results
399 .iter()
400 .collect(),
401 )
402 }
403
404 pub fn fastest_trip(&self) -> Option<&Trip> {
405 let mut trips = self.trips()?;
406 trips.sort_by_key(|t| t.trip.duration);
407 trips.first().map(|t| &t.trip)
408 }
409
410 pub fn place_results(&self) -> Option<Vec<&PlaceResult>> {
412 Some(
413 self.ojp_response
414 .service_delivery
415 .ojp_location_information_delivery
416 .as_ref()?
417 .place_results
418 .iter()
419 .collect(),
420 )
421 }
422
423 pub fn trips_departing_after(&self, date_time: NaiveDateTime) -> Option<Vec<&TripResult>> {
425 let res = self
426 .trips()?
427 .into_iter()
428 .filter(|&t| t.trip.start_time.naive_utc() >= date_time)
429 .collect::<Vec<_>>();
430 if res.is_empty() { None } else { Some(res) }
431 }
432
433 pub fn fastest_trip_departing_after(&self, date_time: NaiveDateTime) -> Option<&Trip> {
434 let mut trips = self.trips_departing_after(date_time)?;
435 trips.sort_by_key(|t| t.trip.duration);
436 trips.first().map(|t| &t.trip)
437 }
438
439 pub fn trip_departing_after(&self, date_time: NaiveDateTime, index: usize) -> Option<&Trip> {
441 Some(
442 &self
443 .trips_departing_after(date_time)?
444 .get(index)
445 .copied()?
446 .trip,
447 )
448 }
449
450 pub fn trip(&self, index: usize) -> Option<&Trip> {
452 Some(&self.trips()?.get(index).copied()?.trip)
453 }
454
455 pub fn error(&self) -> Option<&str> {
458 Some(
459 self.ojp_response
460 .service_delivery
461 .ojp_trip_delivery
462 .as_ref()?
463 .error_condition
464 .as_ref()?
465 .trip_problem_type
466 .as_str(),
467 )
468 }
469}
470
471impl TryFrom<&str> for OJP {
472 type Error = OjpError;
473 fn try_from(value: &str) -> Result<Self, Self::Error> {
474 quick_xml::de::from_str(value).map_err(|e| OjpError::FailedToParseXml(e, value.to_string()))
475 }
476}
477
478#[derive(Deserialize, Debug)]
479#[serde(rename_all = "PascalCase")]
480struct OJPResponse {
481 service_delivery: ServiceDelivery,
482}
483
484#[derive(Deserialize, Debug)]
485#[serde(rename_all = "PascalCase")]
486struct ServiceDelivery {
487 response_timestamp: DateTime<Utc>,
488 producer_ref: String,
489 #[serde(rename = "OJPTripDelivery")]
490 ojp_trip_delivery: Option<OJPTripDelivery>,
491 #[serde(rename = "OJPLocationInformationDelivery")]
492 ojp_location_information_delivery: Option<OJPLocationInformationDelivery>,
493 #[serde(rename = "OJPStopEventDelivery")]
494 ojp_stop_event_delivery: Option<OJPStopEventDelivery>,
495}
496
497#[derive(Deserialize, Debug)]
498#[serde(rename_all = "PascalCase")]
499struct TripResponseContext {
500 places: Option<Places>,
501 situations: Option<Situations>,
502}
503
504#[derive(Deserialize, Debug)]
505#[serde(rename_all = "PascalCase")]
506struct Situations {
507 #[serde(default)]
508 pt_situations: Vec<PtSituation>,
509}
510
511#[derive(Deserialize, Debug)]
512#[serde(rename_all = "PascalCase")]
513struct PtSituation {
514 creation_time: DateTime<Utc>,
515 participation_ref: String,
516 situation_number: String,
517 version: i32,
518 source: Source,
519 validity_period: ValidityPeriod,
520 alert_cause: String,
521 priority: i32,
522 scope_type: String,
523 language: String,
524 #[serde(default)]
525 publishing_actions: Vec<PublishingAction>,
526}
527
528#[derive(Deserialize, Debug)]
529#[serde(rename_all = "PascalCase")]
530struct PublishingAction {
531 publish_at_scope: Option<PublishAtScope>,
534 passenger_sinformation_action: Option<PassengerInformationAction>,
535}
536
537#[derive(Deserialize, Debug)]
538#[serde(rename_all = "PascalCase")]
539struct PassengerInformationAction {
540 #[serde(default)]
541 action_ref: String,
542 recorded_at_time: DateTime<Utc>,
543 perspective: String,
544 textual_content: TextualContent,
545}
546
547#[derive(Deserialize, Debug)]
548#[serde(rename_all = "PascalCase")]
549struct TextualContent {
550 summary_content: SummaryText,
551 reason_content: ReasonText,
552 duration_content: DurationText,
553}
554
555#[derive(Deserialize, Debug)]
556#[serde(rename_all = "PascalCase")]
557struct SummaryText {
558 summary_text: String,
559}
560
561#[derive(Deserialize, Debug)]
562#[serde(rename_all = "PascalCase")]
563struct ReasonText {
564 reason_text: String,
565}
566
567#[derive(Deserialize, Debug)]
568#[serde(rename_all = "PascalCase")]
569struct DurationText {
570 duration_text: String,
571}
572
573#[derive(Deserialize, Debug)]
574#[serde(rename_all = "PascalCase")]
575struct PublishAtScope {
576 scope_type: String,
577 #[serde(default)]
578 affects: String,
579}
580
581#[derive(Deserialize, Debug)]
582#[serde(rename_all = "PascalCase")]
583struct ValidityPeriod {
584 start_time: DateTime<Utc>,
585 end_time: DateTime<Utc>,
586}
587
588#[derive(Deserialize, Debug)]
589#[serde(rename_all = "PascalCase")]
590struct Source {
591 #[serde(default)]
592 source_type: String,
593}
594
595#[derive(Deserialize, Debug)]
596struct Places {
597 #[serde(rename = "Place", default)]
598 places: Vec<Place>,
599}
600
601#[derive(Deserialize, Debug)]
602#[serde(rename_all = "PascalCase")]
603struct OJPTripDelivery {
604 trip_response_context: Option<TripResponseContext>,
605 #[serde(rename = "TripResult", default)]
606 trip_results: Vec<TripResult>,
607 error_condition: Option<ErrorCondition>,
608 response_timestamp: DateTime<Utc>,
609 request_message_ref: String,
610 default_language: String,
611}
612
613#[derive(Deserialize, Debug)]
614#[serde(rename_all = "PascalCase")]
615struct ErrorCondition {
616 trip_problem_type: String,
617}
618
619#[derive(Deserialize, Debug)]
620#[serde(rename_all = "PascalCase")]
621pub struct TripResult {
622 id: String,
623 trip: Trip,
624}
625
626impl TripResult {
627 pub fn trip(&self) -> &Trip {
628 &self.trip
629 }
630}
631
632#[derive(Deserialize, Debug)]
633#[serde(rename_all = "PascalCase")]
634pub struct Trip {
635 id: String,
636 #[serde(with = "duration")]
637 duration: Duration,
638 start_time: DateTime<Utc>,
639 end_time: DateTime<Utc>,
640 transfers: u32,
641 distance: Option<u32>,
642 #[serde(rename = "Leg", default)]
643 legs: Vec<Leg>,
644}
645
646impl Trip {
647 pub fn legs(&self) -> Vec<&Leg> {
648 self.legs.iter().collect()
649 }
650
651 pub fn departure_time(&self) -> NaiveDateTime {
652 self.start_time.naive_utc()
653 }
654
655 pub fn arrival_time_time(&self) -> NaiveDateTime {
656 self.end_time.naive_utc()
657 }
658
659 pub fn duration(&self) -> TimeDelta {
660 self.duration
661 }
662
663 pub fn trip_info(&self) -> TripInfo {
664 TripInfo {
665 departure_time: self.start_time.naive_utc(),
666 arrival_time: self.end_time.naive_utc(),
667 duration: self.duration,
668 }
669 }
670}
671
672#[derive(Debug, Clone, Copy, PartialEq)]
674pub struct TripInfo {
675 departure_time: NaiveDateTime,
676 arrival_time: NaiveDateTime,
677 duration: Duration,
678}
679
680#[derive(Debug, Clone)]
681pub struct SimplifiedLeg {
682 departure_id: i32,
683 departure_stop: String,
684 arrival_id: i32,
685 arrival_stop: String,
686 departure_time: NaiveDateTime,
687 arrival_time: NaiveDateTime,
688 mode: String,
689}
690
691impl SimplifiedLeg {
692 pub fn new(
693 departure_id: i32,
694 departure_stop: &str,
695 arrival_id: i32,
696 arrival_stop: &str,
697 departure_time: NaiveDateTime,
698 arrival_time: NaiveDateTime,
699 mode: String,
700 ) -> Self {
701 SimplifiedLeg {
702 departure_id,
703 departure_stop: departure_stop.to_string(),
704 arrival_id,
705 arrival_stop: arrival_stop.to_string(),
706 departure_time,
707 arrival_time,
708 mode,
709 }
710 }
711}
712
713#[derive(Debug, Clone)]
714pub struct SimplifiedTrip {
715 legs: Vec<SimplifiedLeg>,
716}
717
718impl Display for SimplifiedTrip {
719 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
720 writeln!(
721 f,
722 "Trip from: {} to: {} departing at: {}",
723 self.departure_stop(),
724 self.arrival_stop(),
725 self.departure_time()
726 )?;
727 self.legs().iter().try_for_each(|l| {
728 writeln!(
729 f,
730 "[{:<8}]: {:<40} -> {:<40}, {} - {}",
731 l.mode,
732 l.departure_stop,
733 l.arrival_stop,
734 l.departure_time.format("%H:%M"),
735 l.arrival_time.format("%H:%M")
736 )
737 })
738 }
739}
740
741impl SimplifiedTrip {
742 pub fn new(legs: Vec<SimplifiedLeg>) -> Self {
743 SimplifiedTrip { legs }
744 }
745 pub fn legs(&self) -> Vec<&SimplifiedLeg> {
746 self.legs.iter().collect()
747 }
748
749 pub fn departure_time(&self) -> NaiveDateTime {
750 self.legs().first().map(|l| l.departure_time).unwrap()
751 }
752
753 pub fn arrival_time(&self) -> NaiveDateTime {
754 self.legs().last().map(|l| l.arrival_time).unwrap()
755 }
756
757 pub fn duration(&self) -> TimeDelta {
758 self.arrival_time() - self.departure_time()
759 }
760
761 pub fn departure_id(&self) -> i32 {
762 self.legs().first().map(|l| l.departure_id).unwrap()
763 }
764
765 pub fn arrival_id(&self) -> i32 {
766 self.legs().last().map(|l| l.arrival_id).unwrap()
767 }
768
769 pub fn departure_stop(&self) -> &str {
770 self.legs()
771 .first()
772 .map(|l| l.departure_stop.as_str())
773 .unwrap()
774 }
775
776 pub fn arrival_stop(&self) -> &str {
777 self.legs().last().map(|l| l.arrival_stop.as_str()).unwrap()
778 }
779
780 pub fn approx_equal(&self, rhs: &SimplifiedTrip, tolerance: f64) -> bool {
781 if self.departure_id() != rhs.departure_id() || self.arrival_id() != rhs.arrival_id() {
783 return false;
784 }
785 if (self.duration().as_seconds_f64() - rhs.duration().as_seconds_f64()).abs()
787 / rhs.duration().as_seconds_f64()
788 > tolerance
789 {
790 return false;
791 }
792
793 if (self.departure_time() - rhs.departure_time()).as_seconds_f64()
795 / self.duration().as_seconds_f64()
796 > tolerance
797 || (self.arrival_time() - rhs.arrival_time()).as_seconds_f64()
798 / self.duration().as_seconds_f64()
799 > tolerance
800 {
801 return false;
802 }
803 true
804 }
805}
806
807impl TryFrom<&Trip> for SimplifiedTrip {
808 type Error = OjpError;
809 fn try_from(value: &Trip) -> Result<Self, Self::Error> {
810 let mut prev_arr_time = value.start_time.naive_utc();
811 let st: Vec<_> = value
812 .legs()
813 .into_iter()
814 .map(|leg| {
815 let typed_leg = LegType::try_from(leg)?;
816 let departure_id = typed_leg.departure_id()?;
817 let departure_stop = typed_leg.departure_stop();
818 let arrival_id = typed_leg.arrival_id()?;
819 let arrival_stop = typed_leg.arrival_stop();
820 let departure_time = typed_leg.departure_time().unwrap_or(prev_arr_time);
821 let arrival_time = typed_leg
822 .arrival_time()
823 .unwrap_or(prev_arr_time + typed_leg.duration());
824 prev_arr_time = arrival_time;
825 Ok(SimplifiedLeg::new(
826 departure_id,
827 departure_stop,
828 arrival_id,
829 arrival_stop,
830 departure_time,
831 arrival_time,
832 typed_leg.mode().to_string(),
833 ))
834 })
835 .collect::<Result<Vec<_>, OjpError>>()?;
836 Ok(SimplifiedTrip { legs: st })
837 }
838}
839
840pub enum LegType<'a> {
841 Timed(&'a TimedLeg),
842 Transfer(&'a TransferLeg),
843 Continuous(&'a ContinuousLeg),
844}
845
846impl<'a> LegType<'a> {
847 pub fn duration(&'a self) -> TimeDelta {
848 match *self {
849 Self::Timed(tl) => tl.arrival_time() - tl.departure_time(),
850 Self::Transfer(t) => t.duration,
851 Self::Continuous(t) => t.duration,
852 }
853 }
854
855 pub fn departure_time(&'a self) -> Option<NaiveDateTime> {
856 match *self {
857 Self::Timed(tl) => Some(tl.departure_time().naive_utc()),
858 Self::Transfer(_) => None,
859 Self::Continuous(_) => None,
860 }
861 }
862
863 pub fn arrival_time(&'a self) -> Option<NaiveDateTime> {
864 match *self {
865 Self::Timed(tl) => Some(tl.arrival_time().naive_utc()),
866 Self::Transfer(_) => None,
867 Self::Continuous(_) => None,
868 }
869 }
870
871 pub fn departure_stop(&'a self) -> &'a str {
872 match *self {
873 Self::Timed(tl) => tl.departure_stop(),
874 Self::Transfer(t) => t.departure_stop(),
875 Self::Continuous(t) => t.departure_stop(),
876 }
877 }
878
879 pub fn arrival_stop(&'a self) -> &'a str {
880 match *self {
881 Self::Timed(tl) => tl.arrival_stop(),
882 Self::Transfer(t) => t.arrival_stop(),
883 Self::Continuous(t) => t.arrival_stop(),
884 }
885 }
886
887 pub fn departure_id(&'a self) -> Result<i32, OjpError> {
888 match *self {
889 Self::Timed(tl) => tl.departure_id(),
890 Self::Transfer(t) => t.departure_id(),
891 Self::Continuous(t) => t.departure_id(),
892 }
893 }
894
895 pub fn arrival_id(&'a self) -> Result<i32, OjpError> {
896 match *self {
897 Self::Timed(tl) => tl.arrival_id(),
898 Self::Transfer(t) => t.arrival_id(),
899 Self::Continuous(t) => t.arrival_id(),
900 }
901 }
902
903 pub fn mode(&self) -> &str {
904 match *self {
905 Self::Timed(tl) => tl.service.mode.name(),
906 Self::Transfer(t) => t.transfer_type.as_str(),
907 Self::Continuous(t) => t.service.personal_mode.as_str(),
908 }
909 }
910}
911
912impl<'a> TryFrom<&'a Leg> for LegType<'a> {
913 type Error = OjpError;
914 fn try_from(value: &'a Leg) -> Result<Self, Self::Error> {
915 if let Some(l) = value.timed_leg.as_ref() {
916 Ok(LegType::Timed(l))
917 } else if let Some(l) = value.transfer_leg.as_ref() {
918 Ok(LegType::Transfer(l))
919 } else if let Some(l) = value.continuous_leg.as_ref() {
920 Ok(LegType::Continuous(l))
921 } else {
922 Err(OjpError::UnkownLegType)
923 }
924 }
925}
926
927#[derive(Deserialize, Debug)]
928#[serde(rename_all = "PascalCase")]
929pub struct Leg {
930 id: u32,
931 #[serde(with = "duration")]
932 duration: Duration,
933 timed_leg: Option<TimedLeg>,
934 transfer_leg: Option<TransferLeg>,
935 continuous_leg: Option<ContinuousLeg>,
936 #[serde(rename = "EmissionCO2")]
937 emission_co2: Option<EmissionCO2>,
938}
939
940#[derive(Deserialize, Debug)]
941#[serde(rename_all = "PascalCase")]
942pub struct ContinuousLeg {
943 leg_start: LegEndpoint,
944 leg_end: LegEndpoint,
945 service: ContinuousService,
946 #[serde(with = "duration")]
947 duration: Duration,
948 length: i32,
949 leg_track: LegTrack,
950 path_guidance: PathGuidance,
951}
952
953impl ContinuousLeg {
954 pub fn departure_stop(&self) -> &str {
955 self.leg_start.name()
956 }
957
958 pub fn arrival_stop(&self) -> &str {
959 self.leg_end.name()
960 }
961
962 pub fn departure_id(&self) -> Result<i32, OjpError> {
963 self.leg_start.id()
964 }
965
966 pub fn arrival_id(&self) -> Result<i32, OjpError> {
967 self.leg_end.id()
968 }
969}
970
971#[derive(Deserialize, Debug)]
972#[serde(rename_all = "PascalCase")]
973struct ContinuousService {
974 personal_mode_of_operation: String,
975 personal_mode: String,
976}
977
978#[derive(Deserialize, Debug)]
979#[serde(rename_all = "PascalCase")]
980struct PathGuidance {
981 #[serde(rename = "PathGuidanceSection", default)]
982 path_guidance_sections: Vec<PathGuidanceSection>,
983}
984
985#[derive(Deserialize, Debug)]
986#[serde(rename_all = "PascalCase")]
987struct PathGuidanceSection {
988 track_section: TrackSection,
989 turn_description: Text,
990 guidance_advice: String,
991}
992
993#[derive(Deserialize, Debug)]
994#[serde(rename_all = "PascalCase")]
995pub struct TransferLeg {
996 transfer_type: String,
997 leg_start: LegEndpoint,
998 leg_end: LegEndpoint,
999 #[serde(with = "duration")]
1000 duration: Duration,
1001}
1002
1003impl TransferLeg {
1004 pub fn departure_stop(&self) -> &str {
1005 self.leg_start.name()
1006 }
1007
1008 pub fn arrival_stop(&self) -> &str {
1009 self.leg_end.name()
1010 }
1011
1012 pub fn departure_id(&self) -> Result<i32, OjpError> {
1013 self.leg_start.id()
1014 }
1015
1016 pub fn arrival_id(&self) -> Result<i32, OjpError> {
1017 self.leg_end.id()
1018 }
1019}
1020
1021#[derive(Deserialize, Debug)]
1022#[serde(rename_all = "PascalCase")]
1023struct LegEndpoint {
1024 stop_point_ref: String,
1025 name: Text,
1026}
1027
1028impl LegEndpoint {
1029 pub fn id(&self) -> Result<i32, OjpError> {
1030 if let Ok(num) = self.stop_point_ref.parse::<i32>() {
1031 Ok(num)
1032 } else {
1033 sloid_to_didok(&self.stop_point_ref)
1034 }
1035 }
1036 pub fn name(&self) -> &str {
1037 self.name.text.as_str()
1038 }
1039}
1040
1041#[derive(Deserialize, Debug)]
1042#[serde(rename_all = "PascalCase")]
1043pub struct TimedLeg {
1044 leg_board: LegBoard,
1045 leg_alight: LegAlight,
1046 #[serde(rename = "LegIntermediate", default)]
1047 leg_intermediates: Vec<LegIntermediate>,
1048 service: Service,
1049 leg_track: Option<LegTrack>,
1050}
1051
1052impl TimedLeg {
1053 pub fn departure_time(&self) -> DateTime<Utc> {
1054 if let Some(time) = self.leg_board.service_departure.estimated_time {
1055 time
1056 } else {
1057 self.leg_board.service_departure.timetabled_time
1058 }
1059 }
1060
1061 pub fn arrival_time(&self) -> DateTime<Utc> {
1062 if let Some(time) = self.leg_alight.service_arrival.estimated_time {
1063 time
1064 } else {
1065 self.leg_alight.service_arrival.timetabled_time
1066 }
1067 }
1068
1069 pub fn departure_id(&self) -> Result<i32, OjpError> {
1070 self.leg_board.id()
1071 }
1072
1073 pub fn arrival_id(&self) -> Result<i32, OjpError> {
1074 self.leg_alight.id()
1075 }
1076
1077 pub fn departure_stop(&self) -> &str {
1078 self.leg_board.name()
1079 }
1080
1081 pub fn arrival_stop(&self) -> &str {
1082 self.leg_alight.name()
1083 }
1084}
1085
1086#[derive(Deserialize, Debug)]
1087#[serde(rename_all = "PascalCase")]
1088struct LegIntermediate {
1089 stop_point_ref: String,
1090 stop_point_name: Text,
1091 name_suffix: Option<Text>,
1092 planned_quay: Option<Text>,
1093 service_arrival: Option<ServiceArrival>,
1094 service_departure: Option<ServiceDeparture>,
1095 order: u32,
1096 #[serde(rename = "ExpectedDepartureOccupancy", default)]
1097 expected_departure_occupancies: Vec<ExpectedDepartureOccupancy>,
1098}
1099
1100#[derive(Deserialize, Debug)]
1101#[serde(rename_all = "PascalCase")]
1102struct LegBoard {
1103 stop_point_ref: String,
1104 stop_point_name: Text,
1105 name_suffix: Option<Text>,
1106 planned_quay: Option<Text>,
1107 estimated_quay: Option<Text>,
1108 service_departure: ServiceDeparture,
1109 order: u32,
1110 #[serde(rename = "ExpectedDepartureOccupancy", default)]
1111 expected_departure_occupancies: Vec<ExpectedDepartureOccupancy>,
1112}
1113
1114impl LegBoard {
1115 pub fn id(&self) -> Result<i32, OjpError> {
1116 if let Ok(num) = self.stop_point_ref.parse::<i32>() {
1117 Ok(num)
1118 } else {
1119 sloid_to_didok(&self.stop_point_ref)
1120 }
1121 }
1122 pub fn name(&self) -> &str {
1123 self.stop_point_name.text.as_str()
1124 }
1125}
1126
1127#[derive(Deserialize, Debug)]
1128#[serde(rename_all = "PascalCase")]
1129struct LegAlight {
1130 stop_point_ref: String,
1131 stop_point_name: Text,
1132 name_suffix: Option<Text>,
1133 planned_quay: Option<Text>,
1134 estimated_quay: Option<Text>,
1135 service_arrival: ServiceArrival,
1136 order: u32,
1137}
1138
1139impl LegAlight {
1140 pub fn id(&self) -> Result<i32, OjpError> {
1141 if let Ok(num) = self.stop_point_ref.parse::<i32>() {
1142 Ok(num)
1143 } else {
1144 sloid_to_didok(&self.stop_point_ref)
1145 }
1146 }
1147 pub fn name(&self) -> &str {
1148 self.stop_point_name.text.as_str()
1149 }
1150}
1151
1152#[derive(Deserialize, Debug)]
1153#[serde(rename_all = "PascalCase")]
1154struct ServiceDeparture {
1155 timetabled_time: DateTime<Utc>,
1156 estimated_time: Option<DateTime<Utc>>,
1157}
1158
1159#[derive(Deserialize, Debug)]
1160#[serde(rename_all = "PascalCase")]
1161struct ServiceArrival {
1162 timetabled_time: DateTime<Utc>,
1163 estimated_time: Option<DateTime<Utc>>,
1164}
1165
1166#[derive(Deserialize, Debug)]
1167#[serde(rename_all = "PascalCase")]
1168struct Service {
1169 operating_day_ref: String,
1170 journey_ref: String,
1171 public_code: String,
1172 line_ref: String,
1173 direction_ref: String,
1174 mode: Mode,
1175 product_category: Option<ProductCategory>,
1176 published_service_name: Text,
1177 train_number: String,
1178 #[serde(rename = "Attribute", default)]
1179 attributes: Vec<Attribute>,
1180 origin_text: Text,
1181 operator_ref: String,
1182 destination_stop_point_ref: String,
1183 destination_text: Text,
1184 #[serde(default)]
1185 origin_stop_point_ref: String,
1186}
1187
1188#[derive(Deserialize, Debug)]
1189#[serde(rename_all = "PascalCase")]
1190struct Mode {
1191 pt_mode: String,
1192 #[serde(default)]
1193 rail_submode: String,
1194 name: Text,
1195 short_name: Text,
1196}
1197
1198impl Mode {
1199 pub fn name(&self) -> &str {
1200 self.name.text.as_str()
1201 }
1202}
1203
1204#[derive(Deserialize, Debug)]
1205#[serde(rename_all = "PascalCase")]
1206struct ProductCategory {
1207 name: Text,
1208 short_name: Text,
1209 product_category_ref: String,
1210}
1211
1212#[derive(Deserialize, Debug)]
1213#[serde(rename_all = "PascalCase")]
1214struct Attribute {
1215 user_text: Text,
1216 code: String,
1217 importance: u32,
1218}
1219
1220#[derive(Deserialize, Debug)]
1221#[serde(rename_all = "PascalCase")]
1222struct Text {
1223 text: String,
1224}
1225
1226#[derive(Deserialize, Debug)]
1227#[serde(rename_all = "PascalCase")]
1228struct EmissionCO2 {
1229 kilogram_per_person_km: f32,
1230}
1231
1232#[derive(Deserialize, Debug)]
1233#[serde(rename_all = "PascalCase")]
1234struct ExpectedDepartureOccupancy {
1235 fare_class: String,
1236 occupancy_level: String,
1237}
1238
1239#[derive(Deserialize, Debug)]
1240#[serde(rename_all = "PascalCase")]
1241struct LegTrack {
1242 track_section: TrackSection,
1243}
1244
1245#[derive(Deserialize, Debug)]
1246#[serde(rename_all = "PascalCase")]
1247struct TrackSection {
1248 track_section_start: Option<TrackSectionEndpoint>,
1249 track_section_end: Option<TrackSectionEndpoint>,
1250 link_projection: Option<LinkProjection>,
1251 road_name: Option<String>,
1252 #[serde(with = "duration")]
1253 duration: Duration,
1254 length: i32,
1255}
1256
1257#[derive(Deserialize, Debug)]
1258#[serde(rename_all = "PascalCase")]
1259struct TrackSectionEndpoint {
1260 stop_point_ref: String,
1261 name: Text,
1262}
1263
1264#[derive(Deserialize, Debug)]
1265#[serde(rename_all = "PascalCase")]
1266struct LinkProjection {
1267 #[serde(rename = "Position", default)]
1268 positions: Vec<Position>,
1269}
1270
1271#[derive(Deserialize, Debug)]
1272#[serde(rename_all = "PascalCase")]
1273struct Position {
1274 longitude: f64,
1275 latitude: f64,
1276}
1277
1278#[derive(Deserialize, Debug)]
1279#[serde(rename_all = "PascalCase")]
1280struct OJPLocationInformationDelivery {
1281 #[serde(rename = "PlaceResult", default)]
1282 place_results: Vec<PlaceResult>,
1283}
1284
1285#[derive(Deserialize, Debug)]
1286#[serde(rename_all = "PascalCase")]
1287struct StopEventResponseContext {
1288 places: Places,
1289}
1290
1291#[derive(Deserialize, Debug)]
1292#[serde(rename_all = "PascalCase")]
1293struct OJPStopEventDelivery {
1294 stop_event_response_context: Option<StopEventResponseContext>,
1295 #[serde(rename = "StopEventResult", default)]
1296 stop_event_results: Vec<StopEventResult>,
1297}
1298
1299#[derive(Deserialize, Debug)]
1300#[serde(rename_all = "PascalCase")]
1301struct StopEventResult {
1302 id: String,
1303 stop_event: StopEvent,
1304}
1305
1306#[derive(Deserialize, Debug)]
1307#[serde(rename_all = "PascalCase")]
1308struct StopEvent {
1309 this_call: ThisCall,
1310 service: Service,
1311 operating_days: Option<OperatingDays>,
1312}
1313
1314#[derive(Deserialize, Debug)]
1315#[serde(rename_all = "PascalCase")]
1316struct OperatingDays {
1317 from: String,
1318 to: String,
1319 pattern: String,
1320}
1321
1322#[derive(Deserialize, Debug)]
1323#[serde(rename_all = "PascalCase")]
1324struct ThisCall {
1325 call_at_stop: CallAtStop,
1326}
1327
1328#[derive(Deserialize, Debug)]
1329#[serde(rename_all = "PascalCase")]
1330struct CallAtStop {
1331 stop_point_ref: String,
1332 stop_point_name: Text,
1333 service_departure: Option<ServiceDeparture>,
1334 service_arrival: Option<ServiceArrival>,
1335 order: u32,
1336}
1337
1338#[derive(Deserialize, Debug)]
1339#[serde(rename_all = "PascalCase")]
1340pub struct PlaceResult {
1341 place: Place,
1342 complete: bool,
1343 probability: f64,
1344}
1345
1346impl PlaceResult {
1347 pub fn stop_place_ref(&self) -> Option<i32> {
1348 Some(self.place.stop_place.as_ref()?.stop_place_ref)
1349 }
1350
1351 pub fn stop_place_name(&self) -> Option<&str> {
1352 Some(&self.place.stop_place.as_ref()?.stop_place_name.text)
1353 }
1354}
1355
1356#[derive(Deserialize, Debug)]
1357#[serde(rename_all = "PascalCase")]
1358struct Place {
1359 stop_place: Option<StopPlace>,
1360 topographic_place: Option<TopographicPlace>,
1361 stop_point: Option<StopPoint>,
1362 name: Text,
1363 geo_position: GeoPosition,
1364 #[serde(rename = "Mode", default)]
1365 place_modes: Vec<PlaceMode>,
1366}
1367
1368#[derive(Deserialize, Debug)]
1369#[serde(rename_all = "PascalCase")]
1370struct StopPoint {
1371 stop_point_ref: String,
1372 stop_point_name: Text,
1373 private_code: PrivateCode,
1374 parent_ref: String,
1375 topographic_place_ref: String,
1376}
1377
1378#[derive(Deserialize, Debug)]
1379#[serde(rename_all = "PascalCase")]
1380struct TopographicPlace {
1381 topographic_place_code: String,
1382 topographic_place_name: Text,
1383}
1384
1385#[derive(Deserialize, Debug)]
1386#[serde(rename_all = "PascalCase")]
1387struct StopPlace {
1388 stop_place_ref: i32,
1389 stop_place_name: Text,
1390 private_code: PrivateCode,
1391 topographic_place_ref: String,
1392}
1393
1394#[derive(Deserialize, Debug)]
1395#[serde(rename_all = "PascalCase")]
1396struct PrivateCode {
1397 system: String,
1398 value: String,
1399}
1400
1401#[derive(Deserialize, Debug)]
1402#[serde(rename_all = "PascalCase")]
1403struct GeoPosition {
1404 longitude: f64,
1405 latitude: f64,
1406}
1407
1408#[derive(Deserialize, Debug)]
1409#[serde(rename_all = "PascalCase")]
1410struct PlaceMode {
1411 pt_mode: String,
1412 #[serde(default)]
1413 rail_submode: String,
1414 #[serde(default)]
1415 tram_submode: String,
1416 #[serde(default)]
1417 bus_submode: String,
1418 #[serde(default)]
1419 funicular_submode: String,
1420}
1421
1422#[cfg(test)]
1423mod test {
1424 use crate::{OJP, RequestBuilder, RequestType, SimplifiedTrip, token};
1425 use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
1426 use std::error::Error;
1427 use test_log::test;
1428
1429 const FORMAT: &str = "%Y-%m-%dT%H:%M:%SZ";
1430
1431 #[allow(unused)]
1432 fn parse_xml(xml: &str) -> Result<OJP, Box<dyn Error>> {
1433 let xml = std::fs::read_to_string(xml)?;
1434 let ojp = super::OJP::try_from(xml.as_str())?;
1435 Ok(ojp)
1436 }
1437 #[test]
1438 fn location_coordinate() {
1439 let _ojp = parse_xml("test_xml/location_coordinate.xml").unwrap();
1440 }
1441
1442 #[test]
1443 fn location_extended() {
1444 let _ojp = parse_xml("test_xml/location_extended.xml").unwrap();
1445 }
1446
1447 #[test]
1448 fn location_simple() {
1449 let ojp = parse_xml("test_xml/location_simple.xml").unwrap();
1450 let place_results = ojp.place_results().unwrap();
1451 assert_eq!(place_results.len(), 14);
1452 }
1453
1454 #[test]
1455 fn location_topographic() {
1456 let _ojp = parse_xml("test_xml/location_topographic.xml").unwrap();
1457 }
1458
1459 #[test]
1460 fn stop_simple() {
1461 let _ojp = parse_xml("test_xml/stop_simple.xml").unwrap();
1462 }
1463
1464 #[test]
1465 fn stop_complex() {
1466 let _ojp = parse_xml("test_xml/stop_complex.xml").unwrap();
1467 }
1468
1469 #[test]
1470 fn trip_simple() {
1471 let ojp = parse_xml("test_xml/trip_simple.xml").unwrap();
1472 let fastest_trip = ojp.fastest_trip().unwrap();
1473 assert_eq!(fastest_trip.duration.num_seconds(), 3 * 60 + 30);
1474 let trip_after = ojp
1475 .trip_departing_after(
1476 NaiveDateTime::parse_from_str("2025-10-17T09:00:00Z", FORMAT).unwrap(),
1477 0,
1478 )
1479 .unwrap();
1480
1481 assert_eq!(
1482 trip_after.start_time.naive_utc(),
1483 NaiveDateTime::parse_from_str("2025-10-17T09:07:24Z", FORMAT).unwrap()
1484 );
1485 assert_eq!(
1486 trip_after.end_time.naive_utc(),
1487 NaiveDateTime::parse_from_str("2025-10-17T09:10:54Z", FORMAT).unwrap()
1488 );
1489
1490 assert_eq!(trip_after.id, "ID-5CE0364E-BD0F-4D17-929E-B3E4F4EAA714");
1491
1492 let simplified_trip = SimplifiedTrip::try_from(trip_after).unwrap();
1493 assert_eq!(
1494 simplified_trip.departure_time(),
1495 NaiveDateTime::parse_from_str("2025-10-17T09:07:24Z", FORMAT).unwrap()
1496 );
1497 assert_eq!(
1498 simplified_trip.arrival_time(),
1499 NaiveDateTime::parse_from_str("2025-10-17T09:10:54Z", FORMAT).unwrap()
1500 );
1501
1502 let trips = ojp.trips().unwrap();
1503 assert_eq!(trips.len(), 3);
1504 assert_eq!(
1505 ojp.trips_departing_after(
1506 NaiveDateTime::parse_from_str("2025-10-17T09:00:00Z", FORMAT).unwrap(),
1507 )
1508 .unwrap()
1509 .len(),
1510 2
1511 );
1512 }
1513
1514 #[test]
1515 fn trip_lots() {
1516 let _ojp = parse_xml("test_xml/trip_lots.xml").unwrap();
1517 }
1518
1519 #[tokio::test(flavor = "current_thread")]
1520 #[test_log::test]
1521 async fn request_location_information_service_simple() {
1522 dotenvy::dotenv().ok(); let date_time = NaiveDateTime::new(
1524 NaiveDate::from_ymd_opt(2025, 11, 19).unwrap(),
1525 NaiveTime::from_hms_milli_opt(20, 56, 28, 643).unwrap(),
1526 );
1527 let response = RequestBuilder::new(date_time)
1528 .set_token(token("TOKEN").unwrap())
1529 .set_requestor_ref("Test")
1530 .set_name("bern s")
1531 .set_number_results(3)
1532 .set_request_type(RequestType::LocationInformation)
1533 .send_request()
1534 .await
1535 .unwrap();
1536 let _ojp = OJP::try_from(response.as_str()).unwrap();
1537 }
1538
1539 #[tokio::test(flavor = "current_thread")]
1540 #[test_log::test]
1541 async fn request_trip_service_simple() {
1542 dotenvy::dotenv().ok(); let date_time = NaiveDateTime::new(
1544 NaiveDate::from_ymd_opt(2025, 11, 19).unwrap(),
1545 NaiveTime::from_hms_milli_opt(20, 56, 28, 643).unwrap(),
1546 );
1547 let response = RequestBuilder::new(date_time)
1548 .set_token(token("TOKEN").unwrap())
1549 .set_requestor_ref("Test")
1550 .set_number_results(3)
1551 .set_request_type(RequestType::Trip)
1552 .set_from(8503308)
1553 .set_to(8503424)
1554 .send_request()
1555 .await
1556 .unwrap();
1557 let _ojp = OJP::try_from(response.as_str()).unwrap();
1558 }
1559}