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
33impl HttpExtension {
34 pub fn set_request_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
38 self.request_headers.insert(name.into(), value.into());
39 }
40
41 pub fn get_request_header(&self, name: &str) -> Option<&str> {
43 get_header_ci(&self.request_headers, name)
44 }
45
46 pub fn has_request_header(&self, name: &str) -> bool {
48 self.get_request_header(name).is_some()
49 }
50
51 pub fn add_request_header(
53 &mut self,
54 name: impl Into<String>,
55 value: impl Into<String>,
56 ) -> bool {
57 let name = name.into();
58 if self.has_request_header(&name) {
59 return false;
60 }
61 self.request_headers.insert(name, value.into());
62 true
63 }
64
65 pub fn remove_request_header(&mut self, name: &str) -> Option<String> {
67 remove_header_ci(&mut self.request_headers, name)
68 }
69
70 pub fn set_response_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
74 self.response_headers.insert(name.into(), value.into());
75 }
76
77 pub fn get_response_header(&self, name: &str) -> Option<&str> {
79 get_header_ci(&self.response_headers, name)
80 }
81
82 pub fn has_response_header(&self, name: &str) -> bool {
84 self.get_response_header(name).is_some()
85 }
86
87 pub fn set_header(&mut self, name: impl Into<String>, value: impl Into<String>) {
91 self.set_request_header(name, value);
92 }
93
94 pub fn get_header(&self, name: &str) -> Option<&str> {
96 self.get_request_header(name)
97 }
98
99 pub fn has_header(&self, name: &str) -> bool {
101 self.has_request_header(name)
102 }
103}
104
105fn get_header_ci<'a>(headers: &'a HashMap<String, String>, name: &str) -> Option<&'a str> {
108 let lower = name.to_lowercase();
109 headers
110 .iter()
111 .find(|(k, _)| k.to_lowercase() == lower)
112 .map(|(_, v)| v.as_str())
113}
114
115fn remove_header_ci(headers: &mut HashMap<String, String>, name: &str) -> Option<String> {
116 let lower = name.to_lowercase();
117 let key = headers.keys().find(|k| k.to_lowercase() == lower).cloned();
118 key.and_then(|k| headers.remove(&k))
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_request_header_set_and_get() {
127 let mut http = HttpExtension::default();
128 http.set_request_header("Content-Type", "application/json");
129 assert_eq!(
130 http.get_request_header("Content-Type"),
131 Some("application/json")
132 );
133 }
134
135 #[test]
136 fn test_request_header_case_insensitive() {
137 let mut http = HttpExtension::default();
138 http.set_request_header("Authorization", "Bearer tok");
139 assert_eq!(http.get_request_header("authorization"), Some("Bearer tok"));
140 assert_eq!(http.get_request_header("AUTHORIZATION"), Some("Bearer tok"));
141 }
142
143 #[test]
144 fn test_response_header_set_and_get() {
145 let mut http = HttpExtension::default();
146 http.set_response_header("Content-Type", "text/html");
147 assert_eq!(http.get_response_header("Content-Type"), Some("text/html"));
148 assert!(http.has_response_header("content-type"));
149 }
150
151 #[test]
152 fn test_request_and_response_independent() {
153 let mut http = HttpExtension::default();
154 http.set_request_header("Authorization", "Bearer req-tok");
155 http.set_response_header("X-Response-Time", "42ms");
156
157 assert!(http.get_response_header("Authorization").is_none());
159 assert!(http.get_request_header("X-Response-Time").is_none());
161 }
162
163 #[test]
164 fn test_convenience_aliases_default_to_request() {
165 let mut http = HttpExtension::default();
166 http.set_header("X-Custom", "value");
167 assert_eq!(http.get_header("X-Custom"), Some("value"));
168 assert!(http.has_header("X-Custom"));
169 assert_eq!(http.get_request_header("X-Custom"), Some("value"));
171 }
172
173 #[test]
174 fn test_add_request_header_only_if_absent() {
175 let mut http = HttpExtension::default();
176 assert!(http.add_request_header("X-New", "first"));
177 assert!(!http.add_request_header("X-New", "second"));
178 assert_eq!(http.get_request_header("X-New"), Some("first"));
179 }
180
181 #[test]
182 fn test_remove_request_header() {
183 let mut http = HttpExtension::default();
184 http.set_request_header("X-Remove", "value");
185 let removed = http.remove_request_header("x-remove");
186 assert_eq!(removed, Some("value".to_string()));
187 assert!(!http.has_request_header("X-Remove"));
188 }
189
190 #[test]
191 fn test_serde_roundtrip() {
192 let mut http = HttpExtension::default();
193 http.set_request_header("Authorization", "Bearer tok");
194 http.set_request_header("X-Request-ID", "req-123");
195 http.set_response_header("Content-Type", "application/json");
196 http.set_response_header("X-Response-Time", "15ms");
197
198 let json = serde_json::to_string(&http).unwrap();
199 let deserialized: HttpExtension = serde_json::from_str(&json).unwrap();
200
201 assert_eq!(
202 deserialized.get_request_header("Authorization"),
203 Some("Bearer tok")
204 );
205 assert_eq!(
206 deserialized.get_response_header("Content-Type"),
207 Some("application/json")
208 );
209 }
210}