simbld_http/helpers/
generate_responses_functions.rs

1//! # HTTP Response Code Generation Macro
2//!
3//! This module provides a macro for generating standardized HTTP response enums with consistent
4//! behavior and serialization.
5//!
6//! ## Features
7//!
8//! - Generates enum variants for HTTP response codes
9//! - Implements consistent methods for all response types
10//! - Provides serialization to JSON and XML formats
11//! - Standardizes error handling through unified response structures
12//! - Supports both standard HTTP codes and custom internal codes
13//!
14//! ## Usage
15//!
16//! ```rust
17//! use simbld_http::generate_responses_functions;
18//! use strum_macros::EnumIter;
19//! use simbld_http::traits::get_code_trait::GetCode;
20//! use simbld_http::responses::CustomResponse;
21//!
22//! generate_responses_functions! {
23//!     "Client Errors",
24//!     ResponsesClientCodes,
25//!     BadRequest => (400, "Bad Request", "Invalid request format", 4000, "BadRequest"),
26//!     Unauthorized => (401, "Unauthorized", "Authentication required", 4001, "Unauthorized"),
27//!     Forbidden => (403, "Forbidden", "Permission denied", 4003, "Forbidden"),
28//! }
29//! ```
30//!
31//! Each variant accepts 5 parameters:
32//! - Standard HTTP status code (u16)
33//! - Standard HTTP status name (string)
34//! - Description message (string)
35//! - Internal code (u16) for application-specific tracking
36//! - Internal name (string) for application-specific reference
37//!
38//! The generated enums implement common traits and provide methods for consistent
39//! response handling throughout the application.
40#[macro_export]
41macro_rules! generate_responses_functions {
42    (
43        $doc_family:expr,
44        $enum_name:ident,
45        $first_variant:ident => ($std_code_first:expr, $std_name_first:expr, $desc_first:expr, $int_code_first:expr, $int_std_name_first:expr)
46        $(, $variant:ident => ($std_code:expr, $std_name:expr, $desc:expr, $int_code:expr, $int_name:expr) )* $(,)?
47    ) => {
48        /// Enum representing HTTP response status codes and their descriptions.
49        #[derive(Debug, Clone, Copy, PartialEq, EnumIter)]
50        #[doc = $doc_family]
51        #[doc = concat!(
52            "\n\nEnum representing HTTP response status codes and descriptions for `",
53            stringify!($enum_name),
54            "`. This file defines the following methods:\n",
55            "- `to_http_code`: Converts the enum variant to its corresponding `HttpCode`.\n",
56            "- `get_code`: Returns the standard code as `u16`.\n",
57            "- `from_u16`: Constructs an enum variant from a given `u16` code (first matching standard, then internal).\n",
58            "- `from_internal_code`: Constructs an enum variant from a given internal `u16` code.\n",
59            "- `as_tuple`: Returns a unified tuple representation.\n",
60            "- `as_json`: Returns a JSON representation.\n\n",
61            "# Example\n```rust,no_run\n",
62            "use strum_macros::EnumIter;\n",
63            "use simbld_http::responses::CustomResponse;\n",
64            "use simbld_http::traits::get_code_trait::GetCode;\n",
65           "use simbld_http::responses::",
66            stringify!($enum_name),
67            ";\n\nlet example = ",
68            stringify!($enum_name),
69            "::",
70            stringify!($first_variant),
71            ";\nassert_eq!(example.get_code(), ",
72            stringify!($std_code_first),
73            ");\n// L'as_tuple retourne une structure UnifiedTuple avec les données du code de réponse\nlet tuple = example.as_tuple();\nassert_eq!(tuple.standard_code, ",
74            stringify!($std_code_first),
75            ");\nassert_eq!(tuple.standard_name, ",
76            stringify!($std_name_first),
77            ");\n```"
78        )]
79        pub enum $enum_name {
80            $first_variant,
81            $($variant,)*
82        }
83
84        /// Custom Serialize implementation to include both "type" and "details" fields.
85       impl serde::Serialize for $enum_name {
86            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
87            where
88                S: serde::Serializer,
89            {
90                // Convert the enum variant into its unified HTTP code tuple.
91                // Example: let unified = self.to_http_code().as_unified_tuple();
92                let unified = self.to_http_code().as_unified_tuple();
93                use serde::ser::SerializeStruct;
94                let mut state = serializer.serialize_struct(stringify!($enum_name), 2)?;
95                // Serialize the "type" field using the provided doc_family.
96                state.serialize_field("type", $doc_family)?;
97                // Serialize the "details" field using the custom JSON structure from as_json().
98                state.serialize_field("details", &unified.as_json())?;
99                state.end()
100            }
101        }
102
103        impl $enum_name {
104            /// Returns the description associated with the response code.
105            pub fn get_description(&self) -> &'static str {
106                match self {
107                    Self::$first_variant => $desc_first,
108                    $(
109                        Self::$variant => $desc,
110                    )*
111                }
112            }
113
114            /// Returns the standard code (u16) of the response.
115            pub fn get_code(&self) -> u16 {
116                match self {
117                    Self::$first_variant => $std_code_first,
118                    $(
119                        Self::$variant => $std_code,
120                    )*
121                }
122            }
123
124            /// Implementation of the `get_name` method for the enum.
125            pub fn get_name(&self) -> &'static str {
126                match self {
127                    Self::$first_variant => $std_name_first,
128                    $(
129                        Self::$variant => $std_name,
130                    )*
131                }
132            }
133
134            /// Returns the data associated with the response code.
135            pub fn get_data(&self) -> &'static str {
136                ""
137            }
138
139            /// Returns all data associated with the response code as a tuple.
140            pub fn get_all_data(&self) -> (u16, &'static str, &'static str, &'static str) {
141                (
142                    self.get_code(),
143                    self.get_name(),
144                    self.get_data(),
145                    self.get_description()
146                )
147            }
148
149            /// Converts the enum variant into a `CustomResponse`.
150            pub fn into_response(&self) -> $crate::responses::CustomResponse {
151                let (code, name, data, desc) = self.get_all_data();
152                CustomResponse::new(code, name, data, desc)
153            }
154
155            /// Converts the enum variant into its corresponding `HttpCode`.
156            pub fn to_http_code(&self) -> $crate::helpers::http_code_helper::HttpCode {
157                match self {
158                    Self::$first_variant => {
159                        let internal_code = if $int_code_first == $std_code_first {
160                            None
161                        } else {
162                            Some($int_code_first)
163                        };
164
165                        let internal_name = if $int_std_name_first == $std_name_first {
166                            None
167                        } else {
168                            Some($int_std_name_first)
169                        };
170
171                        $crate::helpers::http_code_helper::HttpCode {
172                            standard_code: $std_code_first,
173                            standard_name: $std_name_first,
174                            unified_description: $desc_first,
175                            internal_code,
176                            internal_name,
177                        }
178                    },
179                    $(
180                        Self::$variant => {
181                            let internal_code = if $int_code == $std_code {
182                                None
183                            } else {
184                                Some($int_code)
185                            };
186
187                            let internal_name = if $int_name == $std_name {
188                                None
189                            } else {
190                                Some($int_name)
191                            };
192
193                            $crate::helpers::http_code_helper::HttpCode {
194                                standard_code: $std_code,
195                                standard_name: $std_name,
196                                unified_description: $desc,
197                                internal_code,
198                                internal_name,
199                            }
200                        },
201                    )*
202                }
203            }
204
205            /// Returns the internal code (u16) of the response.
206            pub fn internal_code(&self) -> u16 {
207                match self {
208                    Self::$first_variant => $int_code_first,
209                    $(
210                        Self::$variant => $int_code,
211                    )*
212                }
213            }
214
215            /// Constructs an enum variant from a given u16 code.
216            /// It first checks standard codes, then internal codes.
217            pub fn from_u16(code: u16) -> Option<Self> {
218                if code == $std_code_first { return Some(Self::$first_variant); }
219                $(
220                    if code == $std_code { return Some(Self::$variant); }
221                )*
222                if code == $int_code_first { return Some(Self::$first_variant); }
223                $(
224                    if code == $int_code { return Some(Self::$variant); }
225                )*
226                None
227            }
228
229            /// Attempts to create a standardized enumeration variant from the HTTP code `U16 '' which is internal to it.
230            /// returns "none" if no variant corresponds.
231            pub fn from_internal_code(code: u16) -> Option<Self> {
232              match code {
233                  $int_code_first => Some(Self::$first_variant),
234                  $(
235                      $int_code => Some(Self::$variant),
236                  )*
237                  _ => None,
238              }
239            }
240
241            /// Returns a unified tuple representation.
242            pub fn as_tuple(&self) -> $crate::helpers::unified_tuple_helper::UnifiedTuple {
243                $crate::helpers::unified_tuple_helper::UnifiedTuple {
244                    standard_code: self.get_code(),
245                    standard_name: self.get_name(),
246                    unified_description: self.get_description(),
247                    internal_code: Some(match self {
248                        Self::$first_variant => $int_code_first,
249                        $(Self::$variant => $int_code,)*
250                    }),
251                    internal_name: Some(match self {
252                        Self::$first_variant => $int_std_name_first,
253                        $(Self::$variant => $int_name,)*
254                    }),
255                }
256            }
257
258
259            /// Returns a JSON representation of the response code.
260            pub fn as_json(&self) -> serde_json::Value {
261                serde_json::to_value(self).unwrap()
262            }
263        }
264
265        /// Implementation for converting the enum into a tuple `(u16, &'static str)`.
266        impl $crate::traits::tuple_traits::IntoTwoFieldsTuple for $enum_name {
267            fn into_two_fields_tuple(self) -> $crate::helpers::two_fields_tuple_helper::TwoFieldsTuple {
268                let http_code = self.to_http_code();
269                http_code.into_two_fields_tuple()
270            }
271        }
272
273        /// Implementation for converting the enum into a tuple '(u16, &'static str, &'static str)'.
274        impl $crate::traits::tuple_traits::IntoThreeFieldsTuple for $enum_name {
275            fn into_three_fields_tuple(self) -> $crate::helpers::three_fields_tuple_helper::ThreeFieldsTuple {
276                let http_code = self.to_http_code();
277                http_code.into_three_fields_tuple()
278            }
279        }
280
281        /// Implementation of the `GetCode` trait for the enum.
282        impl GetCode for $enum_name {
283            fn get_code(&self) -> u16 {
284                match self {
285                    Self::$first_variant => $std_code_first,
286                    $(
287                        Self::$variant => $std_code,
288                    )*
289                }
290            }
291        }
292
293
294        /// Implementation of the `From` trait for converting the enum into a tuple `(u16, &'static str)`.
295        impl From<$enum_name> for (u16, &'static str) {
296            fn from(value: $enum_name) -> Self {
297                (value.get_code(), value.get_description())
298            }
299        }
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use crate::helpers::unified_tuple_helper::UnifiedTuple;
306    use crate::responses::ResponsesClientCodes;
307    use crate::traits::tuple_traits::{IntoThreeFieldsTuple, IntoTwoFieldsTuple};
308    use crate::ResponsesSuccessCodes;
309    use serde_json::json;
310
311    #[test]
312    fn test_get_description() {
313        let ex = ResponsesClientCodes::BadRequest;
314        assert_eq!(ex.get_description(), "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)." );
315    }
316    #[test]
317    fn test_get_code() {
318        let ex = ResponsesClientCodes::BadRequest;
319        assert_eq!(ex.get_code(), 400);
320    }
321
322    #[test]
323    fn test_get_name() {
324        let ex = ResponsesClientCodes::BadRequest;
325        assert_eq!(ex.get_name(), "Bad Request");
326    }
327
328    #[test]
329    fn test_to_http_code() {
330        let ex = ResponsesClientCodes::PageExpired;
331        let expected = crate::helpers::http_code_helper::HttpCode::new(
332            401,
333            "Unauthorized",
334            "Although the HTTP standard specifies 'unauthorized', semantically this response means 'unauthenticated'. That is, the client must authenticate itself to get the requested response.",
335            419,
336            "PageExpired");
337
338        assert_eq!(ex.to_http_code(), expected);
339    }
340
341    #[test]
342    fn test_from_internal_code() {
343        assert_eq!(
344            ResponsesClientCodes::from_internal_code(444),
345            Some(ResponsesClientCodes::NoResponse)
346        );
347        assert_eq!(
348            ResponsesClientCodes::from_internal_code(445),
349            Some(ResponsesClientCodes::TooManyForwardedIPAddresses)
350        );
351        assert_eq!(ResponsesClientCodes::from_internal_code(492), None);
352    }
353
354    #[test]
355    fn test_as_tuple() {
356        let ex = ResponsesClientCodes::BadRequest;
357        let expected = UnifiedTuple {
358            standard_code: 400,
359            standard_name: "Bad Request",
360            unified_description:
361                "The server cannot or will not process the request due to something \
362                that is perceived to be a client error (e.g., malformed request \
363                syntax, invalid request message framing, or deceptive request \
364                routing).",
365            internal_code: Some(400),
366            internal_name: Some("Bad Request"),
367        };
368
369        assert_eq!(ex.as_tuple(), expected);
370    }
371
372    #[test]
373    fn test_as_json() {
374        // Use the BadRequest variant from the client error responses.
375        let ex = ResponsesClientCodes::BadRequest;
376        let ex_json = ex.as_json();
377
378        // Define the expected JSON with exact field names.
379        let expected_json = json!({
380            "type": "Client errors",
381            "details": {
382                "standard http code": {
383                    "code": 400,
384                    "name": "Bad Request"
385                },
386                "description": "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).",
387                "internal http code": {
388                    "code": null,
389                    "name": null
390                }
391            }
392        });
393
394        // Assert that both JSON structures are identical.
395        assert_eq!(ex_json, expected_json);
396    }
397
398    #[test]
399    fn test_from_u16() {
400        assert_eq!(ResponsesClientCodes::from_u16(400), Some(ResponsesClientCodes::BadRequest));
401        assert_eq!(
402            ResponsesClientCodes::from_u16(456),
403            Some(ResponsesClientCodes::UnrecoverableError)
404        );
405        assert_eq!(ResponsesClientCodes::from_u16(500), None);
406    }
407
408    #[test]
409    fn test_internal_code() {
410        let ex = ResponsesClientCodes::NoResponse;
411        assert_eq!(ex.internal_code(), 444);
412    }
413
414    #[test]
415    fn test_into_two_fields_tuple() {
416        let response = ResponsesClientCodes::BadRequest;
417        let tuple = response.into_two_fields_tuple();
418        let json_result = serde_json::to_value(&tuple).unwrap();
419
420        let expected = json!({
421            "code": 400,
422            "name": "Bad Request"
423        });
424        assert_eq!(json_result, expected);
425    }
426
427    #[test]
428    fn test_into_three_fields_tuple() {
429        let response = ResponsesSuccessCodes::Ok;
430        let tuple = response.into_three_fields_tuple();
431        let json_result = serde_json::to_value(&tuple).unwrap();
432
433        let expected = json!({
434            "code": 200,
435            "name": "OK",
436            "description": "Request processed successfully. Response will depend on the request method used, and the result will be either a representation of the requested resource or an empty response"
437        });
438        assert_eq!(json_result, expected);
439    }
440}