ojp_rs/
model.rs

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    // Split SLOID into parts
94    let parts: Vec<&str> = sloid.split(':').collect();
95    if parts.len() < 4 {
96        return Err(OjpError::MalformedSloid(sloid.to_string())); // not enough parts
97    }
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    // Extract number and pad to 5 digits
103    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        // TODO: Currently -PT is treated as negative Duration. But I'm not sure what that means...
133
134        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(&current_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    // src/serde_duration.rs  (or wherever your deserialize() lives)
168
169    #[cfg(test)]
170    mod tests {
171        use super::*; // brings `deserialize` into scope
172        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    /// Given an array of `&str` containing names of places, returns  Finds `number_results` trip `from_id` to `to_id` at `date_time` using the OJP API.
287    /// The name of the environment variable needs to be profived through the varibale `api_key`.
288    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    /// Finds `number_results` trips from a list of departures and arrivals at `date_time` using the OJP API.
309    /// The length of `departures` and `arrivals` must be the same.
310    /// The name of the environment variable needs to be profived through the varibale `api_key`.
311    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    /// Finds `number_results` trip `from_id` to `to_id` at `date_time` using the OJP API.
338    /// The name of the environment variable needs to be profived through the varibale `api_key`.
339    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    /// Returns all trips from the OJP response
392    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    // Returns references over all all PlaceResults
411    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    /// Returns all trips from the OJP response that are starting after `date_time`
424    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    /// Returns the `index`-th Trip if existing
440    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    /// Returns the `index`-th Trip if existing
451    pub fn trip(&self, index: usize) -> Option<&Trip> {
452        Some(&self.trips()?.get(index).copied()?.trip)
453    }
454
455    // Returns the error message, if the OJP trip delivery returned an error (if no trip found for
456    // example)
457    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    // TODO: Both are present until now, but they is an error that say they are missing
532    // the even when present. Impossible to know why.
533    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/// Basic trip information: departure time, arrival time, and duration
673#[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        // deprature and arrival must be the same
782        if self.departure_id() != rhs.departure_id() || self.arrival_id() != rhs.arrival_id() {
783            return false;
784        }
785        // duration must be approximately equal
786        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        // departure and arrival time must be approximately the same with respect to duration
794        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(); // optional
1523        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(); // optional
1543        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}