Skip to main content

vitaminc_aead_napi/
error.rs

1//! The error a walk or a build carries through the sink traits, and how it
2//! comes back to JavaScript as the error it was.
3
4use napi::{Error, Status};
5use vitaminc_aead::plaintext::{self, SourceError};
6
7/// A failure on the JavaScript side of a walk or a build: a getter threw, a
8/// string had an unpaired surrogate, a value was of a type that cannot be
9/// encrypted, a tree was too deep.
10///
11/// A napi [`Error`] is not `Send`, so it cannot travel inside a
12/// [`SourceError`]; this carries its status and reason, which is all a
13/// caller needs to hand it back to JavaScript. A [`Status::PendingException`]
14/// means the original JavaScript exception is still pending in the engine,
15/// and returning this error lets it surface unchanged.
16#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
17#[error("{reason}")]
18pub struct JsFault {
19    status: Status,
20    reason: String,
21}
22
23impl JsFault {
24    pub(crate) fn new(status: Status, reason: impl Into<String>) -> Self {
25        Self {
26            status,
27            reason: reason.into(),
28        }
29    }
30
31    /// The napi status of the failure. [`Status::PendingException`] means a
32    /// JavaScript exception is still pending: return this error to
33    /// JavaScript before running any more JavaScript, so the exception
34    /// surfaces as it was thrown.
35    pub fn status(&self) -> Status {
36        self.status
37    }
38
39    /// The failure's message.
40    pub fn reason(&self) -> &str {
41        &self.reason
42    }
43
44    /// The napi error for a source error, which is a [`JsFault`] when the
45    /// source was a JavaScript value and some other error otherwise.
46    pub fn from_source(error: &SourceError) -> Error {
47        match error.inner().downcast_ref::<JsFault>() {
48            Some(fault) => fault.clone().into(),
49            None => Error::new(Status::InvalidArg, error.to_string()),
50        }
51    }
52}
53
54impl From<Error> for JsFault {
55    fn from(error: Error) -> Self {
56        Self {
57            status: error.status,
58            reason: error.reason,
59        }
60    }
61}
62
63impl From<JsFault> for Error {
64    fn from(fault: JsFault) -> Self {
65        Error::new(fault.status, fault.reason)
66    }
67}
68
69impl plaintext::Error for JsFault {
70    fn source(error: SourceError) -> Self {
71        match error.inner().downcast_ref::<JsFault>() {
72            Some(fault) => fault.clone(),
73            None => Self::new(Status::InvalidArg, error.to_string()),
74        }
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use vitaminc_aead::plaintext::Error as _;
82
83    #[test]
84    fn a_fault_reports_the_status_and_reason_it_was_made_with() {
85        let fault = JsFault::new(Status::GenericFailure, "the getter threw");
86        assert_eq!(fault.status(), Status::GenericFailure);
87        assert_eq!(fault.reason(), "the getter threw");
88    }
89
90    #[test]
91    fn a_fault_round_trips_through_a_napi_error() {
92        let fault = JsFault::new(Status::PendingException, "getter threw");
93        let error: Error = fault.clone().into();
94        assert_eq!(error.status, Status::PendingException);
95        assert_eq!(error.reason, "getter threw");
96        assert_eq!(JsFault::from(error), fault);
97    }
98
99    #[test]
100    fn a_source_error_carrying_a_fault_is_that_fault() {
101        let fault = JsFault::new(Status::InvalidArg, "unpaired surrogate");
102        let wrapped = SourceError::new(fault.clone());
103        assert_eq!(JsFault::source(wrapped), fault);
104        let wrapped = SourceError::new(fault.clone());
105        let error = JsFault::from_source(&wrapped);
106        assert_eq!(
107            (error.status, error.reason.as_str()),
108            (Status::InvalidArg, "unpaired surrogate")
109        );
110    }
111
112    #[test]
113    fn a_source_error_from_elsewhere_keeps_its_message() {
114        let wrapped = SourceError::msg("frame is malformed");
115        assert_eq!(
116            JsFault::source(wrapped),
117            JsFault::new(Status::InvalidArg, "frame is malformed")
118        );
119        let error = JsFault::from_source(&SourceError::msg("frame is malformed"));
120        assert_eq!(error.status, Status::InvalidArg);
121        assert_eq!(error.reason, "frame is malformed");
122    }
123}