1use indexmap::IndexMap;
2
3use utoipa::openapi::Required;
4use utoipa::openapi::path::{Parameter, ParameterBuilder, ParameterIn};
5
6use super::ApiClientError;
7use super::param::{ParamValue, ParameterValue, ResolvedParamValue};
8use super::schema::Schemas;
9
10#[derive(Debug, Clone, Default)]
15pub struct CallHeaders {
16 headers: IndexMap<String, ResolvedParamValue>,
17 pub(super) schemas: Schemas,
18}
19
20impl CallHeaders {
21 pub fn new() -> Self {
23 Self::default()
24 }
25
26 pub fn add_header<T: ParameterValue>(
50 mut self,
51 name: impl Into<String>,
52 value: impl Into<ParamValue<T>>,
53 ) -> Self {
54 let name = name.into();
55 let param_value = value.into();
56
57 let schema = self.schemas.add::<T>();
59
60 let resolved = ResolvedParamValue {
62 value: param_value
63 .as_header_value()
64 .expect("Header serialization should not fail"),
65 schema,
66 style: param_value.header_style(),
67 };
68
69 self.headers.insert(name, resolved);
70 self
71 }
72
73 pub fn merge(mut self, other: Self) -> Self {
77 self.schemas.merge(other.schemas);
79
80 for (name, value) in other.headers {
82 self.headers.insert(name, value);
83 }
84
85 self
86 }
87
88 pub fn is_empty(&self) -> bool {
90 self.headers.is_empty()
91 }
92
93 pub fn len(&self) -> usize {
95 self.headers.len()
96 }
97
98 pub(super) fn to_parameters(&self) -> impl Iterator<Item = Parameter> + '_ {
100 self.headers.iter().map(|(name, resolved)| {
101 ParameterBuilder::new()
102 .name(name)
103 .parameter_in(ParameterIn::Header)
104 .required(Required::False) .schema(Some(resolved.schema.clone()))
106 .build()
107 })
108 }
109
110 pub(super) fn to_http_headers(&self) -> Result<Vec<(String, String)>, ApiClientError> {
112 let mut result = Vec::new();
113
114 for (name, resolved) in &self.headers {
115 let value = resolved.to_string_value()?;
116 result.push((name.clone(), value));
117 }
118
119 Ok(result)
120 }
121
122 pub(super) fn schemas(&self) -> &Schemas {
124 &self.schemas
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::client::ParamStyle;
132 use indexmap::IndexMap;
133 use serde::{Deserialize, Serialize};
134 use utoipa::ToSchema;
135
136 #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
137 struct TestId(u64);
138
139 #[test]
140 fn test_new_empty_headers() {
141 let headers = CallHeaders::new();
142
143 assert!(headers.is_empty());
144 assert_eq!(headers.len(), 0);
145 }
146
147 #[test]
148 fn test_add_string_header() {
149 let headers = CallHeaders::new().add_header("Authorization", "Bearer token123");
150
151 assert!(!headers.is_empty());
152 assert_eq!(headers.len(), 1);
153
154 let http_headers = headers
155 .to_http_headers()
156 .expect("Should convert to HTTP headers");
157 assert_eq!(
158 http_headers,
159 vec![("Authorization".to_string(), "Bearer token123".to_string())]
160 );
161 }
162
163 #[test]
164 fn test_add_multiple_headers() {
165 let headers = CallHeaders::new()
166 .add_header("Authorization", "Bearer token123")
167 .add_header("X-Request-ID", "abc-123-def")
168 .add_header("Content-Type", "application/json");
169
170 assert_eq!(headers.len(), 3);
171
172 let http_headers = headers
173 .to_http_headers()
174 .expect("Should convert to HTTP headers");
175 assert_eq!(http_headers.len(), 3);
176
177 let header_map: IndexMap<String, String> = http_headers.into_iter().collect();
179 assert_eq!(
180 header_map.get("Authorization"),
181 Some(&"Bearer token123".to_string())
182 );
183 assert_eq!(
184 header_map.get("X-Request-ID"),
185 Some(&"abc-123-def".to_string())
186 );
187 assert_eq!(
188 header_map.get("Content-Type"),
189 Some(&"application/json".to_string())
190 );
191
192 let keys: Vec<_> = header_map.keys().cloned().collect();
194 assert_eq!(keys, vec!["Authorization", "X-Request-ID", "Content-Type"]);
195 }
196
197 #[test]
198 fn test_add_numeric_header() {
199 let headers = CallHeaders::new().add_header("X-Rate-Limit", 1000u32);
200
201 let http_headers = headers
202 .to_http_headers()
203 .expect("Should convert to HTTP headers");
204 assert_eq!(
205 http_headers,
206 vec![("X-Rate-Limit".to_string(), "1000".to_string())]
207 );
208 }
209
210 #[test]
211 fn test_add_custom_type_header() {
212 let headers = CallHeaders::new().add_header("X-User-ID", TestId(42));
213
214 let http_headers = headers
215 .to_http_headers()
216 .expect("Should convert to HTTP headers");
217 assert_eq!(
218 http_headers,
219 vec![("X-User-ID".to_string(), "42".to_string())]
220 );
221 }
222
223 #[test]
224 fn test_add_header_with_param_style() {
225 let headers = CallHeaders::new().add_header(
226 "X-Tags",
227 ParamValue::with_style(vec!["rust", "web", "api"], ParamStyle::Simple),
228 );
229
230 let http_headers = headers
231 .to_http_headers()
232 .expect("Should convert to HTTP headers");
233 assert_eq!(
234 http_headers,
235 vec![("X-Tags".to_string(), "rust,web,api".to_string())]
236 );
237 }
238
239 #[test]
240 fn test_header_merge() {
241 let headers1 = CallHeaders::new()
242 .add_header("Authorization", "Bearer token123")
243 .add_header("X-Request-ID", "abc-123-def");
244
245 let headers2 = CallHeaders::new()
246 .add_header("Content-Type", "application/json")
247 .add_header("X-Request-ID", "xyz-789-ghi"); let merged = headers1.merge(headers2);
250
251 assert_eq!(merged.len(), 3);
252
253 let http_headers = merged
254 .to_http_headers()
255 .expect("Should convert to HTTP headers");
256 let header_map: IndexMap<String, String> = http_headers.into_iter().collect();
257
258 assert_eq!(
259 header_map.get("Authorization"),
260 Some(&"Bearer token123".to_string())
261 );
262 assert_eq!(
263 header_map.get("X-Request-ID"),
264 Some(&"xyz-789-ghi".to_string())
265 ); assert_eq!(
267 header_map.get("Content-Type"),
268 Some(&"application/json".to_string())
269 );
270
271 let keys: Vec<_> = header_map.keys().cloned().collect();
273 assert_eq!(keys, vec!["Authorization", "X-Request-ID", "Content-Type"]);
274 }
275
276 #[test]
277 fn test_headers_to_parameters() {
278 let headers = CallHeaders::new()
279 .add_header("Authorization", "Bearer token123")
280 .add_header("X-Rate-Limit", 1000u32);
281
282 let parameters: Vec<Parameter> = headers.to_parameters().collect();
283
284 assert_eq!(parameters.len(), 2);
285
286 for param in ¶meters {
288 assert_eq!(param.parameter_in, ParameterIn::Header);
289 assert_eq!(param.required, Required::False);
290 assert!(param.schema.is_some());
291 assert!(param.name == "Authorization" || param.name == "X-Rate-Limit");
292 }
293 }
294
295 #[test]
296 fn test_empty_headers_merge() {
297 let headers1 = CallHeaders::new().add_header("Authorization", "Bearer token123");
298
299 let headers2 = CallHeaders::new();
300
301 let merged = headers1.merge(headers2);
302 assert_eq!(merged.len(), 1);
303
304 let http_headers = merged
305 .to_http_headers()
306 .expect("Should convert to HTTP headers");
307 assert_eq!(
308 http_headers,
309 vec![("Authorization".to_string(), "Bearer token123".to_string())]
310 );
311 }
312
313 #[test]
314 fn test_headers_schema_collection() {
315 let headers = CallHeaders::new()
316 .add_header("Authorization", "Bearer token123")
317 .add_header("X-User-ID", TestId(42));
318
319 let schemas = headers.schemas();
320
321 assert!(!schemas.schema_vec().is_empty());
323 }
324
325 #[test]
326 fn test_header_insertion_order_preserved() {
327 let headers = CallHeaders::new()
328 .add_header("First", "value1")
329 .add_header("Second", "value2")
330 .add_header("Third", "value3")
331 .add_header("Fourth", "value4");
332
333 let http_headers = headers
334 .to_http_headers()
335 .expect("Should convert to HTTP headers");
336
337 let actual_order: Vec<String> = http_headers.iter().map(|(name, _)| name.clone()).collect();
339 let expected_order = vec!["First", "Second", "Third", "Fourth"];
340
341 assert_eq!(actual_order, expected_order);
342 }
343
344 #[test]
345 fn test_header_with_array_values() {
346 let headers = CallHeaders::new()
347 .add_header("X-Tags", vec!["rust", "web", "api"])
348 .add_header("X-Numbers", vec![1, 2, 3]);
349
350 let http_headers = headers
351 .to_http_headers()
352 .expect("Should convert to HTTP headers");
353
354 let header_map: IndexMap<String, String> = http_headers.into_iter().collect();
355
356 assert_eq!(header_map.get("X-Tags"), Some(&"rust,web,api".to_string()));
358 assert_eq!(header_map.get("X-Numbers"), Some(&"1,2,3".to_string()));
359 }
360
361 #[test]
362 fn test_header_with_boolean_values() {
363 let headers = CallHeaders::new()
364 .add_header("X-Debug", true)
365 .add_header("X-Enabled", false);
366
367 let http_headers = headers
368 .to_http_headers()
369 .expect("Should convert to HTTP headers");
370
371 let header_map: IndexMap<String, String> = http_headers.into_iter().collect();
372
373 assert_eq!(header_map.get("X-Debug"), Some(&"true".to_string()));
374 assert_eq!(header_map.get("X-Enabled"), Some(&"false".to_string()));
375 }
376
377 #[test]
378 fn test_header_with_null_value() {
379 let headers = CallHeaders::new().add_header("X-Optional", serde_json::Value::Null);
380
381 let http_headers = headers
382 .to_http_headers()
383 .expect("Should convert to HTTP headers");
384
385 let header_map: IndexMap<String, String> = http_headers.into_iter().collect();
386
387 assert_eq!(header_map.get("X-Optional"), Some(&String::new()));
389 }
390
391 #[test]
392 fn test_header_error_with_complex_object() {
393 use serde_json::json;
394
395 let mut headers = CallHeaders::new();
400
401 let complex_value = json!({
403 "nested": {
404 "object": "not supported in headers"
405 }
406 });
407
408 let resolved = ResolvedParamValue {
409 value: complex_value,
410 schema: headers.schemas.add::<serde_json::Value>(),
411 style: ParamStyle::Simple,
412 };
413
414 headers.headers.insert("X-Complex".to_string(), resolved);
415
416 let result = headers.to_http_headers();
418 assert!(
419 result.is_err(),
420 "Complex objects should cause error in headers"
421 );
422
423 match result {
424 Err(ApiClientError::UnsupportedParameterValue { .. }) => {
425 }
427 _ => panic!("Expected UnsupportedParameterValue error for complex object in header"),
428 }
429 }
430
431 #[test]
432 fn test_header_error_with_array_containing_objects() {
433 use serde_json::json;
434
435 let mut headers = CallHeaders::new();
437
438 let array_with_objects = json!([
439 "simple_string",
440 {"nested": "object"}
441 ]);
442
443 let resolved = ResolvedParamValue {
444 value: array_with_objects,
445 schema: headers.schemas.add::<serde_json::Value>(),
446 style: ParamStyle::Simple,
447 };
448
449 headers
450 .headers
451 .insert("X-Invalid-Array".to_string(), resolved);
452
453 let result = headers.to_http_headers();
454 assert!(
455 result.is_err(),
456 "Arrays containing objects should cause error"
457 );
458
459 match result {
460 Err(ApiClientError::UnsupportedParameterValue { .. }) => {
461 }
463 _ => panic!("Expected UnsupportedParameterValue error for array with objects"),
464 }
465 }
466
467 #[test]
468 fn test_header_with_empty_array() {
469 let headers = CallHeaders::new().add_header("X-Empty-List", Vec::<String>::new());
470
471 let http_headers = headers
472 .to_http_headers()
473 .expect("Should handle empty arrays");
474
475 let header_map: IndexMap<String, String> = http_headers.into_iter().collect();
476
477 assert_eq!(header_map.get("X-Empty-List"), Some(&String::new()));
479 }
480
481 #[test]
482 fn test_header_override_in_merge() {
483 let headers1 = CallHeaders::new()
484 .add_header("Same-Header", "original-value")
485 .add_header("Unique-1", "value1");
486
487 let headers2 = CallHeaders::new()
488 .add_header("Same-Header", "new-value")
489 .add_header("Unique-2", "value2");
490
491 let merged = headers1.merge(headers2);
492
493 let http_headers = merged
494 .to_http_headers()
495 .expect("Should convert merged headers");
496
497 let header_map: IndexMap<String, String> = http_headers.into_iter().collect();
498
499 assert_eq!(
501 header_map.get("Same-Header"),
502 Some(&"new-value".to_string())
503 );
504 assert_eq!(header_map.get("Unique-1"), Some(&"value1".to_string()));
505 assert_eq!(header_map.get("Unique-2"), Some(&"value2".to_string()));
506 assert_eq!(header_map.len(), 3);
507 }
508}