From a6e7adcaefdecebb8d0c0547bb7294d09a68ea1e Mon Sep 17 00:00:00 2001 From: Xevion Date: Fri, 12 Sep 2025 23:36:07 -0500 Subject: [PATCH] fix: improve json error handling, make email_address optional --- Cargo.lock | 1 + Cargo.toml | 1 + src/banner/json.rs | 26 ++++++++++++++++++++++---- src/banner/models/meetings.rs | 2 +- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b874132..3e4397c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -193,6 +193,7 @@ dependencies = [ "reqwest-middleware", "serde", "serde_json", + "serde_path_to_error", "serenity", "sqlx", "thiserror 2.0.16", diff --git a/Cargo.toml b/Cargo.toml index accc2f9..76d336a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,5 +43,6 @@ tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } url = "2.5" governor = "0.10.1" once_cell = "1.21.3" +serde_path_to_error = "0.1.17" [dev-dependencies] diff --git a/src/banner/json.rs b/src/banner/json.rs index 85d6e1c..ead2720 100644 --- a/src/banner/json.rs +++ b/src/banner/json.rs @@ -1,16 +1,34 @@ //! JSON parsing utilities for the Banner API client. use anyhow::Result; +use serde_json; /// 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. pub fn parse_json_with_context(body: &str) -> Result { - match serde_json::from_str::(body) { + let jd = &mut serde_json::Deserializer::from_str(body); + match serde_path_to_error::deserialize(jd) { Ok(value) => Ok(value), Err(err) => { - let (line, column) = (err.line(), err.column()); - // let snippet = build_error_snippet(body, line, column, 80); - Err(anyhow::anyhow!("{err} at line {line}, column {column}",)) + let inner_err = err.inner(); + let (line, column) = (inner_err.line(), inner_err.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)) } } } diff --git a/src/banner/models/meetings.rs b/src/banner/models/meetings.rs index 1f70a1d..7ed1424 100644 --- a/src/banner/models/meetings.rs +++ b/src/banner/models/meetings.rs @@ -33,7 +33,7 @@ pub struct FacultyItem { #[serde(deserialize_with = "deserialize_string_to_u32")] pub course_reference_number: u32, // CRN, e.g 27294 pub display_name: String, // "LastName, FirstName" - pub email_address: String, // e.g. FirstName.LastName@utsaedu + pub email_address: Option, // e.g. FirstName.LastName@utsaedu pub primary_indicator: bool, pub term: String, // e.g "202420" }