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}