use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidationError {
pub line: usize,
pub field: Option<String>,
pub message: String,
}
impl ValidationError {
pub fn field(line: usize, field: impl Into<String>, message: impl Into<String>) -> Self {
Self {
line,
field: Some(field.into()),
message: message.into(),
}
}
pub fn row(line: usize, message: impl Into<String>) -> Self {
Self {
line,
field: None,
message: message.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ParseError {
pub line: usize,
pub message: String,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "line {}: {}", self.line, self.message)
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiError {
pub status: u16,
pub message: String,
}
impl ApiError {
pub fn new(status: u16, message: impl Into<String>) -> Self {
Self {
status,
message: message.into(),
}
}
pub fn bad_request(message: impl Into<String>) -> Self {
Self::new(400, message)
}
pub fn server(message: impl Into<String>) -> Self {
Self::new(500, message)
}
pub fn from_parse(file: &str, err: &ParseError) -> Self {
Self::server(format!("{file} {err}"))
}
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.status, self.message)
}
}
impl std::error::Error for ApiError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validation_error_serializes_absent_field_as_null() {
let json = serde_json::to_value(ValidationError::row(3, "no date")).unwrap();
assert_eq!(json["line"], 3);
assert!(json["field"].is_null());
assert_eq!(json["message"], "no date");
}
#[test]
fn parse_failure_names_the_file_and_line() {
let err = ApiError::from_parse(
"Books.jsonl",
&ParseError {
line: 7,
message: "expected value".into(),
},
);
assert_eq!(err.status, 500);
assert_eq!(err.message, "Books.jsonl line 7: expected value");
}
}