Skip to main content

table_editor/
error.rs

1//! The failures the editor reports: per-row problems the browser renders beside
2//! the offending cell, and request failures it renders as a banner.
3
4use std::fmt;
5
6use serde::{Deserialize, Serialize};
7
8/// A problem with one row of a table. `field` names the column at fault when
9/// the check is specific to one, and is null for a whole-row check.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ValidationError {
12    /// The row's one-based position in the set being validated, which is the
13    /// row the editor highlights. Blank lines in the stored file are skipped on
14    /// the way in, so this need not be the file line the row was read from.
15    pub line: usize,
16    pub field: Option<String>,
17    pub message: String,
18}
19
20impl ValidationError {
21    /// A problem with the column `field` of the row on `line`.
22    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    /// A problem with the row on `line` as a whole.
31    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/// A line of a JSONL file that does not deserialize into its row type.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct ParseError {
43    /// The one-based physical line of the file, counting blank lines.
44    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/// A failure with the HTTP status to report it under. Body and parse problems
57/// are 400; a write that does not come from a page this server served is 403 or
58/// 415; an endpoint or an action nobody offers is 404; an endpoint reached by
59/// the wrong method is 405; a body past the size cap is 413; and filesystem and
60/// serialization failures are 500.
61#[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    /// A malformed request the client should not repeat unchanged (400).
76    pub fn bad_request(message: impl Into<String>) -> Self {
77        Self::new(400, message)
78    }
79
80    /// A failure on this side of the wire (500).
81    pub fn server(message: impl Into<String>) -> Self {
82        Self::new(500, message)
83    }
84
85    /// An unparseable line of a stored table (500): the file is the server's to
86    /// keep readable, so a client cannot fix it by retrying.
87    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}