cpex_core/extensions/
http.rs1use std::collections::HashMap;
10
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
23pub struct HttpExtension {
24 #[serde(default)]
26 pub request_headers: HashMap<String, String>,
27
28 #[serde(default)]
30 pub response_headers: HashMap<String, String>,
31
32 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub method: Option<String>,
36
37 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub path: Option<String>,
41
42 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub host: Option<String>,
48
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub scheme: Option<String>,
52}
53
54impl HttpExtension {
55 pub fn set_request_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
59 self.request_headers.insert(name.into(), value.into());
60 }
61
62 pub fn get_request_header(&self, name: &str) -> Option<&str> {
64 get_header_ci(&self.request_headers, name)
65 }
66
67 pub fn has_request_header(&self, name: &str) -> bool {
69 self.get_request_header(name).is_some()
70 }
71
72 pub fn add_request_header(
74 &mut self,
75 name: impl Into<String>,
76 value: impl Into<String>,
77 ) -> bool {
78 let name = name.into();
79 if self.has_request_header(&name) {
80 return false;
81 }
82 self.request_headers.insert(name, value.into());
83 true
84 }
85
86 pub fn remove_request_header(&mut self, name: &str) -> Option<String> {
88 remove_header_ci(&mut self.request_headers, name)
89 }
90
91 pub fn set_response_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
95 self.response_headers.insert(name.into(), value.into());
96 }
97
98 pub fn get_response_header(&self, name: &str) -> Option<&str> {
100 get_header_ci(&self.response_headers, name)
101 }
102
103 pub fn has_response_header(&self, name: &str) -> bool {
105 self.get_response_header(name).is_some()
106 }
107
108 pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
112 self.set_request_header(name, value);
113 }
114
115 pub fn get_header(&self, name: &str) -> Option<&str> {
117 self.get_request_header(name)
118 }
119
120 pub fn has_header(&self, name: &str) -> bool {
122 self.has_request_header(name)
123 }
124}
125
126fn get_header_ci<'a>(headers: &'a HashMap<String, String>, name: &str) -> Option<&'a str> {
129 let lower = name.to_lowercase();
130 headers
131 .iter()
132 .find(|(k, _)| k.to_lowercase() == lower)
133 .map(|(_, v)| v.as_str())
134}
135
136fn remove_header_ci(headers: &mut HashMap<String, String>, name: &str) -> Option<String> {
137 let lower = name.to_lowercase();
138 let key = headers.keys().find(|k| k.to_lowercase() == lower).cloned();
139 key.and_then(|k| headers.remove(&k))
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_request_header_set_and_get() {
148 let mut http = HttpExtension::default();
149 http.set_request_header("Content-Type", "application/json");
150 assert_eq!(
151 http.get_request_header("Content-Type"),
152 Some("application/json")
153 );
154 }
155
156 #[test]
157 fn test_request_header_case_insensitive() {
158 let mut http = HttpExtension::default();
159 http.set_request_header("Authorization", "Bearer tok");
160 assert_eq!(http.get_request_header("authorization"), Some("Bearer tok"));
161 assert_eq!(http.get_request_header("AUTHORIZATION"), Some("Bearer tok"));
162 }
163
164 #[test]
165 fn test_response_header_set_and_get() {
166 let mut http = HttpExtension::default();
167 http.set_response_header("Content-Type", "text/html");
168 assert_eq!(http.get_response_header("Content-Type"), Some("text/html"));
169 assert!(http.has_response_header("content-type"));
170 }
171
172 #[test]
173 fn test_request_and_response_independent() {
174 let mut http = HttpExtension::default();
175 http.set_request_header("Authorization", "Bearer req-tok");
176 http.set_response_header("X-Response-Time", "42ms");
177
178 assert!(http.get_response_header("Authorization").is_none());
180 assert!(http.get_request_header("X-Response-Time").is_none());
182 }
183
184 #[test]
185 fn test_convenience_aliases_default_to_request() {
186 let mut http = HttpExtension::default();
187 http.set_header("X-Custom", "value");
188 assert_eq!(http.get_header("X-Custom"), Some("value"));
189 assert!(http.has_header("X-Custom"));
190 assert_eq!(http.get_request_header("X-Custom"), Some("value"));
192 }
193
194 #[test]
195 fn test_add_request_header_only_if_absent() {
196 let mut http = HttpExtension::default();
197 assert!(http.add_request_header("X-New", "first"));
198 assert!(!http.add_request_header("X-New", "second"));
199 assert_eq!(http.get_request_header("X-New"), Some("first"));
200 }
201
202 #[test]
203 fn test_remove_request_header() {
204 let mut http = HttpExtension::default();
205 http.set_request_header("X-Remove", "value");
206 let removed = http.remove_request_header("x-remove");
207 assert_eq!(removed, Some("value".to_string()));
208 assert!(!http.has_request_header("X-Remove"));
209 }
210
211 #[test]
212 fn test_serde_roundtrip() {
213 let mut http = HttpExtension::default();
214 http.set_request_header("Authorization", "Bearer tok");
215 http.set_request_header("X-Request-ID", "req-123");
216 http.set_response_header("Content-Type", "application/json");
217 http.set_response_header("X-Response-Time", "15ms");
218
219 let json = serde_json::to_string(&http).unwrap();
220 let deserialized: HttpExtension = serde_json::from_str(&json).unwrap();
221
222 assert_eq!(
223 deserialized.get_request_header("Authorization"),
224 Some("Bearer tok")
225 );
226 assert_eq!(
227 deserialized.get_response_header("Content-Type"),
228 Some("application/json")
229 );
230 }
231}