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