fix: improve json error handling, make email_address optional

This commit is contained in:
2025-09-12 23:36:07 -05:00
parent 752c855dec
commit a6e7adcaef
4 changed files with 25 additions and 5 deletions

1
Cargo.lock generated
View File

@@ -193,6 +193,7 @@ dependencies = [
"reqwest-middleware", "reqwest-middleware",
"serde", "serde",
"serde_json", "serde_json",
"serde_path_to_error",
"serenity", "serenity",
"sqlx", "sqlx",
"thiserror 2.0.16", "thiserror 2.0.16",

View File

@@ -43,5 +43,6 @@ tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] }
url = "2.5" url = "2.5"
governor = "0.10.1" governor = "0.10.1"
once_cell = "1.21.3" once_cell = "1.21.3"
serde_path_to_error = "0.1.17"
[dev-dependencies] [dev-dependencies]

View File

@@ -1,16 +1,34 @@
//! JSON parsing utilities for the Banner API client. //! JSON parsing utilities for the Banner API client.
use anyhow::Result; use anyhow::Result;
use serde_json;
/// Attempt to parse JSON and, on failure, include a contextual snippet of the /// Attempt to parse JSON and, on failure, include a contextual snippet of the
/// line where the error occurred. This prevents dumping huge JSON bodies to logs. /// line where the error occurred. This prevents dumping huge JSON bodies to logs.
pub fn parse_json_with_context<T: serde::de::DeserializeOwned>(body: &str) -> Result<T> { pub fn parse_json_with_context<T: serde::de::DeserializeOwned>(body: &str) -> Result<T> {
match serde_json::from_str::<T>(body) { let jd = &mut serde_json::Deserializer::from_str(body);
match serde_path_to_error::deserialize(jd) {
Ok(value) => Ok(value), Ok(value) => Ok(value),
Err(err) => { Err(err) => {
let (line, column) = (err.line(), err.column()); let inner_err = err.inner();
// let snippet = build_error_snippet(body, line, column, 80); let (line, column) = (inner_err.line(), inner_err.column());
Err(anyhow::anyhow!("{err} at line {line}, column {column}",)) let snippet = build_error_snippet(body, line, column, 20);
let path = err.path().to_string();
let msg = inner_err.to_string();
let loc = format!(" at line {line} column {column}");
let msg_without_loc = msg.strip_suffix(&loc).unwrap_or(&msg).to_string();
let mut final_err = String::new();
if !path.is_empty() && path != "." {
final_err.push_str(&format!("for path '{}' ", path));
}
final_err.push_str(&format!(
"({msg_without_loc}) at line {line} column {column}"
));
final_err.push_str(&format!("\n{snippet}"));
Err(anyhow::anyhow!(final_err))
} }
} }
} }

View File

@@ -33,7 +33,7 @@ pub struct FacultyItem {
#[serde(deserialize_with = "deserialize_string_to_u32")] #[serde(deserialize_with = "deserialize_string_to_u32")]
pub course_reference_number: u32, // CRN, e.g 27294 pub course_reference_number: u32, // CRN, e.g 27294
pub display_name: String, // "LastName, FirstName" pub display_name: String, // "LastName, FirstName"
pub email_address: String, // e.g. FirstName.LastName@utsaedu pub email_address: Option<String>, // e.g. FirstName.LastName@utsaedu
pub primary_indicator: bool, pub primary_indicator: bool,
pub term: String, // e.g "202420" pub term: String, // e.g "202420"
} }