From e78211b38c2699dd23b3c661f2ba7a908f567f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonah=20Br=C3=BCchert?= Date: Thu, 21 May 2026 18:01:23 +0200 Subject: [PATCH 1/5] Add support for Google Transit Ticketing extension --- src/enums.rs | 13 +++++++++++++ src/gtfs_reader.rs | 4 ++++ src/objects.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/raw_gtfs.rs | 6 ++++++ 4 files changed, 67 insertions(+) diff --git a/src/enums.rs b/src/enums.rs index ae869dc..533668d 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -657,3 +657,16 @@ impl<'de> Deserialize<'de> for DefaultFareCategory { }) } } + +/// Specifies whether tickets can be bought for this item +#[derive(Debug, Derivative, Deserialize, Serialize, Copy, Clone, PartialEq, Eq)] +#[derivative(Default)] +pub enum TicketingType { + /// If a ticketing_deep_link_id is set, tickets are available + #[derivative(Default)] + #[serde(rename = "0")] + Available, + /// Tickets are unavailable + #[serde(rename = "1")] + Unavailable, +} diff --git a/src/gtfs_reader.rs b/src/gtfs_reader.rs index f9e609d..376f1b7 100644 --- a/src/gtfs_reader.rs +++ b/src/gtfs_reader.rs @@ -187,6 +187,8 @@ impl RawGtfsReader { feed_info: self.read_objs_from_optional_path(p, "feed_info.txt"), read_duration: start_of_read_instant.elapsed(), translations: self.read_objs_from_optional_path(p, "translations.txt"), + ticketing_deep_links: self.read_objs_from_optional_path(p, "ticketing_deep_links.txt"), + ticketing_identifiers: self.read_objs_from_optional_path(p, "ticketing_identifiers.txt"), files, source_format: crate::SourceFormat::Directory, sha256: None, @@ -330,6 +332,8 @@ impl RawGtfsReader { Some(Ok(Vec::new())) }, translations: self.read_optional_file(&file_mapping, &mut archive, "translations.txt"), + ticketing_deep_links: self.read_optional_file(&file_mapping, &mut archive, "ticketing_deep_links.txt"), + ticketing_identifiers: self.read_optional_file(&file_mapping, &mut archive, "ticketing_identifiers.txt"), read_duration: start_of_read_instant.elapsed(), files, source_format: crate::SourceFormat::Zip, diff --git a/src/objects.rs b/src/objects.rs index 54d5b43..302ff39 100644 --- a/src/objects.rs +++ b/src/objects.rs @@ -261,6 +261,10 @@ pub struct RawStopTime { /// Indicates if arrival and departure times for a stop are strictly adhered to by the vehicle or if they are instead approximate and/or interpolated times #[serde(default)] pub timepoint: TimepointType, + /// This field is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension + /// Enable or disable buying tickets via a deep link + #[serde(default)] + pub ticketing_type: TicketingType, } /// The moment where a vehicle, running on [Trip] stops at a [Stop]. See @@ -360,6 +364,9 @@ pub struct Route { /// Indicates whether a rider can alight from the transit vehicle at any point along the vehicle’s travel path #[serde(default)] pub continuous_drop_off: ContinuousPickupDropOff, + /// This field is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension + /// Enable or disable buying tickets via a deep link + pub ticketing_deep_link_id: Option, } impl Route { @@ -445,6 +452,14 @@ pub struct RawTrip { /// Indicates whether bikes are allowed #[serde(default)] pub bikes_allowed: BikesAllowedType, + /// This field is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension + /// Trip ID to pass to a ticket shop + #[serde(default)] + pub ticketing_trip_id: Option, + /// This field is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension + /// Enable or disable buying tickets via a deep link + #[serde(default)] + pub ticketing_type: TicketingType, } impl Type for RawTrip { @@ -547,6 +562,9 @@ pub struct Agency { /// Email address actively monitored by the agency’s customer service department #[serde(rename = "agency_email")] pub email: Option, + /// This field is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension + /// Trip ID to pass to a ticket shop + pub ticketing_deep_link_id: Option, } impl Type for Agency { @@ -907,6 +925,32 @@ impl Id for RawPathway { } } +/// This object is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension +/// A mapping of the GTFS-identifiers to the ticket shop identifiers +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct TicketingIdentifier { + /// Stop ID used by the ticket shop + pub ticketing_stop_id: String, + /// GTFS-side stop id + pub stop_id: String, + /// GTFS-side agency id + pub agency_id: String, +} + +/// This object is not part of the main GTFS specification, it is part of the Google Transit Ticketing extension +/// The base url to a ticket shop without the trip specific parameters +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct TicketingDeepLink { + /// Unique identifier for this base link + pub ticketing_deep_link_id: String, + /// URL to be used for buying a ticket on the web + pub web_url: Option, + /// URI to be used for buying a ticket in an android app + pub android_intent_url: Option, + /// URL used to use on iOS + pub ios_universal_link_url: Option, +} + impl Type for RawPathway { fn object_type(&self) -> ObjectType { ObjectType::Pathway diff --git a/src/raw_gtfs.rs b/src/raw_gtfs.rs index 04a411b..8d91388 100644 --- a/src/raw_gtfs.rs +++ b/src/raw_gtfs.rs @@ -54,6 +54,10 @@ pub struct RawGtfs { pub sha256: Option, /// All translations, None if the file was absent as it is not mandatory pub translations: Option, Error>>, + /// Base urls to ticket shops + pub ticketing_deep_links: Option, Error>>, + /// Identifiers to pass to ticket shops + pub ticketing_identifiers: Option, Error>> } impl RawGtfs { @@ -79,6 +83,8 @@ impl RawGtfs { " Translations: {}", optional_file_summary(&self.translations) ); + println!(" Ticketing deep links: {}", optional_file_summary(&self.ticketing_deep_links)); + println!(" Ticketing identifiers: {}", optional_file_summary(&self.ticketing_identifiers)); } /// Reads from an url (if starts with http), or a local path (either a directory or zipped file) From aa48592c90c15f1a86b4b9cfd07de94f830ae8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonah=20Br=C3=BCchert?= Date: Fri, 22 May 2026 00:52:42 +0200 Subject: [PATCH 2/5] format --- src/gtfs_reader.rs | 15 ++++++++++++--- src/raw_gtfs.rs | 12 +++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/gtfs_reader.rs b/src/gtfs_reader.rs index 376f1b7..9986f61 100644 --- a/src/gtfs_reader.rs +++ b/src/gtfs_reader.rs @@ -188,7 +188,8 @@ impl RawGtfsReader { read_duration: start_of_read_instant.elapsed(), translations: self.read_objs_from_optional_path(p, "translations.txt"), ticketing_deep_links: self.read_objs_from_optional_path(p, "ticketing_deep_links.txt"), - ticketing_identifiers: self.read_objs_from_optional_path(p, "ticketing_identifiers.txt"), + ticketing_identifiers: self + .read_objs_from_optional_path(p, "ticketing_identifiers.txt"), files, source_format: crate::SourceFormat::Directory, sha256: None, @@ -332,8 +333,16 @@ impl RawGtfsReader { Some(Ok(Vec::new())) }, translations: self.read_optional_file(&file_mapping, &mut archive, "translations.txt"), - ticketing_deep_links: self.read_optional_file(&file_mapping, &mut archive, "ticketing_deep_links.txt"), - ticketing_identifiers: self.read_optional_file(&file_mapping, &mut archive, "ticketing_identifiers.txt"), + ticketing_deep_links: self.read_optional_file( + &file_mapping, + &mut archive, + "ticketing_deep_links.txt", + ), + ticketing_identifiers: self.read_optional_file( + &file_mapping, + &mut archive, + "ticketing_identifiers.txt", + ), read_duration: start_of_read_instant.elapsed(), files, source_format: crate::SourceFormat::Zip, diff --git a/src/raw_gtfs.rs b/src/raw_gtfs.rs index 8d91388..eaf37f5 100644 --- a/src/raw_gtfs.rs +++ b/src/raw_gtfs.rs @@ -57,7 +57,7 @@ pub struct RawGtfs { /// Base urls to ticket shops pub ticketing_deep_links: Option, Error>>, /// Identifiers to pass to ticket shops - pub ticketing_identifiers: Option, Error>> + pub ticketing_identifiers: Option, Error>>, } impl RawGtfs { @@ -83,8 +83,14 @@ impl RawGtfs { " Translations: {}", optional_file_summary(&self.translations) ); - println!(" Ticketing deep links: {}", optional_file_summary(&self.ticketing_deep_links)); - println!(" Ticketing identifiers: {}", optional_file_summary(&self.ticketing_identifiers)); + println!( + " Ticketing deep links: {}", + optional_file_summary(&self.ticketing_deep_links) + ); + println!( + " Ticketing identifiers: {}", + optional_file_summary(&self.ticketing_identifiers) + ); } /// Reads from an url (if starts with http), or a local path (either a directory or zipped file) From a4b8324691e936de654b94fd395b460bf1475296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonah=20Br=C3=BCchert?= Date: Fri, 22 May 2026 00:54:37 +0200 Subject: [PATCH 3/5] Make linter happy --- src/gtfs.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/gtfs.rs b/src/gtfs.rs index cf2a812..adc296d 100644 --- a/src/gtfs.rs +++ b/src/gtfs.rs @@ -358,8 +358,7 @@ fn create_trips( } for trip in &mut trips.values_mut() { - trip.stop_times - .sort_by(|a, b| a.stop_sequence.cmp(&b.stop_sequence)); + trip.stop_times.sort_by_key(|st| st.stop_sequence); } for f in raw_frequencies { From ae0383e4bc7ae1fdf254ea08d8fc753e3f85000e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonah=20Br=C3=BCchert?= Date: Tue, 2 Jun 2026 22:35:43 +0200 Subject: [PATCH 4/5] Add ticketing tests --- fixtures/ticketing-extension/agency.txt | 2 ++ fixtures/ticketing-extension/routes.txt | 3 ++ fixtures/ticketing-extension/stop_times.txt | 7 ++++ fixtures/ticketing-extension/stops.txt | 4 +++ .../ticketing_deep_links.txt | 2 ++ .../ticketing_identifiers.txt | 4 +++ fixtures/ticketing-extension/trips.txt | 3 ++ src/tests.rs | 36 +++++++++++++++++++ 8 files changed, 61 insertions(+) create mode 100644 fixtures/ticketing-extension/agency.txt create mode 100644 fixtures/ticketing-extension/routes.txt create mode 100644 fixtures/ticketing-extension/stop_times.txt create mode 100644 fixtures/ticketing-extension/stops.txt create mode 100644 fixtures/ticketing-extension/ticketing_deep_links.txt create mode 100644 fixtures/ticketing-extension/ticketing_identifiers.txt create mode 100644 fixtures/ticketing-extension/trips.txt diff --git a/fixtures/ticketing-extension/agency.txt b/fixtures/ticketing-extension/agency.txt new file mode 100644 index 0000000..ab3cded --- /dev/null +++ b/fixtures/ticketing-extension/agency.txt @@ -0,0 +1,2 @@ +agency_id,agency_name,agency_url,agency_timezone,ticketing_deep_link_id +agency,Agency,https://example.com,Etc/UTC,ticket-shop diff --git a/fixtures/ticketing-extension/routes.txt b/fixtures/ticketing-extension/routes.txt new file mode 100644 index 0000000..75509ab --- /dev/null +++ b/fixtures/ticketing-extension/routes.txt @@ -0,0 +1,3 @@ +route_id,route_short_name,route_long_name,route_desc,route_type,route_url,agency_id,ticketing_deep_link_id +route1,667,,,2,,agency,ticket-shop +route2,689,,,2,,agency, diff --git a/fixtures/ticketing-extension/stop_times.txt b/fixtures/ticketing-extension/stop_times.txt new file mode 100644 index 0000000..5f42fc1 --- /dev/null +++ b/fixtures/ticketing-extension/stop_times.txt @@ -0,0 +1,7 @@ +trip_id,arrival_time,departure_time,stop_id,stop_sequence,ticketing_type +trip1,13:57:00,13:57:00,stop1,0,0 +trip1,14:09:00,14:10:00,stop2,1,0 +trip1,14:17:00,14:18:00,stop3,2,0 +trip2,14:24:00,14:25:00,stop1,0,0 +trip2,14:34:00,14:35:00,stop2,1,1 +trip2,14:41:00,14:42:00,stop3,2,0 diff --git a/fixtures/ticketing-extension/stops.txt b/fixtures/ticketing-extension/stops.txt new file mode 100644 index 0000000..62e88c2 --- /dev/null +++ b/fixtures/ticketing-extension/stops.txt @@ -0,0 +1,4 @@ +stop_id,stop_name,stop_lon,stop_lat +stop1,Stop 1,1.0,2.0 +stop2,Stop 2,2.0,1.0 +stop3,Stop 2,4.0,2.0 diff --git a/fixtures/ticketing-extension/ticketing_deep_links.txt b/fixtures/ticketing-extension/ticketing_deep_links.txt new file mode 100644 index 0000000..27869f4 --- /dev/null +++ b/fixtures/ticketing-extension/ticketing_deep_links.txt @@ -0,0 +1,2 @@ +ticketing_deep_link_id,web_url,android_intent_url,ios_universal_link_url +ticket-shop,https://example.com/tickets,, diff --git a/fixtures/ticketing-extension/ticketing_identifiers.txt b/fixtures/ticketing-extension/ticketing_identifiers.txt new file mode 100644 index 0000000..321c799 --- /dev/null +++ b/fixtures/ticketing-extension/ticketing_identifiers.txt @@ -0,0 +1,4 @@ +ticketing_stop_id,stop_id,agency_id +ticketing-stop1,stop1,agency +ticketing-stop2,stop2,agency +ticketing-stop3,stop3,agency diff --git a/fixtures/ticketing-extension/trips.txt b/fixtures/ticketing-extension/trips.txt new file mode 100644 index 0000000..19d0e4c --- /dev/null +++ b/fixtures/ticketing-extension/trips.txt @@ -0,0 +1,3 @@ +trip_id,service_id,route_id,shape_id,trip_headsign,trip_short_name,direction_id,block_id,wheelchair_accessible,bikes_allowed,ticketing_trip_id,ticketing_type +trip1,service1,route1,,,,,,0,1,test,0 +trip2,service1,route2,,,,,,0,1,,0 diff --git a/src/tests.rs b/src/tests.rs index bfeecab..6639d3b 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -561,3 +561,39 @@ fn fares_v2() { assert_eq!(gtfs.rider_categories.len(), 2); assert_eq!(gtfs.rider_categories["concession"], expected); } + +#[test] +fn ticketing_extension() { + let gtfs = RawGtfs::from_path("fixtures/ticketing-extension").expect("impossible to read gtfs"); + + assert_eq!(gtfs.ticketing_deep_links.unwrap().unwrap().len(), 1); + assert_eq!(gtfs.ticketing_identifiers.unwrap().unwrap().len(), 3); + assert_eq!(gtfs.routes.as_ref().unwrap().len(), 2); + assert_eq!(gtfs.trips.unwrap().len(), 2); + assert_eq!(gtfs.agencies.as_ref().unwrap().len(), 1); + assert!( + gtfs.agencies + .unwrap() + .first() + .unwrap() + .ticketing_deep_link_id + .is_some() + ); + assert!( + gtfs.routes + .as_ref() + .unwrap() + .last() + .unwrap() + .ticketing_deep_link_id + .is_none() + ); + assert!( + gtfs.routes + .unwrap() + .first() + .unwrap() + .ticketing_deep_link_id + .is_some() + ); +} From 5d13bd29241f6d11ce66a3d6ad585946399f3af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonah=20Br=C3=BCchert?= Date: Thu, 4 Jun 2026 23:22:49 +0200 Subject: [PATCH 5/5] Continue with 0.48.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 40958a3..29b682e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] description = "Read GTFS (public transit timetables) files" name = "gtfs-structures" -version = "0.47.0" +version = "0.48.0" authors = [ "Tristram Gräbener ", "Antoine Desbordes ",