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}