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}