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