1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use std::{error, fmt, sync::Arc};

// Define our error types. These may be customized for our error handling cases.
// Now we will be able to write our own errors, defer to an underlying error
// implementation, or do something in between.
#[derive(Debug, Clone)]
pub enum CouchError {
    /// A `CouchDB` operation failed, typically indicated by a specific HTTP error status that was returned.
    OperationFailed(ErrorDetails),
    /// Parsing of a JSON document failed.
    InvalidJson(ErrorMessage),
    /// The provided url is invalid.
    MalformedUrl(ErrorMessage),
    /// A design document could not be created.
    CreateDesignFailed(ErrorMessage),
}

#[derive(Debug, Clone)]
pub struct ErrorDetails {
    /// Some (bulk) transaction might return an id as part of the error
    pub id: Option<String>,
    /// HTTP Status Code
    pub status: http::StatusCode,
    /// Detailed error message
    pub message: String,
    upstream: Option<UpstreamError>,
}

#[derive(Debug, Clone)]
pub struct ErrorMessage {
    /// Detailed error message
    pub message: String,
    pub(crate) upstream: Option<UpstreamError>,
}

type UpstreamError = Arc<dyn error::Error + Send + Sync + 'static>;
pub type CouchResult<T> = Result<T, CouchError>;

impl CouchError {
    #[must_use]
    pub fn new(message: String, status: http::StatusCode) -> CouchError {
        CouchError::OperationFailed(ErrorDetails {
            id: None,
            message,
            status,
            upstream: None,
        })
    }

    #[must_use]
    pub fn new_with_id(id: Option<String>, message: String, status: http::StatusCode) -> CouchError {
        CouchError::OperationFailed(ErrorDetails {
            id,
            message,
            status,
            upstream: None,
        })
    }

    #[must_use]
    pub fn is_not_found(&self) -> bool {
        self.status() == Some(http::StatusCode::NOT_FOUND)
    }

    #[must_use]
    pub fn status(&self) -> Option<http::StatusCode> {
        match self {
            CouchError::OperationFailed(details) => Some(details.status),
            _ => None,
        }
    }
}

pub trait CouchResultExt<T> {
    /// turns an Ok into an Ok(Some), a not-found into an Ok(None), otherwise it will return the error.
    fn into_option(self) -> CouchResult<Option<T>>;
}

impl<T> CouchResultExt<T> for CouchResult<T> {
    fn into_option(self) -> CouchResult<Option<T>> {
        match self {
            Ok(r) => Ok(Some(r)),
            Err(err) => {
                if err.is_not_found() {
                    Ok(None)
                } else {
                    Err(err)
                }
            }
        }
    }
}

impl fmt::Display for CouchError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            CouchError::OperationFailed(details) => {
                if let Some(id) = &details.id {
                    write!(f, "{} -> {}: {}", id, details.status, details.message)
                } else {
                    write!(f, "{}: {}", details.status, details.message)
                }
            }
            CouchError::InvalidJson(err) | CouchError::MalformedUrl(err) | CouchError::CreateDesignFailed(err) => {
                write!(f, "{}", err.message)
            }
        }
    }
}

// This is important for other errors to wrap this one.
impl error::Error for CouchError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        // Generic error, underlying cause isn't tracked.
        match self {
            CouchError::OperationFailed(details) => details.upstream.as_ref().map(|e| &**e as _),
            CouchError::InvalidJson(err) | CouchError::MalformedUrl(err) | CouchError::CreateDesignFailed(err) => {
                err.upstream.as_ref().map(|e| &**e as _)
            }
        }
    }
}

impl std::convert::From<reqwest::Error> for CouchError {
    fn from(err: reqwest::Error) -> Self {
        CouchError::OperationFailed(ErrorDetails {
            id: None,
            status: err.status().unwrap_or(http::StatusCode::NOT_IMPLEMENTED),
            message: err.to_string(),
            upstream: Some(Arc::new(err)),
        })
    }
}

impl std::convert::From<serde_json::Error> for CouchError {
    fn from(err: serde_json::Error) -> Self {
        CouchError::InvalidJson(ErrorMessage {
            message: err.to_string(),
            upstream: Some(Arc::new(err)),
        })
    }
}

impl std::convert::From<url::ParseError> for CouchError {
    fn from(err: url::ParseError) -> Self {
        CouchError::MalformedUrl(ErrorMessage {
            message: err.to_string(),
            upstream: Some(Arc::new(err)),
        })
    }
}