1use std::fmt;
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ValidationError {
12 pub line: usize,
16 pub field: Option<String>,
17 pub message: String,
18}
19
20impl ValidationError {
21 pub fn field(line: usize, field: impl Into<String>, message: impl Into<String>) -> Self {
23 Self {
24 line,
25 field: Some(field.into()),
26 message: message.into(),
27 }
28 }
29
30 pub fn row(line: usize, message: impl Into<String>) -> Self {
32 Self {
33 line,
34 field: None,
35 message: message.into(),
36 }
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ParseError {
43 pub line: usize,
45 pub message: String,
46}
47
48impl fmt::Display for ParseError {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(f, "line {}: {}", self.line, self.message)
51 }
52}
53
54impl std::error::Error for ParseError {}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ApiError {
63 pub status: u16,
64 pub message: String,
65}
66
67impl ApiError {
68 pub fn new(status: u16, message: impl Into<String>) -> Self {
69 Self {
70 status,
71 message: message.into(),
72 }
73 }
74
75 pub fn bad_request(message: impl Into<String>) -> Self {
77 Self::new(400, message)
78 }
79
80 pub fn server(message: impl Into<String>) -> Self {
82 Self::new(500, message)
83 }
84
85 pub fn from_parse(file: &str, err: &ParseError) -> Self {
88 Self::server(format!("{file} {err}"))
89 }
90}
91
92impl fmt::Display for ApiError {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(f, "{} {}", self.status, self.message)
95 }
96}
97
98impl std::error::Error for ApiError {}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn validation_error_serializes_absent_field_as_null() {
106 let json = serde_json::to_value(ValidationError::row(3, "no date")).unwrap();
107 assert_eq!(json["line"], 3);
108 assert!(json["field"].is_null());
109 assert_eq!(json["message"], "no date");
110 }
111
112 #[test]
113 fn parse_failure_names_the_file_and_line() {
114 let err = ApiError::from_parse(
115 "Books.jsonl",
116 &ParseError {
117 line: 7,
118 message: "expected value".into(),
119 },
120 );
121 assert_eq!(err.status, 500);
122 assert_eq!(err.message, "Books.jsonl line 7: expected value");
123 }
124}