What can go wrong, in the vocabulary of what was being done. No port returns a bare string as its error: a caller must be able to decide (refuse, retry, fall back) from the type alone.

4use std::fmt;
6use serde::{Deserialize, Serialize};

A holder of Whiskers' documents could not do its job. Which file, which table or which request failed is the adapter's to log; the caller only needs to know whether to try again or to give up.

10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum StoreError {

The storage could not be reached or written. Trying again may work.

13    Unavailable,

What is stored does not read back as the document it should be. Trying again will not help; a person has to look.

16    Damaged,
17}
19impl fmt::Display for StoreError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            StoreError::Unavailable => write!(f, "storage unavailable"),
23            StoreError::Damaged => write!(f, "stored document damaged"),
24        }
25    }
26}
27
28impl std::error::Error for StoreError {}

What the other side said about a failure, already phrased for a person (and for the tablet's log): the status line, the vendor's complaint. Carried so it can be shown and logged. It is never branched on: anything a caller must decide from is a variant or a field of the error that holds it.

33#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Diagnostic(String);
36impl Diagnostic {
37    pub fn new(said: impl Into<String>) -> Self {
38        Self(said.into())
39    }
40
41    pub fn as_str(&self) -> &str {
42        &self.0
43    }
44}
45
46impl fmt::Display for Diagnostic {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str(&self.0)
49    }
50}