jsend/
lib.rs

1//! The `jsend` crate provides an implementation of the [JSend specification](https://github.com/omniti-labs/jsend)
2//! for API responses in Rust applications.
3//!
4//! ## Usage
5//!
6//! Add `jsend` to your `Cargo.toml`:
7//!
8//! ```toml
9//! [dependencies]
10//! jsend = "1.0"
11//! serde = { version = "1.0", features = ["derive"] }
12//! ```
13//!
14//! ### Basic Example
15//!
16//! ```rust
17//! use jsend::JSendResponse;
18//! use std::collections::HashMap;
19//!
20//! // Success response with data
21//! let data = Some(HashMap::from([("key", "value")]));
22//! let response = JSendResponse::success(data);
23//! println!("{}", serde_json::to_string(&response).unwrap());
24//!
25//! // Error response
26//! let error_response = JSendResponse::error("An error occurred".to_string(), Some(100), None::<String>);
27//! println!("{}", serde_json::to_string(&error_response).unwrap());
28//! ```
29//!
30//! A more in-depth example of how this crate could be used with a framework
31//! like [axum](https://crates.io/crates/axum) can be found in the `examples/`
32//! directory.
33//!
34//! ## Features
35//! - `serde`: Enabled by default. Adds [serde::Serialize] and
36//! [serde::Deserialize] derives, along with attributes to serialize into JSON
37//! according to the JSend specification.
38
39#[cfg(feature = "serde")]
40use serde::{Deserialize, Serialize};
41
42/// The `JSendResponse` enum provides a way to model JSend compliant responses.
43///
44/// It supports the three JSend response types as variants: `Success`, `Fail`,
45/// and `Error`.
46#[derive(Debug, Clone, PartialEq, Hash)]
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48#[cfg_attr(feature = "serde", serde(tag = "status", rename_all = "lowercase"))]
49pub enum JSendResponse<T> {
50    Success {
51        /// Acts as the wrapper for any data returned by the API call. If the
52        /// call returns no data, `data` should be set to `None`.
53        data: Option<T>,
54    },
55    Fail {
56        /// Provides the wrapper for the details of why the request failed. If
57        /// the reasons for failure correspond to POST values, the response
58        /// object's keys SHOULD correspond to those POST values.
59        data: T,
60    },
61    Error {
62        /// A meaningful, end-user-readable (or at the least log-worthy)
63        /// message, explaining what went wrong.
64        message: String,
65        /// A numeric code corresponding to the error, if applicable.
66        #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
67        code: Option<i64>,
68        /// A generic container for any other information about the error, i.e.
69        /// the conditions that caused the error, stack traces, etc.
70        #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
71        data: Option<T>,
72    },
73}
74
75impl<T> JSendResponse<T> {
76    /// Constructs the [JSendResponse::Success] variant.
77    pub fn success(data: Option<T>) -> JSendResponse<T> {
78        JSendResponse::Success { data }
79    }
80
81    /// Constructs the [JSendResponse::Fail] variant.
82    pub fn fail(data: T) -> JSendResponse<T> {
83        JSendResponse::Fail { data }
84    }
85
86    /// Constructs the [JSendResponse::Error] variant.
87    pub fn error(message: String, code: Option<i64>, data: Option<T>) -> JSendResponse<T> {
88        JSendResponse::Error {
89            message,
90            code,
91            data,
92        }
93    }
94
95    /// Returns a reference to the underlying `Option` value if set, and `None`
96    /// otherwise.
97    ///
98    /// This getter "flattens" the structure of the enum:
99    /// ```rust
100    /// # use std::collections::HashMap;
101    /// # use jsend::JSendResponse;
102    /// # let data = HashMap::from([("key", "value")]);
103    /// let response_with_data = JSendResponse::success(Some(data.clone()));
104    /// assert_eq!(response_with_data.data(), Some(data).as_ref());
105    ///
106    /// let response_without_data = JSendResponse::success(None::<HashMap<&str, &str>>);
107    /// assert_eq!(response_without_data.data(), None)
108    /// ```
109    pub fn data(&self) -> Option<&T> {
110        match self {
111            JSendResponse::Success { data } => data.as_ref(),
112            JSendResponse::Fail { data } => Some(data),
113            JSendResponse::Error { data, .. } => data.as_ref(),
114        }
115    }
116
117    /// Returns a reference to `message` for the `Error` variant, and `None`
118    /// for the other variants.
119    pub fn message(&self) -> Option<&String> {
120        match self {
121            JSendResponse::Error { message, .. } => Some(message),
122            _ => None,
123        }
124    }
125
126    /// Returns a reference to the value of `code`for the `Error` variant, and
127    /// `None` for the other variants.
128    ///
129    /// This getter "flattens" the structure of the enum:
130    /// ```rust
131    /// # use std::collections::HashMap;
132    /// # use jsend::JSendResponse;
133    /// # let message = "error message".to_string();
134    /// # let code = 123;
135    /// # let data = HashMap::from([("key", "value")]);
136    /// let response_with_code = JSendResponse::error(message.clone(), Some(code), Some(data.clone()));
137    /// assert_eq!(response_with_code.code(), Some(code).as_ref());
138    ///
139    /// let response_without_code = JSendResponse::error(message.clone(), None, Some(data.clone()));
140    /// assert_eq!(response_without_code.code(), None);
141    /// ```
142    pub fn code(&self) -> Option<&i64> {
143        match self {
144            JSendResponse::Error { code, .. } => code.as_ref(),
145            _ => None,
146        }
147    }
148}
149
150#[cfg(test)]
151mod test {
152    use std::collections::HashMap;
153
154    use super::*;
155
156    #[test]
157    fn test_success_variant() {
158        let data = HashMap::from([("key", "value")]);
159        let response = JSendResponse::success(Some(data.clone()));
160        assert_eq!(Some(data).as_ref(), response.data());
161        assert_eq!(None, response.code());
162        assert_eq!(None, response.message());
163    }
164
165    #[test]
166    fn test_success_variant_no_data() {
167        let response: JSendResponse<HashMap<&str, &str>> = JSendResponse::success(None);
168        assert_eq!(None, response.data());
169        assert_eq!(None, response.code());
170        assert_eq!(None, response.message());
171    }
172
173    #[test]
174    fn test_fail_variant() {
175        let data = HashMap::from([("key", "value")]);
176        let response = JSendResponse::fail(data.clone());
177        assert_eq!(Some(data).as_ref(), response.data());
178        assert_eq!(None, response.code());
179        assert_eq!(None, response.message());
180    }
181
182    #[test]
183    fn test_fail_variant_no_data() {
184        let data: Option<String> = None;
185        let response = JSendResponse::fail(data.clone());
186        assert_eq!(Some(data).as_ref(), response.data());
187        assert_eq!(None, response.code());
188        assert_eq!(None, response.message());
189    }
190
191    #[test]
192    fn test_error_variant() {
193        let message = "error message".to_string();
194        let code = 123;
195        let data = HashMap::from([("key", "value")]);
196        let response = JSendResponse::error(message.clone(), Some(code), Some(data.clone()));
197        assert_eq!(Some(message).as_ref(), response.message());
198        assert_eq!(Some(code).as_ref(), response.code());
199        assert_eq!(Some(data).as_ref(), response.data());
200    }
201
202    #[test]
203    fn test_error_variant_only_message() {
204        let message = "error message".to_string();
205        let response: JSendResponse<String> = JSendResponse::error(message.clone(), None, None);
206        assert_eq!(Some(message).as_ref(), response.message());
207        assert_eq!(None, response.code());
208        assert_eq!(None, response.data());
209    }
210}