use std::fmt;
use serde::de::DeserializeOwned;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldError {
path: Option<String>,
message: String,
}
impl FieldError {
pub fn new(e: serde_path_to_error::Error<serde_json::Error>) -> Self {
let path = e.path().to_string();
let message = message(e.inner());
Self {
path: (!matches!(path.as_str(), "" | "." | "?")
&& !names_its_field(&message, &path))
.then_some(path),
message,
}
}
pub fn path(&self) -> Option<&str> {
self.path.as_deref()
}
pub fn message(&self) -> &str {
&self.message
}
pub fn after(&self, lead: &str) -> String {
match &self.path {
Some(path) => format!("{lead} at `{path}`: {}", self.message),
None => format!("{lead}: {}", self.message),
}
}
}
impl fmt::Display for FieldError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.path {
Some(path) => write!(f, "at `{path}`: {}", self.message),
None => f.write_str(&self.message),
}
}
}
impl std::error::Error for FieldError {}
fn names_its_field(message: &str, path: &str) -> bool {
let rest = message
.strip_prefix('`')
.and_then(|m| m.strip_prefix(path))
.and_then(|m| m.strip_prefix('`'))
.or_else(|| message.strip_prefix(path));
rest.is_some_and(|rest| rest.starts_with([' ', ':']))
}
pub fn message(e: &serde_json::Error) -> String {
let text = e.to_string();
if e.line() == 0 {
return text;
}
let position = format!(" at line {} column {}", e.line(), e.column());
match text.strip_suffix(&position) {
Some(message) => message.to_string(),
None => text,
}
}
pub fn from_value<T: DeserializeOwned>(
value: serde_json::Value,
) -> Result<T, FieldError> {
serde_path_to_error::deserialize(value).map_err(FieldError::new)
}
pub fn from_str<T: DeserializeOwned>(text: &str) -> Result<T, FieldError> {
from_slice(text.as_bytes())
}
pub fn from_slice<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, FieldError> {
let mut de = serde_json::Deserializer::from_slice(bytes);
let value =
serde_path_to_error::deserialize(&mut de).map_err(FieldError::new)?;
de.end().map_err(|e| FieldError {
path: None,
message: message(&e),
})?;
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
#[allow(dead_code)]
struct Outer {
options: Options,
#[serde(default)]
items: Vec<Item>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(deny_unknown_fields)]
#[allow(dead_code)]
struct Options {
mode: Mode,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
enum Mode {
Full,
Summary,
}
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
struct Item {
id: u32,
}
#[test]
fn a_nested_error_names_its_path_and_no_position() {
let text = r#"{"options": {"mode": "bogus"}}"#;
let before = serde_json::from_str::<Outer>(text).unwrap_err();
assert_eq!(
before.to_string(),
"unknown variant `bogus`, expected `full` or `summary` at line 1 \
column 28"
);
let after = from_str::<Outer>(text).unwrap_err();
assert_eq!(
after.to_string(),
"at `options.mode`: unknown variant `bogus`, expected `full` or \
`summary`"
);
assert_eq!(after.path(), Some("options.mode"));
let value: serde_json::Value = serde_json::from_str(text).unwrap();
assert_eq!(from_value::<Outer>(value).unwrap_err(), after);
let err = from_str::<Outer>(
r#"{"options": {"mode": "full"}, "items": [{"id": 1}, {"id": "x"}]}"#,
)
.unwrap_err();
assert_eq!(
err.to_string(),
"at `items[1].id`: invalid type: string \"x\", expected u32"
);
}
#[test]
fn whole_value_errors_keep_the_message_alone() {
let err =
from_str::<Outer>(r#"{"options": {"mode": "full"}"#).unwrap_err();
assert_eq!(err.path(), None);
assert_eq!(err.message(), "EOF while parsing an object");
let err = from_str::<Outer>("7").unwrap_err();
assert_eq!(err.path(), None);
assert_eq!(
err.message(),
"invalid type: integer `7`, expected struct Outer"
);
assert_eq!(
err.after("Invalid JSON"),
format!("Invalid JSON: {}", err.message())
);
let err = from_str::<serde_json::Value>("{} x").unwrap_err();
assert_eq!(err.to_string(), "trailing characters");
}
#[derive(Debug, serde::Deserialize)]
#[allow(dead_code)]
struct Named {
reply_to: Checked,
}
#[derive(Debug)]
struct Checked;
impl<'de> serde::Deserialize<'de> for Checked {
fn deserialize<D: serde::Deserializer<'de>>(
_: D,
) -> Result<Self, D::Error> {
Err(serde::de::Error::custom(
"`reply_to` must be a post or comment id",
))
}
}
#[test]
fn a_message_naming_its_field_is_not_given_it_twice() {
let err = from_str::<Named>(r#"{"reply_to": "x"}"#).unwrap_err();
assert_eq!(err.path(), None);
assert_eq!(err.to_string(), "`reply_to` must be a post or comment id");
assert!(names_its_field(
"entry_type \"bogus\" is not one of: a",
"entry_type"
));
assert!(!names_its_field("entry_types are listed", "entry_type"));
assert!(!names_its_field("expected `reply_to` here", "reply_to"));
}
}