gistools/readers/gbfs/mod.rs
1mod schema_v1;
2mod schema_v2;
3mod schema_v3;
4
5use crate::{
6 readers::{FeatureReader, parse_csv_as_btree},
7 util::fetch_url,
8};
9use alloc::{boxed::Box, format, string::String, vec, vec::Vec};
10use s2json::{MValue, Properties, ValuePrimitive, VectorFeature};
11pub use schema_v1::*;
12pub use schema_v2::*;
13pub use schema_v3::*;
14use serde::{Deserialize, Deserializer, Serialize};
15
16/// Contains rental URIs for Android, iOS, and web (added in v1.1).
17#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, MValue, ValuePrimitive)]
18pub struct GBFSRentalUri {
19 /// URI that can be passed to an Android app with an intent (added in v1.1).
20 /// **Format**: URI
21 pub android: Option<String>,
22 /// URI that can be used on iOS to launch the rental app for this vehicle (added in v1.1).
23 /// **Format**: URI
24 pub ios: Option<String>,
25 /// URL that can be used by a web browser to show more information about renting this vehicle (added in v1.1).
26 /// **Format**: URI
27 pub web: Option<String>,
28}
29
30/// GBFS name
31#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, MValue, ValuePrimitive)]
32pub struct GBFSName {
33 /// The translated text.
34 pub text: String,
35 /// IETF BCP 47 language code.
36 /// **pattern** ^[a-z]{2,3}(-[A-Z]{2})?$
37 pub language: String,
38}
39
40/// GBFS Versions
41#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
42pub struct GBFSVersion {
43 /// The semantic version of the feed in the form X.Y.
44 /// **Enum**: "1.0", "1.1", "2.0", "2.1", "2.2", "2.3", "3.0"
45 pub version: String,
46 /// URL of the corresponding gbfs.json endpoint.
47 /// **Format**: uri
48 pub url: String,
49}
50
51/// # General Bikeshare Feed Specification (GBFS) Reader
52///
53/// ## Description
54/// The versions of GBFS reader classes this data could be (1, 2, or 3)
55///
56/// Implements the [`FeatureReader`] interface.
57///
58/// ## Usage
59///
60/// If you want to know what datasets are available to you, you can get started with
61/// [`parse_gtfs_systems_from_url`].
62///
63/// If you want to build from a URL,
64/// See [`build_gbfs_reader`] to build a GBFSReader or use [`GBFSReader::from_url`]
65///
66/// ```rust
67/// // TODO
68/// ```
69///
70/// ## Links
71/// - https://github.com/MobilityData/gbfs
72/// - https://github.com/MobilityData/gbfs-json-schema/tree/master/v3.0
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
74#[serde(untagged)]
75pub enum GBFSReader {
76 /// GBFS V1 Reader
77 V1(Box<GBFSReaderV1>),
78 /// GBFS V2 Reader
79 V2(Box<GBFSReaderV2>),
80 /// GBFS V3 Reader
81 V3(Box<GBFSReaderV3>),
82}
83impl GBFSReader {
84 /// Build a GBFSReader from a URL. See [`build_gbfs_reader`]
85 pub async fn from_url(url: &str, locale: Option<String>) -> GBFSReader {
86 build_gbfs_reader(url, locale).await
87 }
88}
89/// A feature reader trait with a callback-based approach
90/// The GBFS V1 Iterator tool
91#[derive(Debug)]
92pub struct GBFSIterator {
93 features: Vec<VectorFeature>,
94 index: usize,
95 len: usize,
96}
97impl Iterator for GBFSIterator {
98 type Item = VectorFeature;
99
100 fn next(&mut self) -> Option<Self::Item> {
101 if self.index >= self.len {
102 return None;
103 }
104 self.index += 1;
105 self.features.get(self.index - 1).cloned()
106 }
107}
108/// A feature reader trait with a callback-based approach
109impl FeatureReader<(), Properties, MValue> for GBFSReader {
110 type FeatureIterator<'a> = GBFSIterator;
111
112 fn iter(&self) -> Self::FeatureIterator<'_> {
113 let features: Vec<VectorFeature> = match self {
114 GBFSReader::V1(reader) => reader.features(),
115 GBFSReader::V2(reader) => reader.features(),
116 GBFSReader::V3(reader) => reader.features(),
117 };
118 let len = features.len();
119 GBFSIterator { features, index: 0, len }
120 }
121
122 fn par_iter(&self, pool_size: usize, thread_id: usize) -> Self::FeatureIterator<'_> {
123 let features: Vec<VectorFeature> = match self {
124 GBFSReader::V1(reader) => reader.features(),
125 GBFSReader::V2(reader) => reader.features(),
126 GBFSReader::V3(reader) => reader.features(),
127 };
128 let start = features.len() * thread_id / pool_size;
129 let end = features.len() * (thread_id + 1) / pool_size;
130 GBFSIterator { features, index: start, len: end }
131 }
132}
133
134/// # General Bikeshare Feed Specification (GBFS) Schema
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
136#[serde(untagged)]
137pub enum GBFSSchema {
138 /// GBFS V1
139 V1(GBFSV1),
140 /// GBFS V2
141 V2(GBFSV2),
142 /// GBFS V3
143 V3(GBFSV3),
144}
145
146/// # General Bikeshare Feed Specification (GBFS) Reader
147///
148/// ## Description
149/// Given a link to a GBFS feed, build the appropriate reader for the feed.
150/// The versions of GBFS reader classes this data could be (1, 2, or 3).
151///
152/// Implements the [`FeatureReader`] interface.
153///
154/// ## Usage
155///
156/// You can read more about the spec and reader from the [`GBFSReader`] struct.
157///
158/// ```rust
159/// // TODO
160/// ```
161///
162/// ## Links
163/// - https://github.com/MobilityData/gbfs
164/// - https://github.com/MobilityData/gbfs-json-schema/tree/master/v3.0
165/// - v3 example data: https://backend.citiz.fr/public/provider/9/gbfs/v3.0/gbfs.json
166/// - v2 example data: https://gbfs.helbiz.com/v2.2/durham/gbfs.json
167/// - v1 example data: https://gbfs.urbansharing.com/gbfs/gbfs.json
168///
169/// ## Parameters
170/// - `url`: The link to the GBFS feed
171/// - `locale`: The locale to use if provided, otherwise default to "en" (e.g., "en", "en-US").
172///
173/// ## Returns
174/// A GBFSReader of the appropriate version
175pub async fn build_gbfs_reader(url: &str, locale: Option<String>) -> GBFSReader {
176 let data = fetch_url::<()>(url, &[], None, None).await.unwrap();
177 let schema = serde_json::from_slice::<GBFSSchema>(&data).unwrap();
178
179 let mut path = None;
180 if url.contains("localhost") || url.contains("0.0.0.0") || url.contains("127.0.0.1") {
181 let mut parts: Vec<&str> = url.split('/').collect();
182 parts.pop();
183 path = Some(parts.join("/"));
184 }
185
186 match schema {
187 GBFSSchema::V1(v1) => GBFSReader::V1(build_gbfs_reader_v1(&v1, locale, path).await.into()),
188 GBFSSchema::V2(v2) => GBFSReader::V2(build_gbfs_reader_v2(&v2, locale, path).await.into()),
189 GBFSSchema::V3(v3) => GBFSReader::V3(build_gbfs_reader_v3(&v3, locale, path).await.into()),
190 }
191}
192
193/// System Definition that is returned from the github CSV file.
194#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
195pub struct GBFSSystem {
196 /// [**Required**] ISO 3166-1 alpha-2 code designating the country where the system is located.
197 #[serde(rename = "countryCode")]
198 pub country_code: String,
199 /// [**Required**] Name of the mobility system. This MUST match the name field in `system_information.json`
200 pub name: String,
201 /// [**Required**] Primary city in which the system is located, followed by the 2-letter state code
202 /// for US systems. The location name SHOULD be in English if the location has an English name
203 /// (e.g.: Brussels).
204 pub location: String,
205 /// [**Required**] ID for the system. This MUST match the system_id field in `system_information.json`.
206 #[serde(rename = "systemId")]
207 pub system_id: String,
208 /// [**Required**] URL for the system from the url field in `system_information.json`.
209 /// If the url field is not included in `system_information.json` this SHOULD be the primary URL
210 /// for the system operator.
211 pub url: String,
212 /// [**Required**] URL for the system's gbfs.json auto-discovery file.
213 #[serde(rename = "autoDiscoveryUrl")]
214 pub auto_discovery_url: String,
215 /// [**Required**] List of GBFS version(s) under which the feed is published. Multiple values are
216 /// separated by a semi-colon surrounded with 1 space on each side for readability (" ; ").
217 #[serde(rename = "supportedVersions")]
218 pub supported_versions: Vec<String>,
219 /// [**Conditionally Required**] If authentication is required, this MUST contain a URL to a
220 /// human-readable page describing how the authentication should be performed and how credentials
221 /// can be created, or directly contain the public key-value pair to append to the feed URLs.
222 #[serde(rename = "authInfo")]
223 pub auth_info: Option<String>,
224}
225
226/// # General Bikeshare Feed Specification (GBFS) Reader
227///
228/// ## Description
229/// Fetches the list of GBFS systems from the github CSV file
230///
231/// If you don't provide a url, it will default to
232/// <https://raw.githubusercontent.com/MobilityData/gbfs/refs/heads/master/systems.csv>
233/// which the spec managers keep updated with the latest systems
234///
235/// ## Usage
236///
237/// ```rust
238/// // TODO
239/// ```
240///
241/// ## Links
242/// - https://github.com/MobilityData/gbfs/blob/master/systems.csv
243///
244/// ## Parameters
245/// - `url`: The link to the GBFS feed.
246///
247/// ## Returns
248/// An array of systems
249pub async fn parse_gtfs_systems_from_url(url: Option<String>) -> Vec<GBFSSystem> {
250 let url = url.unwrap_or(
251 "https://raw.githubusercontent.com/MobilityData/gbfs/refs/heads/master/systems.csv".into(),
252 );
253 let data = fetch_url::<()>(&url, &[], None, None).await.unwrap();
254 parse_gtfs_systems(String::from_utf8_lossy(&data).as_ref())
255}
256
257/// # General Bikeshare Feed Specification (GBFS) Reader
258///
259/// ## Description
260/// Fetches the list of GBFS systems from the github CSV file
261///
262/// ## Usage
263///
264/// ```rust
265/// use gistools::readers::{GBFSSystem, parse_gtfs_systems};
266/// use std::{fs, path::PathBuf};
267///
268/// let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
269/// path.push("tests/readers/gbfs/fixtures/systems.csv");
270/// let file_str = fs::read_to_string(path).unwrap();
271///
272/// let systems = parse_gtfs_systems(file_str.as_str());
273/// assert_eq!(systems.len(), 960);
274/// ```
275///
276/// ## Links
277/// - https://github.com/MobilityData/gbfs/blob/master/systems.csv
278///
279/// ## Parameters
280/// - `systems_csv`: The data of the CSV file as a string. The default is the one used by GBFS.
281/// This variable exists for testing
282///
283/// ## Returns
284/// An array of systems
285pub fn parse_gtfs_systems(systems_csv: &str) -> Vec<GBFSSystem> {
286 let mut res = vec![];
287 let parsed = parse_csv_as_btree(systems_csv, None, None);
288
289 for system in parsed {
290 let name = system.get("Name").cloned().unwrap_or_default();
291 let location = system.get("Location").cloned().unwrap_or_default();
292 let url = system.get("URL").cloned().unwrap_or_default();
293 let country_code = system.get("Country Code").cloned().unwrap_or_default();
294 let system_id = system.get("System ID").cloned().unwrap_or_default();
295 let auto_discovery_url = system.get("Auto-Discovery URL").cloned().unwrap_or_default();
296 let supported_versions = system.get("Supported Versions").cloned().unwrap_or_default();
297 let supported_versions: Vec<String> =
298 supported_versions.split(" ; ").map(|v| v.trim().into()).collect();
299 let auth_info = system.get("Authentication Info").cloned();
300 res.push(GBFSSystem {
301 name,
302 location,
303 url,
304 country_code,
305 system_id,
306 auto_discovery_url,
307 supported_versions,
308 auth_info,
309 });
310 }
311
312 res
313}
314
315/// Converts a boolean or integer 0/1 to a boolean
316pub fn gbfs_bool_or_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
317where
318 D: Deserializer<'de>,
319{
320 struct BoolOrIntVisitor;
321
322 impl<'de> serde::de::Visitor<'de> for BoolOrIntVisitor {
323 type Value = bool;
324
325 fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
326 formatter.write_str("a boolean or an integer 0/1")
327 }
328
329 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
330 Ok(value)
331 }
332
333 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
334 where
335 E: serde::de::Error,
336 {
337 match value {
338 0 => Ok(false),
339 1 => Ok(true),
340 _ => Err(E::custom("expected 0 or 1 for a boolean")),
341 }
342 }
343
344 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
345 where
346 E: serde::de::Error,
347 {
348 self.visit_u64(value as u64)
349 }
350
351 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
352 where
353 E: serde::de::Error,
354 {
355 match value {
356 "0" => Ok(false),
357 "1" => Ok(true),
358 "true" => Ok(true),
359 "false" => Ok(false),
360 _ => Err(E::custom(format!("invalid boolean string: {value}"))),
361 }
362 }
363 }
364
365 deserializer.deserialize_any(BoolOrIntVisitor)
366}