Skip to main content

gtfs_guru_model/
lib.rs

1use compact_str::CompactString;
2use std::fmt;
3
4use chrono::NaiveDate;
5use serde::de::{self, Visitor};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::cell::RefCell;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
10pub struct StringId(pub u32);
11
12impl fmt::Display for StringId {
13    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14        let s = RESOLVER_HOOK.with(|hook| {
15            if let Some(ref func) = *hook.borrow() {
16                func(*self)
17            } else {
18                format!("StringId({})", self.0)
19            }
20        });
21        write!(f, "{}", s)
22    }
23}
24
25type InternerHook = Box<dyn Fn(&str) -> StringId>;
26type ResolverHook = Box<dyn Fn(StringId) -> String>;
27
28thread_local! {
29    static INTERNER_HOOK: RefCell<Option<InternerHook>> = RefCell::new(None);
30    static RESOLVER_HOOK: RefCell<Option<ResolverHook>> = RefCell::new(None);
31}
32
33pub fn set_thread_local_interner<F>(f: F)
34where
35    F: Fn(&str) -> StringId + 'static,
36{
37    INTERNER_HOOK.with(|hook| {
38        *hook.borrow_mut() = Some(Box::new(f));
39    });
40}
41
42pub fn set_thread_local_resolver<F>(f: F)
43where
44    F: Fn(StringId) -> String + 'static,
45{
46    RESOLVER_HOOK.with(|hook| {
47        *hook.borrow_mut() = Some(Box::new(f));
48    });
49}
50
51pub fn clear_thread_local_hooks() {
52    INTERNER_HOOK.with(|hook| {
53        *hook.borrow_mut() = None;
54    });
55    RESOLVER_HOOK.with(|hook| {
56        *hook.borrow_mut() = None;
57    });
58}
59
60impl Serialize for StringId {
61    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
62        let s = RESOLVER_HOOK.with(|hook| {
63            if let Some(ref f) = *hook.borrow() {
64                f(*self)
65            } else {
66                format!("StringId({})", self.0)
67            }
68        });
69        serializer.serialize_str(&s)
70    }
71}
72
73impl<'de> Deserialize<'de> for StringId {
74    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
75        struct StringIdVisitor;
76
77        impl<'de> Visitor<'de> for StringIdVisitor {
78            type Value = StringId;
79
80            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81                formatter.write_str("a string to be interned")
82            }
83
84            fn visit_str<E: de::Error>(self, value: &str) -> Result<StringId, E> {
85                INTERNER_HOOK.with(|hook| {
86                    if let Some(ref f) = *hook.borrow() {
87                        Ok(f(value))
88                    } else {
89                        // Fallback or error? For now, let's treat it as a bug if hook is missing
90                        // but maybe we can just return a dummy ID if we don't care about interning here.
91                        // However, the goal is always to intern during load.
92                        Ok(StringId(0))
93                    }
94                })
95            }
96        }
97
98        deserializer.deserialize_str(StringIdVisitor)
99    }
100}
101
102#[derive(Debug, thiserror::Error)]
103pub enum GtfsParseError {
104    #[error("invalid date format: {0}")]
105    InvalidDateFormat(String),
106    #[error("invalid date value: {0}")]
107    InvalidDateValue(String),
108    #[error("invalid time format: {0}")]
109    InvalidTimeFormat(String),
110    #[error("invalid time value: {0}")]
111    InvalidTimeValue(String),
112    #[error("invalid color format: {0}")]
113    InvalidColorFormat(String),
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
117pub struct GtfsDate {
118    year: i32,
119    month: u8,
120    day: u8,
121}
122
123impl GtfsDate {
124    pub fn parse(value: &str) -> Result<Self, GtfsParseError> {
125        let trimmed = value.trim();
126        if trimmed.len() != 8 || !trimmed.chars().all(|ch| ch.is_ascii_digit()) {
127            return Err(GtfsParseError::InvalidDateFormat(value.to_string()));
128        }
129
130        let year: i32 = trimmed[0..4]
131            .parse()
132            .map_err(|_| GtfsParseError::InvalidDateFormat(value.to_string()))?;
133        let month: u8 = trimmed[4..6]
134            .parse()
135            .map_err(|_| GtfsParseError::InvalidDateFormat(value.to_string()))?;
136        let day: u8 = trimmed[6..8]
137            .parse()
138            .map_err(|_| GtfsParseError::InvalidDateFormat(value.to_string()))?;
139
140        if NaiveDate::from_ymd_opt(year, month as u32, day as u32).is_none() {
141            return Err(GtfsParseError::InvalidDateValue(value.to_string()));
142        }
143
144        Ok(Self { year, month, day })
145    }
146
147    pub fn year(&self) -> i32 {
148        self.year
149    }
150
151    pub fn month(&self) -> u8 {
152        self.month
153    }
154
155    pub fn day(&self) -> u8 {
156        self.day
157    }
158}
159
160impl fmt::Display for GtfsDate {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        write!(f, "{:04}{:02}{:02}", self.year, self.month, self.day)
163    }
164}
165
166impl Serialize for GtfsDate {
167    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
168        serializer.serialize_str(&self.to_string())
169    }
170}
171
172impl<'de> Deserialize<'de> for GtfsDate {
173    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
174        struct GtfsDateVisitor;
175
176        impl<'de> Visitor<'de> for GtfsDateVisitor {
177            type Value = GtfsDate;
178
179            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
180                formatter.write_str("a GTFS date in YYYYMMDD format")
181            }
182
183            fn visit_str<E: de::Error>(self, value: &str) -> Result<GtfsDate, E> {
184                GtfsDate::parse(value).map_err(E::custom)
185            }
186        }
187
188        deserializer.deserialize_str(GtfsDateVisitor)
189    }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
193pub struct GtfsTime {
194    total_seconds: i32,
195}
196
197impl GtfsTime {
198    pub fn from_seconds(total_seconds: i32) -> Self {
199        Self { total_seconds }
200    }
201
202    pub fn parse(value: &str) -> Result<Self, GtfsParseError> {
203        let trimmed = value.trim();
204        let parts: Vec<&str> = trimmed.split(':').collect();
205        if parts.len() != 3 {
206            return Err(GtfsParseError::InvalidTimeFormat(value.to_string()));
207        }
208
209        let hours: i32 = parts[0]
210            .parse()
211            .map_err(|_| GtfsParseError::InvalidTimeFormat(value.to_string()))?;
212        let minutes: i32 = parts[1]
213            .parse()
214            .map_err(|_| GtfsParseError::InvalidTimeFormat(value.to_string()))?;
215        let seconds: i32 = parts[2]
216            .parse()
217            .map_err(|_| GtfsParseError::InvalidTimeFormat(value.to_string()))?;
218
219        if hours < 0 || !(0..=59).contains(&minutes) || !(0..=59).contains(&seconds) {
220            return Err(GtfsParseError::InvalidTimeValue(value.to_string()));
221        }
222
223        Ok(Self {
224            total_seconds: hours * 3600 + minutes * 60 + seconds,
225        })
226    }
227
228    pub fn total_seconds(&self) -> i32 {
229        self.total_seconds
230    }
231
232    pub fn hours(&self) -> i32 {
233        self.total_seconds / 3600
234    }
235
236    pub fn minutes(&self) -> i32 {
237        (self.total_seconds % 3600) / 60
238    }
239
240    pub fn seconds(&self) -> i32 {
241        self.total_seconds % 60
242    }
243}
244
245impl fmt::Display for GtfsTime {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        write!(
248            f,
249            "{:02}:{:02}:{:02}",
250            self.hours(),
251            self.minutes(),
252            self.seconds()
253        )
254    }
255}
256
257impl Serialize for GtfsTime {
258    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
259        serializer.serialize_str(&self.to_string())
260    }
261}
262
263impl<'de> Deserialize<'de> for GtfsTime {
264    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
265        struct GtfsTimeVisitor;
266
267        impl<'de> Visitor<'de> for GtfsTimeVisitor {
268            type Value = GtfsTime;
269
270            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
271                formatter.write_str("a GTFS time in HH:MM:SS format")
272            }
273
274            fn visit_str<E: de::Error>(self, value: &str) -> Result<GtfsTime, E> {
275                GtfsTime::parse(value).map_err(E::custom)
276            }
277        }
278
279        deserializer.deserialize_str(GtfsTimeVisitor)
280    }
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
284pub struct GtfsColor {
285    rgb: u32,
286}
287
288impl GtfsColor {
289    pub fn new(r: u8, g: u8, b: u8) -> Self {
290        Self {
291            rgb: (r as u32) << 16 | (g as u32) << 8 | (b as u32),
292        }
293    }
294
295    pub fn parse(value: &str) -> Result<Self, GtfsParseError> {
296        let trimmed = value.trim();
297        if trimmed.len() != 6 || !trimmed.chars().all(|ch| ch.is_ascii_hexdigit()) {
298            return Err(GtfsParseError::InvalidColorFormat(value.to_string()));
299        }
300
301        let rgb = u32::from_str_radix(trimmed, 16)
302            .map_err(|_| GtfsParseError::InvalidColorFormat(value.to_string()))?;
303        Ok(Self { rgb })
304    }
305
306    pub fn rgb(&self) -> u32 {
307        self.rgb
308    }
309
310    pub fn rec601_luma(&self) -> i32 {
311        let r = ((self.rgb >> 16) & 0xFF) as f64;
312        let g = ((self.rgb >> 8) & 0xFF) as f64;
313        let b = (self.rgb & 0xFF) as f64;
314        (0.30 * r + 0.59 * g + 0.11 * b) as i32
315    }
316}
317
318impl fmt::Display for GtfsColor {
319    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320        write!(f, "{:06X}", self.rgb)
321    }
322}
323
324impl Serialize for GtfsColor {
325    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
326        serializer.serialize_str(&self.to_string())
327    }
328}
329
330impl<'de> Deserialize<'de> for GtfsColor {
331    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
332        struct GtfsColorVisitor;
333
334        impl<'de> Visitor<'de> for GtfsColorVisitor {
335            type Value = GtfsColor;
336
337            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
338                formatter.write_str("a 6-digit GTFS color hex string")
339            }
340
341            fn visit_str<E: de::Error>(self, value: &str) -> Result<GtfsColor, E> {
342                GtfsColor::parse(value).map_err(E::custom)
343            }
344        }
345
346        deserializer.deserialize_str(GtfsColorVisitor)
347    }
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
351pub enum LocationType {
352    #[serde(rename = "0")]
353    StopOrPlatform,
354    #[serde(rename = "1")]
355    Station,
356    #[serde(rename = "2")]
357    EntranceOrExit,
358    #[serde(rename = "3")]
359    GenericNode,
360    #[serde(rename = "4")]
361    BoardingArea,
362    #[serde(other)]
363    Other,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
367pub enum WheelchairBoarding {
368    #[serde(rename = "0")]
369    NoInfo,
370    #[serde(rename = "1")]
371    Some,
372    #[serde(rename = "2")]
373    NotPossible,
374    #[serde(other)]
375    Other,
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
379pub enum RouteType {
380    Tram,
381    Subway,
382    Rail,
383    Bus,
384    Ferry,
385    CableCar,
386    Gondola,
387    Funicular,
388    Trolleybus,
389    Monorail,
390    Extended(u16),
391    Unknown,
392}
393
394impl RouteType {
395    fn from_i32(value: i32) -> Self {
396        match value {
397            0 => RouteType::Tram,
398            1 => RouteType::Subway,
399            2 => RouteType::Rail,
400            3 => RouteType::Bus,
401            4 => RouteType::Ferry,
402            5 => RouteType::CableCar,
403            6 => RouteType::Gondola,
404            7 => RouteType::Funicular,
405            11 => RouteType::Trolleybus,
406            12 => RouteType::Monorail,
407            100..=1702 => RouteType::Extended(value as u16),
408            _ => RouteType::Unknown,
409        }
410    }
411}
412
413impl<'de> Deserialize<'de> for RouteType {
414    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
415        struct RouteTypeVisitor;
416
417        impl<'de> Visitor<'de> for RouteTypeVisitor {
418            type Value = RouteType;
419
420            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
421                formatter.write_str("a GTFS route_type numeric value")
422            }
423
424            fn visit_str<E: de::Error>(self, value: &str) -> Result<RouteType, E> {
425                let trimmed = value.trim();
426                if trimmed.is_empty() {
427                    return Err(E::custom("empty route_type"));
428                }
429                let parsed: i32 = trimmed.parse().map_err(E::custom)?;
430                Ok(RouteType::from_i32(parsed))
431            }
432
433            fn visit_i64<E: de::Error>(self, value: i64) -> Result<RouteType, E> {
434                Ok(RouteType::from_i32(value as i32))
435            }
436
437            fn visit_u64<E: de::Error>(self, value: u64) -> Result<RouteType, E> {
438                Ok(RouteType::from_i32(value as i32))
439            }
440        }
441
442        deserializer.deserialize_any(RouteTypeVisitor)
443    }
444}
445
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
447pub enum ContinuousPickupDropOff {
448    #[serde(rename = "0")]
449    Continuous,
450    #[serde(rename = "1")]
451    NoContinuous,
452    #[serde(rename = "2")]
453    MustPhone,
454    #[serde(rename = "3")]
455    MustCoordinateWithDriver,
456    #[serde(other)]
457    Other,
458}
459
460#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
461pub enum PickupDropOffType {
462    #[serde(rename = "0")]
463    Regular,
464    #[serde(rename = "1")]
465    NoPickup,
466    #[serde(rename = "2")]
467    MustPhone,
468    #[serde(rename = "3")]
469    MustCoordinateWithDriver,
470    #[serde(other)]
471    Other,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
475pub enum BookingType {
476    #[serde(rename = "0")]
477    Realtime,
478    #[serde(rename = "1")]
479    SameDay,
480    #[serde(rename = "2")]
481    PriorDay,
482    #[serde(other)]
483    Other,
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
487pub enum DirectionId {
488    #[serde(rename = "0")]
489    Direction0,
490    #[serde(rename = "1")]
491    Direction1,
492    #[serde(other)]
493    Other,
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
497pub enum WheelchairAccessible {
498    #[serde(rename = "0")]
499    NoInfo,
500    #[serde(rename = "1")]
501    Accessible,
502    #[serde(rename = "2")]
503    NotAccessible,
504    #[serde(other)]
505    Other,
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
509pub enum BikesAllowed {
510    #[serde(rename = "0")]
511    NoInfo,
512    #[serde(rename = "1")]
513    Allowed,
514    #[serde(rename = "2")]
515    NotAllowed,
516    #[serde(other)]
517    Other,
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Default)]
521pub enum ServiceAvailability {
522    #[default]
523    #[serde(rename = "0")]
524    Unavailable,
525    #[serde(rename = "1")]
526    Available,
527    #[serde(other)]
528    Other,
529}
530
531#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Default)]
532pub enum ExceptionType {
533    #[serde(rename = "1")]
534    Added,
535    #[serde(rename = "2")]
536    Removed,
537    #[default]
538    #[serde(other)]
539    Other,
540}
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
543pub enum PaymentMethod {
544    #[serde(rename = "0")]
545    OnBoard,
546    #[serde(rename = "1")]
547    BeforeBoarding,
548    #[serde(other)]
549    Other,
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
553pub enum Transfers {
554    #[serde(rename = "0")]
555    NoTransfers,
556    #[serde(rename = "1")]
557    OneTransfer,
558    #[serde(rename = "2")]
559    TwoTransfers,
560    #[serde(other)]
561    Other,
562}
563
564#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Default)]
565pub enum ExactTimes {
566    #[serde(rename = "0")]
567    FrequencyBased,
568    #[serde(rename = "1")]
569    ExactTimes,
570    #[default]
571    #[serde(other)]
572    Other,
573}
574
575#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
576pub enum TransferType {
577    #[serde(rename = "0")]
578    Recommended,
579    #[serde(rename = "1")]
580    Timed,
581    #[serde(rename = "2")]
582    MinTime,
583    #[serde(rename = "3")]
584    NoTransfer,
585    #[serde(rename = "4")]
586    InSeat,
587    #[serde(rename = "5")]
588    InSeatNotAllowed,
589    #[serde(other)]
590    Other,
591}
592
593#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Default)]
594pub enum PathwayMode {
595    #[default]
596    #[serde(rename = "1")]
597    Walkway,
598    #[serde(rename = "2")]
599    Stairs,
600    #[serde(rename = "3")]
601    MovingSidewalk,
602    #[serde(rename = "4")]
603    Escalator,
604    #[serde(rename = "5")]
605    Elevator,
606    #[serde(rename = "6")]
607    FareGate,
608    #[serde(rename = "7")]
609    ExitGate,
610    #[serde(other)]
611    Other,
612}
613
614#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Default)]
615pub enum Bidirectional {
616    #[default]
617    #[serde(rename = "0")]
618    Unidirectional,
619    #[serde(rename = "1")]
620    Bidirectional,
621    #[serde(other)]
622    Other,
623}
624
625#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
626pub enum YesNo {
627    #[serde(rename = "0")]
628    No,
629    #[serde(rename = "1")]
630    Yes,
631    #[serde(other)]
632    Other,
633}
634
635#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
636pub enum Timepoint {
637    #[serde(rename = "0")]
638    Approximate,
639    #[serde(rename = "1")]
640    Exact,
641    #[serde(other)]
642    Other,
643}
644
645#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
646pub enum FareMediaType {
647    #[serde(rename = "0")]
648    NoneType,
649    #[serde(rename = "1")]
650    PaperTicket,
651    #[serde(rename = "2")]
652    TransitCard,
653    #[serde(rename = "3")]
654    ContactlessEmv,
655    #[serde(rename = "4")]
656    MobileApp,
657    #[serde(other)]
658    Other,
659}
660
661#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
662pub enum DurationLimitType {
663    #[serde(rename = "0")]
664    DepartureToArrival,
665    #[serde(rename = "1")]
666    DepartureToDeparture,
667    #[serde(rename = "2")]
668    ArrivalToDeparture,
669    #[serde(rename = "3")]
670    ArrivalToArrival,
671    #[serde(other)]
672    Other,
673}
674
675#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
676pub enum FareTransferType {
677    #[serde(rename = "0")]
678    APlusAb,
679    #[serde(rename = "1")]
680    APlusAbPlusB,
681    #[serde(rename = "2")]
682    Ab,
683    #[serde(other)]
684    Other,
685}
686
687#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
688pub enum RiderFareCategory {
689    #[serde(rename = "0")]
690    NotDefault,
691    #[serde(rename = "1")]
692    IsDefault,
693    #[serde(other)]
694    Other,
695}
696
697#[derive(Debug, Clone, Deserialize, Default)]
698pub struct Agency {
699    pub agency_id: Option<StringId>,
700    pub agency_name: CompactString,
701    pub agency_url: StringId,
702    pub agency_timezone: StringId,
703    pub agency_lang: Option<StringId>,
704    pub agency_phone: Option<CompactString>,
705    pub agency_fare_url: Option<StringId>,
706    pub agency_email: Option<CompactString>,
707}
708
709#[derive(Debug, Clone, Deserialize, Default)]
710pub struct Stop {
711    pub stop_id: StringId,
712    pub stop_code: Option<CompactString>,
713    pub stop_name: Option<CompactString>,
714    pub tts_stop_name: Option<CompactString>,
715    pub stop_desc: Option<CompactString>,
716    pub stop_lat: Option<f64>,
717    pub stop_lon: Option<f64>,
718    pub zone_id: Option<StringId>,
719    pub stop_url: Option<StringId>,
720    pub location_type: Option<LocationType>,
721    pub parent_station: Option<StringId>,
722    pub stop_timezone: Option<StringId>,
723    pub wheelchair_boarding: Option<WheelchairBoarding>,
724    pub level_id: Option<StringId>,
725    pub platform_code: Option<CompactString>,
726    pub stop_address: Option<CompactString>,
727    pub stop_city: Option<CompactString>,
728    pub stop_region: Option<CompactString>,
729    pub stop_postcode: Option<CompactString>,
730    pub stop_country: Option<CompactString>,
731    pub stop_phone: Option<CompactString>,
732    pub signposted_as: Option<CompactString>,
733    pub vehicle_type: Option<RouteType>,
734}
735
736impl Stop {
737    pub fn has_coordinates(&self) -> bool {
738        self.stop_lat.is_some() && self.stop_lon.is_some()
739    }
740}
741
742#[derive(Debug, Clone, Deserialize)]
743pub struct Route {
744    pub route_id: StringId,
745    pub agency_id: Option<StringId>,
746    pub route_short_name: Option<CompactString>,
747    pub route_long_name: Option<CompactString>,
748    pub route_desc: Option<CompactString>,
749    pub route_type: RouteType,
750    pub route_url: Option<StringId>,
751    pub route_color: Option<GtfsColor>,
752    pub route_text_color: Option<GtfsColor>,
753    pub route_sort_order: Option<u32>,
754    pub continuous_pickup: Option<ContinuousPickupDropOff>,
755    pub continuous_drop_off: Option<ContinuousPickupDropOff>,
756    pub network_id: Option<StringId>,
757    pub route_branding_url: Option<StringId>,
758    pub checkin_duration: Option<u32>,
759}
760
761impl Default for Route {
762    fn default() -> Self {
763        Self {
764            route_id: StringId::default(),
765            agency_id: None,
766            route_short_name: None,
767            route_long_name: None,
768            route_desc: None,
769            route_type: RouteType::Bus,
770            route_url: None,
771            route_color: None,
772            route_text_color: None,
773            route_sort_order: None,
774            continuous_pickup: None,
775            continuous_drop_off: None,
776            network_id: None,
777            route_branding_url: None,
778            checkin_duration: None,
779        }
780    }
781}
782
783#[derive(Debug, Clone, Deserialize, Default)]
784pub struct Trip {
785    pub route_id: StringId,
786    pub service_id: StringId,
787    pub trip_id: StringId,
788    pub trip_headsign: Option<CompactString>,
789    pub trip_short_name: Option<CompactString>,
790    pub direction_id: Option<DirectionId>,
791    pub block_id: Option<StringId>,
792    pub shape_id: Option<StringId>,
793    pub wheelchair_accessible: Option<WheelchairAccessible>,
794    pub bikes_allowed: Option<BikesAllowed>,
795    pub continuous_pickup: Option<ContinuousPickupDropOff>,
796    pub continuous_drop_off: Option<ContinuousPickupDropOff>,
797}
798
799#[derive(Debug, Clone, Deserialize, Default)]
800pub struct StopTime {
801    pub trip_id: StringId,
802    pub arrival_time: Option<GtfsTime>,
803    pub departure_time: Option<GtfsTime>,
804    pub stop_id: StringId,
805    pub location_group_id: Option<StringId>,
806    pub location_id: Option<StringId>,
807    pub stop_sequence: u32,
808    pub stop_headsign: Option<CompactString>,
809    pub pickup_type: Option<PickupDropOffType>,
810    pub drop_off_type: Option<PickupDropOffType>,
811    pub pickup_booking_rule_id: Option<StringId>,
812    pub drop_off_booking_rule_id: Option<StringId>,
813    pub continuous_pickup: Option<ContinuousPickupDropOff>,
814    pub continuous_drop_off: Option<ContinuousPickupDropOff>,
815    pub shape_dist_traveled: Option<f64>,
816    pub timepoint: Option<Timepoint>,
817    pub start_pickup_drop_off_window: Option<GtfsTime>,
818    pub end_pickup_drop_off_window: Option<GtfsTime>,
819    pub stop_direction_name: Option<CompactString>,
820}
821
822#[derive(Debug, Clone, Deserialize)]
823pub struct BookingRules {
824    pub booking_rule_id: StringId,
825    pub booking_type: BookingType,
826    pub prior_notice_duration_min: Option<i32>,
827    pub prior_notice_duration_max: Option<i32>,
828    pub prior_notice_start_day: Option<i32>,
829    pub prior_notice_start_time: Option<GtfsTime>,
830    pub prior_notice_last_day: Option<i32>,
831    pub prior_notice_last_time: Option<GtfsTime>,
832    pub prior_notice_service_id: Option<StringId>,
833    pub message: Option<CompactString>,
834    pub pickup_message: Option<CompactString>,
835    pub drop_off_message: Option<CompactString>,
836    pub phone_number: Option<CompactString>,
837    pub info_url: Option<StringId>,
838    pub booking_url: Option<StringId>,
839}
840
841impl Default for BookingRules {
842    fn default() -> Self {
843        Self {
844            booking_rule_id: StringId::default(),
845            booking_type: BookingType::Other,
846            prior_notice_duration_min: None,
847            prior_notice_duration_max: None,
848            prior_notice_start_day: None,
849            prior_notice_start_time: None,
850            prior_notice_last_day: None,
851            prior_notice_last_time: None,
852            prior_notice_service_id: None,
853            message: None,
854            pickup_message: None,
855            drop_off_message: None,
856            phone_number: None,
857            info_url: None,
858            booking_url: None,
859        }
860    }
861}
862
863#[derive(Debug, Clone, Deserialize)]
864pub struct Calendar {
865    pub service_id: StringId,
866    pub monday: ServiceAvailability,
867    pub tuesday: ServiceAvailability,
868    pub wednesday: ServiceAvailability,
869    pub thursday: ServiceAvailability,
870    pub friday: ServiceAvailability,
871    pub saturday: ServiceAvailability,
872    pub sunday: ServiceAvailability,
873    pub start_date: GtfsDate,
874    pub end_date: GtfsDate,
875}
876
877impl Default for Calendar {
878    fn default() -> Self {
879        Self {
880            service_id: StringId::default(),
881            monday: ServiceAvailability::Unavailable,
882            tuesday: ServiceAvailability::Unavailable,
883            wednesday: ServiceAvailability::Unavailable,
884            thursday: ServiceAvailability::Unavailable,
885            friday: ServiceAvailability::Unavailable,
886            saturday: ServiceAvailability::Unavailable,
887            sunday: ServiceAvailability::Unavailable,
888            start_date: GtfsDate {
889                year: 0,
890                month: 1,
891                day: 1,
892            },
893            end_date: GtfsDate {
894                year: 0,
895                month: 1,
896                day: 1,
897            },
898        }
899    }
900}
901
902#[derive(Debug, Clone, Deserialize, Default)]
903pub struct CalendarDate {
904    pub service_id: StringId,
905    pub date: GtfsDate,
906    pub exception_type: ExceptionType,
907}
908
909#[derive(Debug, Clone, Deserialize)]
910pub struct FareAttribute {
911    pub fare_id: StringId,
912    pub price: f64,
913    pub currency_type: StringId,
914    pub payment_method: PaymentMethod,
915    pub transfers: Option<Transfers>,
916    pub agency_id: Option<StringId>,
917    pub transfer_duration: Option<u32>,
918    pub ic_price: Option<f64>,
919}
920
921impl Default for FareAttribute {
922    fn default() -> Self {
923        Self {
924            fare_id: StringId::default(),
925            price: 0.0,
926            currency_type: StringId::default(),
927            payment_method: PaymentMethod::OnBoard,
928            transfers: None,
929            agency_id: None,
930            transfer_duration: None,
931            ic_price: None,
932        }
933    }
934}
935
936#[derive(Debug, Clone, Deserialize, Default)]
937pub struct FareRule {
938    pub fare_id: StringId,
939    pub route_id: Option<StringId>,
940    pub origin_id: Option<StringId>,
941    pub destination_id: Option<StringId>,
942    pub contains_id: Option<StringId>,
943    pub contains_route_id: Option<StringId>,
944}
945
946#[derive(Debug, Clone, Deserialize, Default)]
947pub struct Shape {
948    pub shape_id: StringId,
949    pub shape_pt_lat: f64,
950    pub shape_pt_lon: f64,
951    pub shape_pt_sequence: u32,
952    pub shape_dist_traveled: Option<f64>,
953}
954
955#[derive(Debug, Clone, Deserialize, Default)]
956pub struct Frequency {
957    pub trip_id: StringId,
958    pub start_time: GtfsTime,
959    pub end_time: GtfsTime,
960    pub headway_secs: u32,
961    pub exact_times: Option<ExactTimes>,
962}
963
964#[derive(Debug, Clone, Deserialize, Default)]
965pub struct Transfer {
966    pub from_stop_id: Option<StringId>,
967    pub to_stop_id: Option<StringId>,
968    pub transfer_type: Option<TransferType>,
969    pub min_transfer_time: Option<u32>,
970    pub from_route_id: Option<StringId>,
971    pub to_route_id: Option<StringId>,
972    pub from_trip_id: Option<StringId>,
973    pub to_trip_id: Option<StringId>,
974}
975
976#[derive(Debug, Clone, Deserialize, Default)]
977pub struct Area {
978    pub area_id: StringId,
979    pub area_name: Option<CompactString>,
980}
981
982#[derive(Debug, Clone, Deserialize, Default)]
983pub struct StopArea {
984    pub area_id: StringId,
985    pub stop_id: StringId,
986}
987
988#[derive(Debug, Clone, Deserialize, Default)]
989pub struct Timeframe {
990    pub timeframe_group_id: Option<StringId>,
991    pub start_time: Option<GtfsTime>,
992    pub end_time: Option<GtfsTime>,
993    pub service_id: StringId,
994}
995
996#[derive(Debug, Clone, Deserialize)]
997pub struct FareMedia {
998    pub fare_media_id: StringId,
999    pub fare_media_name: Option<CompactString>,
1000    pub fare_media_type: FareMediaType,
1001}
1002
1003impl Default for FareMedia {
1004    fn default() -> Self {
1005        Self {
1006            fare_media_id: StringId::default(),
1007            fare_media_name: None,
1008            fare_media_type: FareMediaType::NoneType,
1009        }
1010    }
1011}
1012
1013#[derive(Debug, Clone, Deserialize)]
1014pub struct FareProduct {
1015    pub fare_product_id: StringId,
1016    pub fare_product_name: Option<CompactString>,
1017    pub amount: f64,
1018    pub currency: StringId,
1019    pub fare_media_id: Option<StringId>,
1020    pub rider_category_id: Option<StringId>,
1021}
1022
1023impl Default for FareProduct {
1024    fn default() -> Self {
1025        Self {
1026            fare_product_id: StringId::default(),
1027            fare_product_name: None,
1028            amount: 0.0,
1029            currency: StringId::default(),
1030            fare_media_id: None,
1031            rider_category_id: None,
1032        }
1033    }
1034}
1035
1036#[derive(Debug, Clone, Deserialize, Default)]
1037pub struct FareLegRule {
1038    pub leg_group_id: Option<StringId>,
1039    pub network_id: Option<StringId>,
1040    pub from_area_id: Option<StringId>,
1041    pub to_area_id: Option<StringId>,
1042    pub from_timeframe_group_id: Option<StringId>,
1043    pub to_timeframe_group_id: Option<StringId>,
1044    pub fare_product_id: StringId,
1045    pub rule_priority: Option<u32>,
1046}
1047
1048#[derive(Debug, Clone, Deserialize)]
1049pub struct FareTransferRule {
1050    pub from_leg_group_id: Option<StringId>,
1051    pub to_leg_group_id: Option<StringId>,
1052    pub duration_limit: Option<i32>,
1053    pub duration_limit_type: Option<DurationLimitType>,
1054    pub fare_transfer_type: FareTransferType,
1055    pub transfer_count: Option<i32>,
1056    pub fare_product_id: Option<StringId>,
1057}
1058
1059impl Default for FareTransferRule {
1060    fn default() -> Self {
1061        Self {
1062            from_leg_group_id: None,
1063            to_leg_group_id: None,
1064            duration_limit: None,
1065            duration_limit_type: None,
1066            fare_transfer_type: FareTransferType::APlusAb,
1067            transfer_count: None,
1068            fare_product_id: None,
1069        }
1070    }
1071}
1072
1073#[derive(Debug, Clone, Deserialize, Default)]
1074pub struct FareLegJoinRule {
1075    pub from_network_id: StringId,
1076    pub to_network_id: StringId,
1077    pub from_stop_id: Option<StringId>,
1078    pub to_stop_id: Option<StringId>,
1079    pub from_area_id: Option<StringId>,
1080    pub to_area_id: Option<StringId>,
1081}
1082
1083#[derive(Debug, Clone, Deserialize, Default)]
1084pub struct RiderCategory {
1085    pub rider_category_id: StringId,
1086    pub rider_category_name: CompactString,
1087    #[serde(rename = "is_default_fare_category")]
1088    pub is_default_fare_category: Option<RiderFareCategory>,
1089    pub eligibility_url: Option<StringId>,
1090}
1091
1092#[derive(Debug, Clone, Deserialize, Default)]
1093pub struct LocationGroup {
1094    pub location_group_id: StringId,
1095    pub location_group_name: Option<CompactString>,
1096    pub location_group_desc: Option<CompactString>,
1097}
1098
1099#[derive(Debug, Clone, Deserialize, Default)]
1100pub struct LocationGroupStop {
1101    pub location_group_id: StringId,
1102    pub stop_id: StringId,
1103}
1104
1105#[derive(Debug, Clone, Deserialize, Default)]
1106pub struct Network {
1107    pub network_id: StringId,
1108    pub network_name: Option<CompactString>,
1109}
1110
1111#[derive(Debug, Clone, Deserialize, Default)]
1112pub struct RouteNetwork {
1113    pub route_id: StringId,
1114    pub network_id: StringId,
1115}
1116
1117#[derive(Debug, Clone, Deserialize)]
1118pub struct FeedInfo {
1119    pub feed_publisher_name: CompactString,
1120    pub feed_publisher_url: StringId,
1121    pub feed_lang: StringId,
1122    pub feed_start_date: Option<GtfsDate>,
1123    pub feed_end_date: Option<GtfsDate>,
1124    pub feed_version: Option<CompactString>,
1125    pub feed_contact_email: Option<CompactString>,
1126    pub feed_contact_url: Option<StringId>,
1127    pub default_lang: Option<StringId>,
1128}
1129
1130#[derive(Debug, Clone, Deserialize)]
1131pub struct Attribution {
1132    pub attribution_id: Option<StringId>,
1133    pub agency_id: Option<StringId>,
1134    pub route_id: Option<StringId>,
1135    pub trip_id: Option<StringId>,
1136    pub organization_name: StringId,
1137    pub is_producer: Option<YesNo>,
1138    pub is_operator: Option<YesNo>,
1139    pub is_authority: Option<YesNo>,
1140    pub attribution_url: Option<StringId>,
1141    pub attribution_email: Option<CompactString>,
1142    pub attribution_phone: Option<CompactString>,
1143}
1144
1145#[derive(Debug, Clone, Deserialize, Default)]
1146pub struct Level {
1147    pub level_id: StringId,
1148    pub level_index: f64,
1149    pub level_name: Option<CompactString>,
1150}
1151
1152#[derive(Debug, Clone, Deserialize, Default)]
1153pub struct Pathway {
1154    pub pathway_id: StringId,
1155    pub from_stop_id: StringId,
1156    pub to_stop_id: StringId,
1157    pub pathway_mode: PathwayMode,
1158    pub is_bidirectional: Bidirectional,
1159    pub length: Option<f64>,
1160    pub traversal_time: Option<u32>,
1161    pub stair_count: Option<u32>,
1162    pub max_slope: Option<f64>,
1163    pub min_width: Option<f64>,
1164    pub signposted_as: Option<CompactString>,
1165    pub reversed_signposted_as: Option<CompactString>,
1166}
1167
1168#[derive(Debug, Clone, Deserialize, Default)]
1169#[serde(default)]
1170pub struct Translation {
1171    pub table_name: Option<StringId>,
1172    pub field_name: Option<StringId>,
1173    #[serde(alias = "lang")]
1174    pub language: StringId,
1175    pub translation: CompactString,
1176    pub record_id: Option<StringId>,
1177    pub record_sub_id: Option<StringId>,
1178    #[serde(alias = "trans_id")]
1179    pub field_value: Option<CompactString>,
1180}
1181
1182#[cfg(test)]
1183mod tests {
1184    use super::*;
1185
1186    #[test]
1187    fn parses_gtfs_date() {
1188        let date = GtfsDate::parse("20240131").unwrap();
1189        assert_eq!(date.year(), 2024);
1190        assert_eq!(date.month(), 1);
1191        assert_eq!(date.day(), 31);
1192        assert_eq!(date.to_string(), "20240131");
1193    }
1194
1195    #[test]
1196    fn parses_gtfs_date_with_whitespace() {
1197        let date = GtfsDate::parse(" 20240131 ").unwrap();
1198        assert_eq!(date.to_string(), "20240131");
1199    }
1200
1201    #[test]
1202    fn rejects_invalid_date() {
1203        assert!(GtfsDate::parse("20240230").is_err());
1204        assert!(GtfsDate::parse("2024-01-01").is_err());
1205    }
1206
1207    #[test]
1208    fn parses_gtfs_time() {
1209        let time = GtfsTime::parse("25:10:05").unwrap();
1210        assert_eq!(time.total_seconds(), 25 * 3600 + 10 * 60 + 5);
1211        assert_eq!(time.to_string(), "25:10:05");
1212    }
1213
1214    #[test]
1215    fn parses_gtfs_time_with_whitespace() {
1216        let time = GtfsTime::parse(" 25:10:05 ").unwrap();
1217        assert_eq!(time.to_string(), "25:10:05");
1218    }
1219
1220    #[test]
1221    fn rejects_invalid_time() {
1222        assert!(GtfsTime::parse("25:99:00").is_err());
1223        assert!(GtfsTime::parse("bad").is_err());
1224    }
1225
1226    #[test]
1227    fn parses_gtfs_color() {
1228        let color = GtfsColor::parse("FF00AA").unwrap();
1229        assert_eq!(color.rgb(), 0xFF00AA);
1230        assert_eq!(color.to_string(), "FF00AA");
1231    }
1232
1233    #[test]
1234    fn parses_gtfs_color_with_whitespace() {
1235        let color = GtfsColor::parse(" ff00aa ").unwrap();
1236        assert_eq!(color.rgb(), 0xFF00AA);
1237    }
1238
1239    #[test]
1240    fn rejects_invalid_color() {
1241        assert!(GtfsColor::parse("GG00AA").is_err());
1242        assert!(GtfsColor::parse("12345").is_err());
1243    }
1244}