#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NotFoundError {
pub code: String,
pub resource: String,
pub id: String,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub next_steps: Vec<String>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Vec::is_empty")
)]
pub suggestions: Vec<String>,
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::error::env_control::should_skip_location")
)]
pub location: Option<crate::error::source_location::SourceLocation>,
}
impl NotFoundError {
pub fn new(resource: impl Into<String>, id: impl Into<String>) -> Self {
Self {
code: String::from(crate::error::codes::resource::NOT_FOUND),
resource: resource.into(),
id: id.into(),
next_steps: Vec::new(),
suggestions: Vec::new(),
location: None,
}
}
pub fn with_next_step(mut self, step: impl Into<String>) -> Self {
self.next_steps.push(step.into());
self
}
pub fn with_next_steps(mut self, steps: &[&str]) -> Self {
self
.next_steps
.extend(steps.iter().map(|s| String::from(*s)));
self
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestions.push(suggestion.into());
self
}
pub fn with_suggestions(mut self, suggestions: &[&str]) -> Self {
self
.suggestions
.extend(suggestions.iter().map(|s| String::from(*s)));
self
}
pub fn with_location(mut self, location: crate::error::source_location::SourceLocation) -> Self {
self.location = Some(location);
self
}
}
impl std::fmt::Display for NotFoundError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Error [{}]: {} not found with id '{}'",
self.code, self.resource, self.id
)?;
if self.next_steps.is_empty() {
write!(f, "\nNext Step: Verify {} ID and existence", self.resource)?;
} else {
for step in &self.next_steps {
write!(f, "\nNext Step: {step}")?;
}
}
for suggestion in &self.suggestions {
write!(f, "\nSuggestion: {suggestion}")?;
}
if let Some(ref location) = self.location {
write!(f, "\nSource: {location}")?;
}
Ok(())
}
}
impl std::error::Error for NotFoundError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_not_found_error_creation() {
let err = NotFoundError::new("User", "123");
assert_eq!(err.resource, "User");
assert_eq!(err.id, "123");
assert_eq!(err.code, crate::error::codes::resource::NOT_FOUND);
}
#[test]
fn test_not_found_error_display() {
let err = NotFoundError::new("Order", "abc-123");
let display = format!("{err}");
assert!(display.contains("Order"));
assert!(display.contains("abc-123"));
assert!(display.contains(crate::error::codes::resource::NOT_FOUND));
}
#[test]
#[cfg(feature = "serde")]
fn test_guidance_renders_and_round_trips() {
let err = NotFoundError::new("User", "123")
.with_next_step("Verify the ID")
.with_suggestion("call users.list() to see valid ids");
let display = format!("{err}");
assert!(display.contains("Verify the ID"));
assert!(display.contains("call users.list()"));
let json = serde_json::to_string(&err).unwrap();
let back: NotFoundError = serde_json::from_str(&json).unwrap();
assert_eq!(back.next_steps, err.next_steps);
assert_eq!(back.suggestions, err.suggestions);
}
}