ironshield_types/
request.rs

1use serde::{Deserialize, Serialize};
2
3#[cfg(feature = "openapi")]
4#[allow(unused_imports)]
5use serde_json::json;
6
7/// * `endpoint`:  The endpoint URL for the request.
8/// * `timestamp`: The timestamp of the request in unix millis.
9#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
10#[cfg_attr(feature = "openapi", schema(
11    description = "Request structure for an IronShield proof-of-work challenge",
12    example = json!({
13        "endpoint": "https://example.com",
14        "timestamp": 1704067200000i64
15    })
16))]
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct IronShieldRequest {
19    /// The protected endpoint URL (must be HTTPS)
20    #[cfg_attr(feature = "openapi", schema(example = "https://example.com"))]
21    pub endpoint:  String,
22    /// Request timestamp in Unix milliseconds (use Date.now() in JavaScript)
23    #[cfg_attr(feature = "openapi", schema(example = 1704067200000i64))]
24    pub timestamp: i64,
25}
26
27impl IronShieldRequest {
28    /// Constructor for creating a new IronShieldRequest instance.
29    pub fn new(
30        endpoint:  String,
31        timestamp: i64
32    ) -> Self {
33        Self { 
34            endpoint,
35            timestamp,
36        }
37    }
38    
39    /// Concatenates the request data into a string.
40    /// 
41    /// Concatenates:
42    /// - `endpoint`:  as a string.
43    /// - `timestamp`: as a string.
44    pub fn concat_struct(&self) -> String {
45        format!(
46            "{}|{}",
47            self.endpoint,
48            self.timestamp
49        )
50    }
51    
52    /// Creates an `IronShieldRequest` from a concatenated string.
53    /// 
54    /// This function reverses the operation of
55    /// `IronShieldRequest::concat_struct`.
56    /// Expects a string in the format: "endpoint|timestamp".
57    /// 
58    /// # Arguments
59    /// * `concat_string`: The concatenated string to parse, typically
60    ///                    generated by `concat_struct()`.
61    /// 
62    /// # Returns
63    /// * `Result<Self, String>`: A result containing the parsed
64    ///                           `IronShieldRequest`
65    ///                           or an error message if parsing fails.
66    pub fn from_concat_struct(concat_string: &str) -> Result<Self, String> {
67        let parts: Vec<&str> = concat_string.split('|').collect();
68
69        if parts.len() != 2 {
70            return Err("Invalid format, expected 'endpoint|timestamp'".to_string());
71        }
72
73        let endpoint = parts[0].to_string();
74        let timestamp = parts[1].parse::<i64>()
75            .map_err(|_| "Failed to parse timestamp".to_string())?;
76
77        Ok(Self { endpoint, timestamp })
78    }
79
80    /// Encodes the response as a base64url string for HTTP header transport.
81    ///
82    /// This method concatenates all response fields using the established `|` delimiter
83    /// format, and then base64url-encodes the result for safe transport in HTTP headers.
84    ///
85    /// # Returns
86    /// * `String`: Base64url-encoded string ready for HTTP header use
87    ///
88    /// # Example
89    /// ```
90    /// use ironshield_types::IronShieldRequest;
91    /// let request = IronShieldRequest::new("https://example.com/api".to_string(), 123456789);
92    /// let header_value = request.to_base64url_header();
93    /// ```
94    pub fn to_base64url_header(&self) -> String {
95        crate::serde_utils::concat_struct_base64url_encode(&self.concat_struct())
96    }
97
98    /// Decodes a base64url-encoded response from an HTTP header.
99    ///
100    /// This method reverses the `to_base64url_header()` operation by first base64url-decoding
101    /// the input string and then parsing it using the established `|` delimiter format.
102    ///
103    /// # Arguments
104    /// * `encoded_header`: The base64url-encoded string from the HTTP header.
105    ///
106    /// # Returns
107    /// * `Result<Self, String>`: Decoded response or detailed error message.
108    ///
109    /// # Example
110    /// ```
111    /// use ironshield_types::IronShieldRequest;
112    /// // Create a response and encode it.
113    /// let original = IronShieldRequest::new("https://example.com/api".to_string(), 123456789);
114    /// let header_value = original.to_base64url_header();
115    /// // Decode it back.
116    /// let decoded = IronShieldRequest::from_base64url_header(&header_value).unwrap();
117    /// assert_eq!(original.endpoint, decoded.endpoint);
118    /// ```
119    pub fn from_base64url_header(encoded_header: &str) -> Result<Self, String> {
120        // Decode using the existing serde_utils function.
121        let concat_str: String = crate::serde_utils::concat_struct_base64url_decode(encoded_header.to_string())?;
122        // Parse using the existing concat_struct format.
123        Self::from_concat_struct(&concat_str)
124    }
125}