amtrak_api/
responses.rs

1use std::{collections::HashMap, fmt};
2
3use chrono::{DateTime, FixedOffset};
4use serde::{de, Deserialize};
5
6/// The response from the `/trains` or `/trains/{:train_id}` endpoint.
7///
8/// Each key in the hashmap is the string representation of the
9/// [`train_num`] field. The value is a list of trains that have the
10/// specified [`train_num`] field. I have not seen an instance where
11/// multiple trains have the same [`train_num`] and therefore each list
12/// in the map has only one item. It is possible for multiple trains to
13/// have the same [`train_num`] so that case must be handled in the
14/// client code.
15///
16/// [`train_num`]: Train::train_num
17pub type TrainResponse = HashMap<String, Vec<Train>>;
18
19/// The response from the `/trains` or `/trains/{:train_id}` endpoint.
20///
21/// We have to wrap this in a structure so that we can implement the
22/// Deserialize trait for it.
23#[derive(Debug, Clone)]
24pub(crate) struct TrainResponseWrapper(
25    /// The actual response from the Amtrak API
26    pub(crate) TrainResponse,
27);
28
29impl<'de> Deserialize<'de> for TrainResponseWrapper {
30    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
31    where
32        D: de::Deserializer<'de>,
33    {
34        deserializer.deserialize_any(TrainResponseWrapperVisitor)
35    }
36}
37
38/// Custom visitor used to deserialize responses from the `/trains` or
39/// `/trains/{:train_id}` endpoint.
40///
41/// On empty data the Amtrak API will serialize an empty vector as `[]`. On
42/// normal content responses, the API will instead serialize a dictionary using
43/// `{"key1", "<content>"}`. This does not place nicely with serde which
44/// (rightfully) expects the type to be the same for every endpoint response. To
45/// handle this discrepancy, we implement our own visitor which will handle both
46/// response.
47struct TrainResponseWrapperVisitor;
48
49impl<'de> de::Visitor<'de> for TrainResponseWrapperVisitor {
50    type Value = TrainResponseWrapper;
51
52    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
53        formatter.write_str("a HashMap or an empty array")
54    }
55
56    fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
57    where
58        A: de::MapAccess<'de>,
59    {
60        Ok(TrainResponseWrapper(Deserialize::deserialize(
61            de::value::MapAccessDeserializer::new(map),
62        )?))
63    }
64
65    fn visit_seq<A>(self, _seq: A) -> Result<Self::Value, A::Error>
66    where
67        A: de::SeqAccess<'de>,
68    {
69        Ok(TrainResponseWrapper(HashMap::new()))
70    }
71}
72
73/// Represents an Amtrak train
74#[derive(Debug, Deserialize, Clone)]
75pub struct Train {
76    /// The human readable route name of this train.
77    ///
78    /// # Examples:
79    /// * "Keystone" (for the Keystone Corridor)
80    /// * "Northeast Regional" (for the Northeast Corridor)
81    #[serde(rename = "routeName")]
82    pub route_name: String,
83
84    /// The (possible unique) number identifying the train.
85    #[serde(rename = "trainNum")]
86    pub train_num: String,
87
88    /// The concatenation of the [`train_num`] with another number (not sure
89    /// what exactly) in the format "{:train_num}-{:instance}".
90    ///
91    /// # Examples:
92    /// * `6-4`
93    /// * `93-4`
94    ///
95    /// [`train_num`]: Self::train_num
96    #[serde(rename = "trainID")]
97    pub train_id: String,
98
99    /// The current latitude of the train
100    pub lat: f64,
101
102    /// The current longitude of the train
103    pub lon: f64,
104
105    /// The human readable status of the timelyness of the train.
106    ///
107    /// # Examples:
108    /// * `X Minutes Early`
109    /// * `X Hours, Y Minutes Early`
110    /// * `X Minutes Late`
111    /// * `X Hours, Y Minutes Late`
112    /// * `On Time`
113    /// * `Unknown`
114    /// * `NaN Minutes Early` (yes really)
115    #[serde(rename = "trainTimely")]
116    pub train_timely: String,
117
118    /// List of stations that the train will visit. The stations are listed in
119    /// the same order the train will stop at each.
120    pub stations: Vec<TrainStation>,
121
122    /// The current compass heading of the train.
123    pub heading: Heading,
124
125    /// Unsure of what this field symbolizes.
126    #[serde(rename = "eventCode")]
127    pub event_code: String,
128
129    /// Unsure of what this field symbolizes.
130    #[serde(rename = "eventTZ")]
131    pub event_tz: Option<String>,
132
133    /// Unsure of what this field symbolizes.
134    #[serde(rename = "eventName")]
135    pub event_name: Option<String>,
136
137    /// The station code where the train originated from (aka the first
138    /// station in this train's route).
139    ///
140    /// # Examples:
141    /// * `PHL`
142    /// * `NYP`
143    #[serde(rename = "origCode")]
144    pub origin_code: String,
145
146    /// The timezone of the original station
147    ///
148    /// # Examples:
149    /// * `America/New_York`
150    /// * `America/Chicago`
151    #[serde(rename = "originTZ")]
152    pub origin_tz: String,
153
154    /// The full human readable name of the station where the train originated
155    /// from (aka the first station in this train's route).
156    ///
157    /// # Examples:
158    /// * `Philadelphia 30th Street`
159    /// * `New York Penn`
160    #[serde(rename = "origName")]
161    pub origin_name: String,
162
163    /// The station code where the train is heading to (aka the final
164    /// destination of the train).
165    ///
166    /// # Examples:
167    /// * `PHL`
168    /// * `NYP`
169    #[serde(rename = "destCode")]
170    pub destination_code: String,
171
172    /// The timezone of destination station
173    ///
174    /// # Examples:
175    /// * `America/New_York`
176    /// * `America/Chicago`
177    #[serde(rename = "destTZ")]
178    pub destination_tz: String,
179
180    /// The full human readable name of the station where the train is heading
181    /// (aka the final destination of the train).
182    ///
183    /// # Examples:
184    /// * `Philadelphia 30th Street`
185    /// * `New York Penn`
186    #[serde(rename = "destName")]
187    pub destination_name: String,
188
189    /// The current state of the train
190    #[serde(rename = "trainState")]
191    pub train_state: TrainState,
192
193    /// The current velocity (in mph) of the train
194    pub velocity: f32,
195
196    /// A human readable status message.
197    ///
198    /// # Examples:
199    /// * ` ` (Empty string, single whitespace)
200    /// * `SERVICE DISRUPTION`
201    #[serde(rename = "statusMsg")]
202    pub status_message: String,
203
204    /// The time at which this train entry was created. The entry will have the
205    /// local timezone as a fixed offset.
206    ///
207    /// # Examples:
208    /// * `2023-09-04T07:46:06-04:00`
209    /// * `2023-09-04T07:00:00-05:00`
210    #[serde(rename = "createdAt")]
211    pub created_at: DateTime<FixedOffset>,
212
213    /// The time at which this train entry was last updated. The entry will have
214    /// the local timezone as a fixed offset.
215    ///
216    /// # Examples:
217    /// * `2023-09-04T07:46:06-04:00`
218    /// * `2023-09-04T07:00:00-05:00`
219    #[serde(rename = "updatedAt")]
220    pub updated_at: DateTime<FixedOffset>,
221
222    /// Unsure of what this field symbolizes.
223    #[serde(rename = "lastValTS")]
224    pub last_value: DateTime<FixedOffset>,
225
226    /// Unsure of what this field symbolizes.
227    #[serde(rename = "objectID")]
228    pub object_id: u32,
229}
230
231#[derive(Debug, Deserialize, Clone)]
232pub struct TrainStation {
233    /// The full human readable name of the station.
234    ///
235    /// # Examples:
236    /// * `Philadelphia 30th Street`
237    /// * `New York Penn`
238    pub name: String,
239
240    /// The unique identification code of this station.
241    ///
242    /// # Examples:
243    /// * `PHL`
244    /// * `NYP`
245    pub code: String,
246
247    /// The timezone of this station.
248    pub tz: String,
249    pub bus: bool,
250
251    /// The scheduled arrival time of this train for the current station.
252    #[serde(rename = "schArr")]
253    pub schedule_arrival: DateTime<FixedOffset>,
254
255    /// The scheduled departure time of this train for the current station.
256    #[serde(rename = "schDep")]
257    pub schedule_departure: DateTime<FixedOffset>,
258
259    /// The actual arrival time of this train for the current station specified
260    /// by [`name`] or [`code`]. When the [`status`] is [`Departed`] this
261    /// field shows a historical value of how late or early the train
262    /// arrived. When the [`status`] is [`Enroute`] this field is a
263    /// prediction on how late or early the train will arrive.
264    ///
265    /// Examples:
266    /// `2023-09-05T16:22:00-05:00`
267    /// `2023-09-05T15:54:00-05:00`
268    /// `null` or not included in response
269    ///
270    /// [`name`]: Self::name
271    /// [`code`]: Self::code
272    /// [`status`]: Self::status
273    /// [`Departed`]: TrainStatus::Departed
274    /// [`Enroute`]: TrainStatus::Enroute
275    #[serde(rename = "arr", default)]
276    pub arrival: Option<DateTime<FixedOffset>>,
277
278    /// The actual departure time of this train for the current station
279    /// specified by [`name`] or [`code`]. When the [`status`] is [`Departed`]
280    /// this field shows a historical value of how late or early the train
281    /// departed. When the [`status`] is [`Enroute`] this field is a
282    /// prediction on how late or early the train will depart.
283    ///
284    /// Examples:
285    /// `2023-09-05T16:22:00-05:00`
286    /// `2023-09-05T15:54:00-05:00`
287    /// `null` or not included in response
288    ///
289    /// [`name`]: Self::name
290    /// [`code`]: Self::code
291    /// [`status`]: Self::status
292    /// [`Departed`]: TrainStatus::Departed
293    /// [`Enroute`]: TrainStatus::Enroute
294    #[serde(rename = "dep", default)]
295    pub departure: Option<DateTime<FixedOffset>>,
296
297    /// A human readable comment on the arrival time of this train for current
298    /// station specified by [`name`] or [`code`]. When the [`status`] is
299    /// [`Departed`] this field shows a historical value of how late or
300    /// early the train arrived. When the [`status`] is [`Enroute`] this
301    /// field is a prediction on how late or early the train will arrive.
302    ///
303    /// Examples:
304    /// `19 Minutes Late`
305    /// `On Time`
306    /// `NaN Minutes Early` (Yes really)
307    ///
308    /// [`name`]: Self::name
309    /// [`code`]: Self::code
310    /// [`status`]: Self::status
311    /// [`Departed`]: TrainStatus::Departed
312    /// [`Enroute`]: TrainStatus::Enroute
313    #[serde(rename = "arrCmnt")]
314    pub arrival_comment: String,
315
316    /// A human readable comment on the departure time of this train for the
317    /// current station specified by [`name`] or [`code`]. When the
318    /// [`status`] is [`Departed`] this field shows a historical value of
319    /// how late or early the train departed. When the [`status`] is
320    /// [`Enroute`] this field is a prediction on how late or early the
321    /// train will depart.
322    ///
323    /// Examples:
324    /// `19 Minutes Late`
325    /// `On Time`
326    /// `NaN Minutes Early` (Yes really)
327    ///
328    /// [`name`]: Self::name
329    /// [`code`]: Self::code
330    /// [`status`]: Self::status
331    /// [`Departed`]: TrainStatus::Departed
332    /// [`Enroute`]: TrainStatus::Enroute
333    #[serde(rename = "depCmnt")]
334    pub departure_comment: String,
335
336    /// The current status of this train for the current station specified by
337    /// [`name`] or [`code`].
338    pub status: TrainStatus,
339}
340
341/// Describes a train's heading using cardinal directions
342#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
343pub enum Heading {
344    /// North heading
345    N,
346
347    /// Northeast heading
348    NE,
349
350    /// East heading
351    E,
352
353    /// Southeast heading
354    SE,
355
356    /// South heading
357    S,
358
359    /// Southwest heading
360    SW,
361
362    /// West heading
363    W,
364
365    /// Northwest heading
366    NW,
367}
368
369/// Represents the current status of an Amtrak train being tracked in
370/// association with a [`Station`].
371///
372/// This status can only be applied to a combination of a [`Train`] and a
373/// [`Station`]. It is referenced in the [`stations`] field.
374///
375/// [`Station`]: Station
376/// [`Train`]: Train
377/// [`stations`]: Train::stations
378#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
379pub enum TrainStatus {
380    /// The train has not yet arrived at the specified station.
381    Enroute,
382
383    /// The train is currently at the specified station.
384    Station,
385
386    /// The train has already arrived at departed from teh specified station.
387    Departed,
388
389    /// The status of the train is unknown
390    Unknown,
391}
392
393#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
394pub enum TrainState {
395    /// The train is awaiting departure from its origin station
396    Predeparture,
397
398    /// The train is currently on its route.
399    Active,
400
401    /// The train has completed its journey is not longer servicing its route.
402    Completed,
403}
404
405/// The response from the `/stations` or `/stations/{:station_code}` endpoint.
406///
407/// Each key in the hashmap is the unique station code which will match the
408/// [`code`] field. The value is the [`Station`] structure that is
409/// associated with the unique station [`code`].
410///
411/// [`code`]: Station::code
412/// [`Station`]: Station
413pub type StationResponse = HashMap<String, Station>;
414
415/// The response from the `/stations` or `/stations/{:station_code}` endpoint.
416///
417/// We have to wrap this in a structure so that we can implement the
418/// Deserialize trait for it.
419#[derive(Debug, Clone)]
420pub(crate) struct StationResponseWrapper(
421    /// The actual response from the Amtrak API
422    pub(crate) HashMap<String, Station>,
423);
424
425impl<'de> Deserialize<'de> for StationResponseWrapper {
426    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
427    where
428        D: de::Deserializer<'de>,
429    {
430        deserializer.deserialize_any(StationResponseWrapperVisitor)
431    }
432}
433
434/// Custom visitor used to deserialize responses from the `/stations` or
435/// `/stations/{:station_code}` endpoint.
436///
437/// On empty data the Amtrak API will serialize an empty vector as `[]`. On
438/// normal content responses, the API will instead serialize a dictionary using
439/// `{"key1", "<content>"}`. This does not place nicely with serde which
440/// (rightfully) expects the type to be the same for every endpoint response. To
441/// handle this discrepancy, we implement our own visitor which will handle both
442/// response.
443struct StationResponseWrapperVisitor;
444
445impl<'de> de::Visitor<'de> for StationResponseWrapperVisitor {
446    type Value = StationResponseWrapper;
447
448    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
449        formatter.write_str("a HashMap or an empty array")
450    }
451
452    fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
453    where
454        A: de::MapAccess<'de>,
455    {
456        Ok(StationResponseWrapper(Deserialize::deserialize(
457            de::value::MapAccessDeserializer::new(map),
458        )?))
459    }
460
461    fn visit_seq<A>(self, _seq: A) -> Result<Self::Value, A::Error>
462    where
463        A: de::SeqAccess<'de>,
464    {
465        Ok(StationResponseWrapper(HashMap::new()))
466    }
467}
468
469/// Represents a unique station that Amtrak services
470#[derive(Debug, Deserialize, Clone)]
471pub struct Station {
472    /// The full human readable name of the station.
473    ///
474    /// # Examples:
475    /// * `Philadelphia 30th Street`
476    /// * `New York Penn`
477    #[serde(default)]
478    pub name: String,
479
480    /// The unique identification code of this station.
481    ///
482    /// # Examples:
483    /// * `PHL`
484    /// * `NYP`
485    pub code: String,
486
487    /// The timezone of the station
488    ///
489    /// # Examples:
490    /// * `America/New_York`
491    /// * `America/Chicago`
492    #[serde(default)]
493    pub tz: String,
494
495    /// The latitude of the station
496    pub lat: f64,
497
498    /// The longitude of the station
499    pub lon: f64,
500
501    /// The first address line of the stations
502    ///
503    /// # Examples:
504    /// * `2955 Market Street`
505    /// * `351 West 31st Street`
506    pub address1: String,
507
508    /// The second address line of the station
509    pub address2: String,
510
511    /// The city of the station
512    ///
513    /// # Examples:
514    /// * `Philadelphia`
515    /// * `New York`
516    pub city: String,
517
518    /// The two character abbreviation of the state of the station
519    ///
520    /// # Examples:
521    /// * `PA`
522    /// * `NY`
523    pub state: String,
524
525    /// The zip code of the station
526    ///
527    /// # Examples:
528    /// * `19104`
529    /// * `10001`
530    pub zip: u32,
531
532    /// A list of current [`train_id`] that have departed from or are enroute to
533    /// this station.
534    ///
535    /// [`train_id`]: Train::train_id
536    pub trains: Vec<String>,
537}