Handle errors from the API more gracefully

This commit is contained in:
Joey Hines 2025-05-23 17:32:13 -06:00
parent 6f4c408a26
commit 353eb865db
Signed by: joeyahines
GPG Key ID: 38BA6F25C94C9382
7 changed files with 50 additions and 10 deletions

2
Cargo.lock generated
View File

@ -101,7 +101,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cta-api" name = "cta-api"
version = "0.2.0" version = "0.3.0"
dependencies = [ dependencies = [
"reqwest", "reqwest",
"serde", "serde",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cta-api" name = "cta-api"
version = "0.2.0" version = "0.3.0"
edition = "2024" edition = "2024"
description = "CTA API Client" description = "CTA API Client"
authors = ["Joey Hines joey@ahines.net"] authors = ["Joey Hines joey@ahines.net"]

View File

@ -1,7 +1,7 @@
use crate::models::Response;
use crate::models::eta::{EtaRequest, EtaResp}; use crate::models::eta::{EtaRequest, EtaResp};
use crate::models::follow::{FollowRequest, FollowResp}; use crate::models::follow::{FollowRequest, FollowResp};
use crate::models::route::{RouteRequest, RouteResp}; use crate::models::route::{RouteRequest, RouteResp};
use crate::models::{CTAResponse, Response};
use reqwest::{Client, Url}; use reqwest::{Client, Url};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
@ -18,6 +18,8 @@ pub enum Error {
ReqwestFailure(#[from] reqwest::Error), ReqwestFailure(#[from] reqwest::Error),
#[error("URL parse failure: {0}")] #[error("URL parse failure: {0}")]
UrlParseError(#[from] url::ParseError), UrlParseError(#[from] url::ParseError),
#[error("Got error number '{0}': {1}")]
CtaError(String, String),
} }
pub struct CTAClient { pub struct CTAClient {
@ -35,7 +37,7 @@ impl CTAClient {
} }
} }
pub async fn make_request<T: Serialize, V: for<'a> Deserialize<'a>>( pub async fn make_request<T: Serialize, V: for<'a> Deserialize<'a> + CTAResponse>(
&mut self, &mut self,
endpoint: &str, endpoint: &str,
req: T, req: T,
@ -53,6 +55,8 @@ impl CTAClient {
.json() .json()
.await?; .await?;
resp.ctatt.check_error()?;
Ok(resp.ctatt) Ok(resp.ctatt)
} }

View File

@ -1,4 +1,4 @@
use crate::models::Ctatt; use crate::models::{CTAResponse, Ctatt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
@ -13,9 +13,16 @@ pub struct EtaRequest {
pub struct EtaResp { pub struct EtaResp {
#[serde(flatten)] #[serde(flatten)]
pub header: Ctatt, pub header: Ctatt,
#[serde(default)]
pub eta: Vec<Eta>, pub eta: Vec<Eta>,
} }
impl CTAResponse for EtaResp {
fn get_header(&self) -> Ctatt {
self.header.clone()
}
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct Eta { pub struct Eta {
#[serde(rename = "staId")] #[serde(rename = "staId")]

View File

@ -1,4 +1,4 @@
use crate::models::Ctatt; use crate::models::{CTAResponse, Ctatt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
@ -7,7 +7,7 @@ pub struct FollowRequest {
pub run_number: u32, pub run_number: u32,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug, Default)]
pub struct Position { pub struct Position {
pub lat: String, pub lat: String,
pub lon: String, pub lon: String,
@ -48,6 +48,14 @@ pub struct ScheduledStop {
pub struct FollowResp { pub struct FollowResp {
#[serde(flatten)] #[serde(flatten)]
pub header: Ctatt, pub header: Ctatt,
#[serde(default)]
pub position: Position, pub position: Position,
#[serde(default)]
pub eta: Vec<ScheduledStop>, pub eta: Vec<ScheduledStop>,
} }
impl CTAResponse for FollowResp {
fn get_header(&self) -> Ctatt {
self.header.clone()
}
}

View File

@ -3,9 +3,10 @@ pub mod follow;
pub mod route; pub mod route;
pub mod train; pub mod train;
use crate::Error;
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Clone)]
pub struct Ctatt { pub struct Ctatt {
pub tmst: String, pub tmst: String,
#[serde(rename = "errCd")] #[serde(rename = "errCd")]
@ -14,7 +15,20 @@ pub struct Ctatt {
pub err_nm: Option<String>, pub err_nm: Option<String>,
} }
pub trait CTAResponse {
fn get_header(&self) -> Ctatt;
fn check_error(&self) -> Result<(), Error> {
let header = self.get_header();
if let Some(err_number) = header.err_nm {
Err(Error::CtaError(err_number, header.err_cd))
} else {
Ok(())
}
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Response<T> { pub struct Response<T: CTAResponse> {
pub ctatt: T, pub ctatt: T,
} }

View File

@ -1,5 +1,5 @@
use crate::models::Ctatt;
use crate::models::train::Train; use crate::models::train::Train;
use crate::models::{CTAResponse, Ctatt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
@ -18,5 +18,12 @@ pub struct Route {
pub struct RouteResp { pub struct RouteResp {
#[serde(flatten)] #[serde(flatten)]
pub header: Ctatt, pub header: Ctatt,
#[serde(default)]
pub route: Vec<Route>, pub route: Vec<Route>,
} }
impl CTAResponse for RouteResp {
fn get_header(&self) -> Ctatt {
self.header.clone()
}
}