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}