clawspec_core/client/call/builder.rs
1use std::ops::{Range, RangeInclusive};
2
3use serde::Serialize;
4use utoipa::ToSchema;
5
6use super::ApiCall;
7use crate::client::parameters::{ParamValue, ParameterValue};
8use crate::client::response::ExpectedStatusCodes;
9#[cfg(feature = "redaction")]
10use crate::client::response::RequestBodyRedactionBuilder;
11use crate::client::security::SecurityRequirement;
12use crate::client::{ApiClientError, CallBody, CallCookies, CallHeaders, CallQuery};
13
14impl ApiCall {
15 // =============================================================================
16 // OpenAPI Metadata Methods
17 // =============================================================================
18 pub fn with_operation_id(mut self, operation_id: impl Into<String>) -> Self {
19 self.metadata.operation_id = operation_id.into();
20 self
21 }
22
23 /// Sets the operation description for OpenAPI documentation.
24 ///
25 /// # Examples
26 ///
27 /// ```rust
28 /// # use clawspec_core::ApiClient;
29 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
30 /// let mut client = ApiClient::builder().build()?;
31 /// let call = client.get("/users")?.with_description("Retrieve all users");
32 /// # Ok(())
33 /// # }
34 /// ```
35 pub fn with_description(mut self, description: impl Into<String>) -> Self {
36 self.metadata.description = Some(description.into());
37 self
38 }
39
40 /// Sets the operation tags for OpenAPI categorization.
41 ///
42 /// # Examples
43 ///
44 /// ```rust
45 /// # use clawspec_core::ApiClient;
46 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
47 /// let mut client = ApiClient::builder().build()?;
48 /// let call = client.get("/users")?.with_tags(vec!["users", "admin"]);
49 /// // Also works with arrays, slices, or any IntoIterator
50 /// let call = client.get("/users")?.with_tags(["users", "admin"]);
51 /// # Ok(())
52 /// # }
53 /// ```
54 pub fn with_tags<I, T>(mut self, tags: I) -> Self
55 where
56 I: IntoIterator<Item = T>,
57 T: Into<String>,
58 {
59 self.metadata.tags = Some(tags.into_iter().map(|t| t.into()).collect());
60 self
61 }
62
63 /// Adds a single tag to the operation for OpenAPI categorization.
64 ///
65 /// # Examples
66 ///
67 /// ```rust
68 /// # use clawspec_core::ApiClient;
69 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
70 /// let mut client = ApiClient::builder().build()?;
71 /// let call = client.get("/users")?.with_tag("users").with_tag("admin");
72 /// # Ok(())
73 /// # }
74 /// ```
75 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
76 self.metadata
77 .tags
78 .get_or_insert_with(Vec::new)
79 .push(tag.into());
80 self
81 }
82
83 /// Sets a response description for the actual returned status code.
84 ///
85 /// This method allows you to document what the response means for your API endpoint.
86 /// The description will be applied to whatever status code is actually returned by the server
87 /// and included in the generated OpenAPI specification.
88 ///
89 /// **Note**: This method is only available with the `redaction` feature enabled.
90 ///
91 /// # Examples
92 ///
93 /// ```rust
94 /// # use clawspec_core::ApiClient;
95 /// # #[cfg(feature = "redaction")]
96 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
97 /// let mut client = ApiClient::builder().build()?;
98 /// let call = client.get("/users/{id}")?
99 /// .with_response_description("User details if found, or error information");
100 /// # Ok(())
101 /// # }
102 /// ```
103 #[cfg(feature = "redaction")]
104 pub fn with_response_description(mut self, description: impl Into<String>) -> Self {
105 self.response_description = Some(description.into());
106 self
107 }
108
109 /// Excludes this API call from OpenAPI collection and documentation generation.
110 ///
111 /// When called, this API call will be executed normally but will not appear
112 /// in the generated OpenAPI specification. This is useful for:
113 /// - Health check endpoints
114 /// - Debug/diagnostic endpoints
115 /// - Authentication/session management calls
116 /// - Test setup/teardown calls
117 /// - Internal utility endpoints
118 /// - Administrative endpoints not part of public API
119 ///
120 /// # Examples
121 ///
122 /// ```rust
123 /// # use clawspec_core::ApiClient;
124 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
125 /// let mut client = ApiClient::builder().build()?;
126 ///
127 /// // Health check that won't appear in OpenAPI spec
128 /// client
129 /// .get("/health")?
130 /// .without_collection()
131 /// .await?
132 /// .as_empty()
133 /// .await?;
134 ///
135 /// // Debug endpoint excluded from documentation
136 /// client
137 /// .get("/debug/status")?
138 /// .without_collection()
139 /// .await?
140 /// .as_text()
141 /// .await?;
142 /// # Ok(())
143 /// # }
144 /// ```
145 pub fn without_collection(mut self) -> Self {
146 self.skip_collection = true;
147 self
148 }
149
150 /// Sets the security requirements for this specific operation.
151 ///
152 /// This method overrides the default security configured on the client.
153 /// Use this when an endpoint requires different authentication than the default.
154 ///
155 /// # Parameters
156 ///
157 /// * `requirement` - The security requirement to apply to this operation
158 ///
159 /// # Examples
160 ///
161 /// ```rust
162 /// use clawspec_core::{ApiClient, SecurityScheme, SecurityRequirement};
163 ///
164 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
165 /// let mut client = ApiClient::builder()
166 /// .with_security_scheme("bearerAuth", SecurityScheme::bearer())
167 /// .with_security_scheme("adminAuth", SecurityScheme::bearer_with_format("JWT"))
168 /// .with_default_security(SecurityRequirement::new("bearerAuth"))
169 /// .build()?;
170 ///
171 /// // This endpoint requires admin authentication instead of the default
172 /// client
173 /// .post("/admin/users")?
174 /// .with_security(SecurityRequirement::new("adminAuth"))
175 /// .await?
176 /// .as_empty()
177 /// .await?;
178 /// # Ok(())
179 /// # }
180 /// ```
181 ///
182 /// # Generated OpenAPI
183 ///
184 /// ```yaml
185 /// paths:
186 /// /admin/users:
187 /// post:
188 /// security:
189 /// - adminAuth: []
190 /// ```
191 pub fn with_security(mut self, requirement: SecurityRequirement) -> Self {
192 self.security = Some(vec![requirement]);
193 self
194 }
195
196 /// Sets multiple security requirements for this operation (OR relationship).
197 ///
198 /// When multiple security requirements are set, they represent alternative
199 /// authentication methods. The client can satisfy any one of them.
200 ///
201 /// # Parameters
202 ///
203 /// * `requirements` - Iterator of security requirements
204 ///
205 /// # Examples
206 ///
207 /// ```rust
208 /// use clawspec_core::{ApiClient, SecurityScheme, SecurityRequirement, ApiKeyLocation};
209 ///
210 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
211 /// let mut client = ApiClient::builder()
212 /// .with_security_scheme("bearerAuth", SecurityScheme::bearer())
213 /// .with_security_scheme("apiKey", SecurityScheme::api_key("X-API-Key", ApiKeyLocation::Header))
214 /// .build()?;
215 ///
216 /// // This endpoint accepts either bearer token OR API key
217 /// client
218 /// .get("/data")?
219 /// .with_securities([
220 /// SecurityRequirement::new("bearerAuth"),
221 /// SecurityRequirement::new("apiKey"),
222 /// ])
223 /// .await?
224 /// .as_empty()
225 /// .await?;
226 /// # Ok(())
227 /// # }
228 /// ```
229 pub fn with_securities(
230 mut self,
231 requirements: impl IntoIterator<Item = SecurityRequirement>,
232 ) -> Self {
233 self.security = Some(requirements.into_iter().collect());
234 self
235 }
236
237 /// Marks this operation as not requiring authentication.
238 ///
239 /// Use this for public endpoints that don't need security, overriding
240 /// any default security configured on the client.
241 ///
242 /// # Examples
243 ///
244 /// ```rust
245 /// use clawspec_core::{ApiClient, SecurityScheme, SecurityRequirement};
246 ///
247 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
248 /// let mut client = ApiClient::builder()
249 /// .with_security_scheme("bearerAuth", SecurityScheme::bearer())
250 /// .with_default_security(SecurityRequirement::new("bearerAuth"))
251 /// .build()?;
252 ///
253 /// // Public endpoint - no authentication needed
254 /// client
255 /// .get("/public/health")?
256 /// .without_security()
257 /// .await?
258 /// .as_empty()
259 /// .await?;
260 /// # Ok(())
261 /// # }
262 /// ```
263 ///
264 /// # Generated OpenAPI
265 ///
266 /// ```yaml
267 /// paths:
268 /// /public/health:
269 /// get:
270 /// security: [] # Empty array means no security required
271 /// ```
272 pub fn without_security(mut self) -> Self {
273 self.security = Some(vec![]); // Empty array = no security required
274 self
275 }
276
277 // =============================================================================
278 // Request Configuration Methods
279 // =============================================================================
280
281 pub fn with_query(mut self, query: CallQuery) -> Self {
282 self.query = query;
283 self
284 }
285
286 pub fn with_headers_option(mut self, headers: Option<CallHeaders>) -> Self {
287 self.headers = match (self.headers.take(), headers) {
288 (Some(existing), Some(new)) => Some(existing.merge(new)),
289 (existing, new) => existing.or(new),
290 };
291 self
292 }
293
294 /// Adds headers to the API call, merging with any existing headers.
295 ///
296 /// This is a convenience method that automatically wraps the headers in Some().
297 pub fn with_headers(self, headers: CallHeaders) -> Self {
298 self.with_headers_option(Some(headers))
299 }
300
301 /// Convenience method to add a single header.
302 ///
303 /// This method automatically handles type conversion and merges with existing headers.
304 /// If a header with the same name already exists, the new value will override it.
305 ///
306 /// # Examples
307 ///
308 /// ## Basic Usage
309 /// ```rust
310 /// # use clawspec_core::ApiClient;
311 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
312 /// let mut client = ApiClient::builder().build()?;
313 /// let call = client.get("/users")?
314 /// .with_header("Authorization", "Bearer token123")
315 /// .with_header("X-Request-ID", "abc-123-def");
316 /// # Ok(())
317 /// # }
318 /// ```
319 ///
320 /// ## Type Flexibility and Edge Cases
321 /// ```rust
322 /// # use clawspec_core::ApiClient;
323 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
324 /// let mut client = ApiClient::builder().build()?;
325 ///
326 /// // Different value types are automatically converted
327 /// let call = client.post("/api/data")?
328 /// .with_header("Content-Length", 1024_u64) // Numeric values
329 /// .with_header("X-Retry-Count", 3_u32) // Different numeric types
330 /// .with_header("X-Debug", true) // Boolean values
331 /// .with_header("X-Session-ID", "session-123"); // String values
332 ///
333 /// // Headers can be chained and overridden
334 /// let call = client.get("/protected")?
335 /// .with_header("Authorization", "Bearer old-token")
336 /// .with_header("Authorization", "Bearer new-token"); // Overrides previous value
337 /// # Ok(())
338 /// # }
339 /// ```
340 pub fn with_header<T: ParameterValue>(
341 self,
342 name: impl Into<String>,
343 value: impl Into<ParamValue<T>>,
344 ) -> Self {
345 let headers = CallHeaders::new().add_header(name, value);
346 self.with_headers(headers)
347 }
348
349 /// Adds cookies to the API call, merging with any existing cookies.
350 ///
351 /// This method accepts a `CallCookies` instance and merges it with any existing
352 /// cookies on the request. Cookies are sent in the HTTP Cookie header and can
353 /// be used for session management, authentication, and storing user preferences.
354 ///
355 /// # Examples
356 ///
357 /// ```rust
358 /// # use clawspec_core::{ApiClient, CallCookies};
359 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
360 /// let mut client = ApiClient::builder().build()?;
361 /// let cookies = CallCookies::new()
362 /// .add_cookie("session_id", "abc123")
363 /// .add_cookie("user_id", 456);
364 ///
365 /// let call = client.get("/dashboard")?
366 /// .with_cookies(cookies);
367 /// # Ok(())
368 /// # }
369 /// ```
370 pub fn with_cookies(mut self, cookies: CallCookies) -> Self {
371 self.cookies = match self.cookies.take() {
372 Some(existing) => Some(existing.merge(cookies)),
373 None => Some(cookies),
374 };
375 self
376 }
377
378 /// Convenience method to add a single cookie.
379 ///
380 /// This method automatically handles type conversion and merges with existing cookies.
381 /// If a cookie with the same name already exists, the new value will override it.
382 ///
383 /// # Examples
384 ///
385 /// ## Basic Usage
386 /// ```rust
387 /// # use clawspec_core::ApiClient;
388 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
389 /// let mut client = ApiClient::builder().build()?;
390 /// let call = client.get("/dashboard")?
391 /// .with_cookie("session_id", "abc123")
392 /// .with_cookie("user_id", 456);
393 /// # Ok(())
394 /// # }
395 /// ```
396 ///
397 /// ## Type Flexibility and Edge Cases
398 /// ```rust
399 /// # use clawspec_core::ApiClient;
400 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
401 /// let mut client = ApiClient::builder().build()?;
402 ///
403 /// // Different value types are automatically converted
404 /// let call = client.get("/preferences")?
405 /// .with_cookie("theme", "dark") // String values
406 /// .with_cookie("user_id", 12345_u64) // Numeric values
407 /// .with_cookie("is_premium", true) // Boolean values
408 /// .with_cookie("selected_tags", vec!["rust", "web"]); // Array values
409 ///
410 /// // Cookies can be chained and overridden
411 /// let call = client.get("/profile")?
412 /// .with_cookie("session_id", "old-session")
413 /// .with_cookie("session_id", "new-session"); // Overrides previous value
414 /// # Ok(())
415 /// # }
416 /// ```
417 pub fn with_cookie<T: ParameterValue>(
418 self,
419 name: impl Into<String>,
420 value: impl Into<ParamValue<T>>,
421 ) -> Self {
422 let cookies = CallCookies::new().add_cookie(name, value);
423 self.with_cookies(cookies)
424 }
425
426 /// Overrides the authentication for this specific request.
427 ///
428 /// This method allows you to use different authentication for a specific request,
429 /// overriding the default authentication configured on the API client.
430 ///
431 /// # Examples
432 ///
433 /// ```rust
434 /// use clawspec_core::{ApiClient, Authentication};
435 ///
436 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
437 /// // Client with default authentication
438 /// let mut client = ApiClient::builder()
439 /// .with_authentication(Authentication::Bearer("default-token".into()))
440 /// .build()?;
441 ///
442 /// // Use different authentication for a specific request
443 /// let response = client
444 /// .get("/admin/users")?
445 /// .with_authentication(Authentication::Bearer("admin-token".into()))
446 /// .await?;
447 ///
448 /// // Remove authentication for a public endpoint
449 /// let response = client
450 /// .get("/public/health")?
451 /// .with_authentication_none()
452 /// .await?;
453 /// # Ok(())
454 /// # }
455 /// ```
456 pub fn with_authentication(mut self, authentication: crate::client::Authentication) -> Self {
457 self.authentication = Some(authentication);
458 self
459 }
460
461 /// Removes authentication for this specific request.
462 ///
463 /// This is useful when making requests to public endpoints that don't require
464 /// authentication, even when the client has default authentication configured.
465 ///
466 /// # Examples
467 ///
468 /// ```rust
469 /// use clawspec_core::{ApiClient, Authentication};
470 ///
471 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
472 /// // Client with default authentication
473 /// let mut client = ApiClient::builder()
474 /// .with_authentication(Authentication::Bearer("token".into()))
475 /// .build()?;
476 ///
477 /// // Remove authentication for public endpoint
478 /// let response = client
479 /// .get("/public/status")?
480 /// .with_authentication_none()
481 /// .await?;
482 /// # Ok(())
483 /// # }
484 /// ```
485 pub fn with_authentication_none(mut self) -> Self {
486 self.authentication = None;
487 self
488 }
489
490 // =============================================================================
491 // Status Code Validation Methods
492 // =============================================================================
493
494 /// Sets the expected status codes for this request using an inclusive range.
495 ///
496 /// By default, status codes 200..500 are considered successful.
497 /// Use this method to customize which status codes should be accepted.
498 ///
499 /// # Examples
500 ///
501 /// ## Basic Usage
502 /// ```rust
503 /// # use clawspec_core::ApiClient;
504 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
505 /// let mut client = ApiClient::builder().build()?;
506 ///
507 /// // Accept only 200 to 201 (inclusive)
508 /// let call = client.post("/users")?.with_status_range_inclusive(200..=201);
509 ///
510 /// // Accept any 2xx status code
511 /// let call = client.get("/users")?.with_status_range_inclusive(200..=299);
512 /// # Ok(())
513 /// # }
514 /// ```
515 ///
516 /// ## Edge Cases
517 /// ```rust
518 /// # use clawspec_core::ApiClient;
519 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
520 /// let mut client = ApiClient::builder().build()?;
521 ///
522 /// // Single status code range (equivalent to with_expected_status)
523 /// let call = client.get("/health")?.with_status_range_inclusive(200..=200);
524 ///
525 /// // Accept both success and client error ranges
526 /// let call = client.delete("/users/123")?
527 /// .with_status_range_inclusive(200..=299)
528 /// .add_expected_status_range_inclusive(400..=404);
529 ///
530 /// // Handle APIs that return 2xx or 3xx for different success states
531 /// let call = client.post("/async-operation")?.with_status_range_inclusive(200..=302);
532 /// # Ok(())
533 /// # }
534 /// ```
535 pub fn with_status_range_inclusive(mut self, range: RangeInclusive<u16>) -> Self {
536 self.expected_status_codes = ExpectedStatusCodes::from_inclusive_range(range);
537 self
538 }
539
540 /// Sets the expected status codes for this request using an exclusive range.
541 ///
542 /// # Examples
543 ///
544 /// ```rust
545 /// # use clawspec_core::ApiClient;
546 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
547 /// let mut client = ApiClient::builder().build()?;
548 ///
549 /// // Accept 200 to 299 (200 included, 300 excluded)
550 /// let call = client.get("/users")?.with_status_range(200..300);
551 /// # Ok(())
552 /// # }
553 /// ```
554 pub fn with_status_range(mut self, range: Range<u16>) -> Self {
555 self.expected_status_codes = ExpectedStatusCodes::from_exclusive_range(range);
556 self
557 }
558
559 /// Sets a single expected status code for this request.
560 ///
561 /// # Examples
562 ///
563 /// ```rust
564 /// # use clawspec_core::ApiClient;
565 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
566 /// let mut client = ApiClient::builder().build()?;
567 ///
568 /// // Accept only 204 for DELETE operations
569 /// let call = client.delete("/users/123")?.with_expected_status(204);
570 /// # Ok(())
571 /// # }
572 /// ```
573 pub fn with_expected_status(mut self, status: u16) -> Self {
574 self.expected_status_codes = ExpectedStatusCodes::from_single(status);
575 self
576 }
577
578 /// Adds an additional expected status code to the existing set.
579 ///
580 /// # Examples
581 ///
582 /// ```rust
583 /// # use clawspec_core::ApiClient;
584 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
585 /// let mut client = ApiClient::builder().build()?;
586 ///
587 /// // Accept 200..299 and also 404
588 /// let call = client.get("/users")?.with_status_range_inclusive(200..=299).add_expected_status(404);
589 /// # Ok(())
590 /// # }
591 /// ```
592 pub fn add_expected_status(mut self, status: u16) -> Self {
593 self.expected_status_codes = self.expected_status_codes.add_expected_status(status);
594 self
595 }
596
597 /// Adds an additional expected status range (inclusive) to the existing set.
598 ///
599 /// # Examples
600 ///
601 /// ```rust
602 /// # use clawspec_core::ApiClient;
603 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
604 /// let mut client = ApiClient::builder().build()?;
605 ///
606 /// // Accept 200..=204 and also 400..=402
607 /// let call = client.post("/users")?.with_status_range_inclusive(200..=204).add_expected_status_range_inclusive(400..=402);
608 /// # Ok(())
609 /// # }
610 /// ```
611 pub fn add_expected_status_range_inclusive(mut self, range: RangeInclusive<u16>) -> Self {
612 self.expected_status_codes = self.expected_status_codes.add_expected_range(range);
613 self
614 }
615
616 /// Adds an additional expected status range (exclusive) to the existing set.
617 ///
618 /// # Examples
619 ///
620 /// ```rust
621 /// # use clawspec_core::ApiClient;
622 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
623 /// let mut client = ApiClient::builder().build()?;
624 ///
625 /// // Accept 200..=204 and also 400..403
626 /// let call = client.post("/users")?.with_status_range_inclusive(200..=204).add_expected_status_range(400..403);
627 /// # Ok(())
628 /// # }
629 /// ```
630 pub fn add_expected_status_range(mut self, range: Range<u16>) -> Self {
631 self.expected_status_codes = self.expected_status_codes.add_exclusive_range(range);
632 self
633 }
634
635 /// Convenience method to accept only 2xx status codes (200..300).
636 ///
637 /// # Examples
638 ///
639 /// ```rust
640 /// # use clawspec_core::ApiClient;
641 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
642 /// let mut client = ApiClient::builder().build()?;
643 /// let call = client.get("/users")?.with_success_only();
644 /// # Ok(())
645 /// # }
646 /// ```
647 pub fn with_success_only(self) -> Self {
648 self.with_status_range(200..300)
649 }
650
651 /// Convenience method to accept 2xx and 4xx status codes (200..500, excluding 3xx).
652 ///
653 /// # Examples
654 ///
655 /// ```rust
656 /// # use clawspec_core::ApiClient;
657 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
658 /// let mut client = ApiClient::builder().build()?;
659 /// let call = client.post("/users")?.with_client_errors();
660 /// # Ok(())
661 /// # }
662 /// ```
663 pub fn with_client_errors(self) -> Self {
664 self.with_status_range_inclusive(200..=299)
665 .add_expected_status_range_inclusive(400..=499)
666 }
667
668 /// Sets the expected status codes using an `ExpectedStatusCodes` instance.
669 ///
670 /// This method allows you to pass pre-configured `ExpectedStatusCodes` instances,
671 /// which is particularly useful with the `expected_status_codes!` macro.
672 ///
673 /// # Examples
674 ///
675 /// ```rust
676 /// use clawspec_core::{ApiClient, expected_status_codes};
677 ///
678 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
679 /// let mut client = ApiClient::builder().build()?;
680 ///
681 /// // Using the macro with with_expected_status_codes
682 /// let call = client.get("/users")?
683 /// .with_expected_status_codes(expected_status_codes!(200-299));
684 ///
685 /// // Using manually created ExpectedStatusCodes
686 /// let codes = clawspec_core::ExpectedStatusCodes::from_inclusive_range(200..=204)
687 /// .add_expected_status(404);
688 /// let call = client.get("/items")?.with_expected_status_codes(codes);
689 /// # Ok(())
690 /// # }
691 /// ```
692 pub fn with_expected_status_codes(mut self, codes: ExpectedStatusCodes) -> Self {
693 self.expected_status_codes = codes;
694 self
695 }
696
697 /// Sets expected status codes from a single `http::StatusCode`.
698 ///
699 /// This method provides **compile-time validation** of status codes through the type system.
700 /// Unlike the `u16` variants, this method does not perform runtime validation since
701 /// `http::StatusCode` guarantees valid HTTP status codes at compile time.
702 ///
703 /// # Example
704 ///
705 /// ```rust
706 /// use clawspec_core::ApiClient;
707 /// use http::StatusCode;
708 ///
709 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
710 /// let mut client = ApiClient::builder().build()?;
711 ///
712 /// let call = client.get("/users")?
713 /// .with_expected_status_code(StatusCode::OK);
714 /// # Ok(())
715 /// # }
716 /// ```
717 pub fn with_expected_status_code(self, status: http::StatusCode) -> Self {
718 self.with_expected_status_codes(ExpectedStatusCodes::from_status_code(status))
719 }
720
721 /// Sets expected status codes from a range of `http::StatusCode`.
722 ///
723 /// This method provides **compile-time validation** of status codes through the type system.
724 /// Unlike the `u16` variants, this method does not perform runtime validation since
725 /// `http::StatusCode` guarantees valid HTTP status codes at compile time.
726 ///
727 /// # Example
728 ///
729 /// ```rust
730 /// use clawspec_core::ApiClient;
731 /// use http::StatusCode;
732 ///
733 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
734 /// let mut client = ApiClient::builder().build()?;
735 ///
736 /// let call = client.get("/users")?
737 /// .with_expected_status_code_range(StatusCode::OK..=StatusCode::NO_CONTENT);
738 /// # Ok(())
739 /// # }
740 /// ```
741 pub fn with_expected_status_code_range(self, range: RangeInclusive<http::StatusCode>) -> Self {
742 self.with_expected_status_codes(ExpectedStatusCodes::from_status_code_range_inclusive(
743 range,
744 ))
745 }
746
747 // =============================================================================
748 // Request Body Methods
749 // =============================================================================
750
751 /// Sets the request body to JSON.
752 ///
753 /// This method serializes the provided data as JSON and sets the
754 /// Content-Type header to `application/json`.
755 ///
756 /// # Examples
757 ///
758 /// ```rust
759 /// # use clawspec_core::ApiClient;
760 /// # use serde::Serialize;
761 /// # use utoipa::ToSchema;
762 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
763 /// #[derive(Serialize, ToSchema)]
764 /// struct CreateUser {
765 /// name: String,
766 /// email: String,
767 /// }
768 ///
769 /// let mut client = ApiClient::builder().build()?;
770 /// let user_data = CreateUser {
771 /// name: "John Doe".to_string(),
772 /// email: "john@example.com".to_string(),
773 /// };
774 ///
775 /// let call = client.post("/users")?.json(&user_data)?;
776 /// # Ok(())
777 /// # }
778 /// ```
779 pub fn json<T>(mut self, t: &T) -> Result<Self, ApiClientError>
780 where
781 T: Serialize + ToSchema + 'static,
782 {
783 let body = CallBody::json(t)?;
784 self.body = Some(body);
785 Ok(self)
786 }
787
788 /// Sets the request body to JSON with redaction support for OpenAPI examples.
789 ///
790 /// This method returns a [`RequestBodyRedactionBuilder`] that allows you to
791 /// redact sensitive values (like passwords, API keys, tokens) in the OpenAPI
792 /// documentation while sending the original values in the HTTP request.
793 ///
794 /// **Key principle:**
795 /// - **HTTP Request**: Uses the original value with real data for testing
796 /// - **OpenAPI Example**: Uses the redacted value with stable placeholders
797 ///
798 /// This is useful when you want to:
799 /// - Hide sensitive credentials in documentation
800 /// - Create stable, deterministic OpenAPI examples
801 /// - Test with real data while documenting with sanitized examples
802 ///
803 /// # Type Parameters
804 ///
805 /// * `T` - The type to serialize. Must implement `Serialize`, `ToSchema`, and `Clone`.
806 ///
807 /// # Examples
808 ///
809 /// ## Basic Usage
810 ///
811 /// ```rust
812 /// # use clawspec_core::ApiClient;
813 /// # use serde::Serialize;
814 /// # use utoipa::ToSchema;
815 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
816 /// #[derive(Clone, Serialize, ToSchema)]
817 /// struct LoginRequest {
818 /// username: String,
819 /// password: String,
820 /// }
821 ///
822 /// let mut client = ApiClient::builder().build()?;
823 /// let request = LoginRequest {
824 /// username: "alice".to_string(),
825 /// password: "secret123".to_string(),
826 /// };
827 ///
828 /// // The HTTP request will contain the real password,
829 /// // but the OpenAPI example will show "[REDACTED]"
830 /// client
831 /// .post("/auth/login")?
832 /// .json_redacted(&request)?
833 /// .redact("/password", "[REDACTED]")?
834 /// .await?; // IntoFuture - no .finish() needed
835 /// # Ok(())
836 /// # }
837 /// ```
838 ///
839 /// ## Multiple Redactions
840 ///
841 /// ```rust
842 /// # use clawspec_core::ApiClient;
843 /// # use serde::Serialize;
844 /// # use utoipa::ToSchema;
845 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
846 /// #[derive(Clone, Serialize, ToSchema)]
847 /// struct CreateApiKey {
848 /// name: String,
849 /// secret: String,
850 /// internal_id: String,
851 /// }
852 ///
853 /// let mut client = ApiClient::builder().build()?;
854 /// let request = CreateApiKey {
855 /// name: "my-key".to_string(),
856 /// secret: "sk-live-abc123def456".to_string(),
857 /// internal_id: "internal-ref-789".to_string(),
858 /// };
859 ///
860 /// client
861 /// .post("/api-keys")?
862 /// .json_redacted(&request)?
863 /// .redact("/secret", "[REDACTED_SECRET]")?
864 /// .redact_remove("/internal_id")? // Remove entirely from docs
865 /// .await?; // IntoFuture - no .finish() needed
866 /// # Ok(())
867 /// # }
868 /// ```
869 ///
870 /// ## JSONPath Wildcards
871 ///
872 /// ```rust
873 /// # use clawspec_core::ApiClient;
874 /// # use serde::Serialize;
875 /// # use utoipa::ToSchema;
876 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
877 /// #[derive(Clone, Serialize, ToSchema)]
878 /// struct BulkCreateUsers {
879 /// users: Vec<UserData>,
880 /// }
881 ///
882 /// #[derive(Clone, Serialize, ToSchema)]
883 /// struct UserData {
884 /// name: String,
885 /// password: String,
886 /// }
887 ///
888 /// let mut client = ApiClient::builder().build()?;
889 /// let request = BulkCreateUsers {
890 /// users: vec![
891 /// UserData { name: "alice".to_string(), password: "secret1".to_string() },
892 /// UserData { name: "bob".to_string(), password: "secret2".to_string() },
893 /// ],
894 /// };
895 ///
896 /// // Redact ALL passwords in the array
897 /// client
898 /// .post("/users/bulk")?
899 /// .json_redacted(&request)?
900 /// .redact("$.users[*].password", "[REDACTED]")?
901 /// .await?; // IntoFuture - no .finish() needed
902 /// # Ok(())
903 /// # }
904 /// ```
905 ///
906 /// # Errors
907 ///
908 /// Returns an error if JSON serialization fails.
909 ///
910 /// # Feature Flag
911 ///
912 /// This method requires the `redaction` feature to be enabled:
913 ///
914 /// ```toml
915 /// [dependencies]
916 /// clawspec-core = { version = "...", features = ["redaction"] }
917 /// ```
918 #[cfg(feature = "redaction")]
919 #[cfg_attr(docsrs, doc(cfg(feature = "redaction")))]
920 pub fn json_redacted<T>(self, t: &T) -> Result<RequestBodyRedactionBuilder<T>, ApiClientError>
921 where
922 T: Serialize + ToSchema + Clone + 'static,
923 {
924 let body = CallBody::json_without_example(t)?;
925 let json_value = serde_json::to_value(t)?;
926 Ok(RequestBodyRedactionBuilder::new(
927 t.clone(),
928 json_value,
929 body,
930 self,
931 ))
932 }
933
934 /// Sets the request body to form-encoded data.
935 ///
936 /// This method serializes the provided data as `application/x-www-form-urlencoded`
937 /// and sets the appropriate Content-Type header.
938 ///
939 /// # Examples
940 ///
941 /// ```rust
942 /// # use clawspec_core::ApiClient;
943 /// # use serde::Serialize;
944 /// # use utoipa::ToSchema;
945 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
946 /// #[derive(Serialize, ToSchema)]
947 /// struct LoginForm {
948 /// username: String,
949 /// password: String,
950 /// }
951 ///
952 /// let mut client = ApiClient::builder().build()?;
953 /// let form_data = LoginForm {
954 /// username: "user@example.com".to_string(),
955 /// password: "secret".to_string(),
956 /// };
957 ///
958 /// let call = client.post("/login")?.form(&form_data)?;
959 /// # Ok(())
960 /// # }
961 /// ```
962 pub fn form<T>(mut self, t: &T) -> Result<Self, ApiClientError>
963 where
964 T: Serialize + ToSchema + 'static,
965 {
966 let body = CallBody::form(t)?;
967 self.body = Some(body);
968 Ok(self)
969 }
970
971 /// Sets the request body to raw binary data with a custom content type.
972 ///
973 /// This method allows you to send arbitrary binary data with a specified
974 /// content type. This is useful for sending data that doesn't fit into
975 /// the standard JSON or form categories.
976 ///
977 /// # Examples
978 ///
979 /// ```rust
980 /// # use clawspec_core::ApiClient;
981 /// # use headers::ContentType;
982 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
983 /// let mut client = ApiClient::builder().build()?;
984 /// // Send XML data
985 /// let xml_data = r#"<?xml version="1.0"?><user><name>John</name></user>"#;
986 /// let call = client.post("/import")?
987 /// .raw(xml_data.as_bytes().to_vec(), ContentType::xml());
988 ///
989 /// // Send binary file
990 /// let binary_data = vec![0xFF, 0xFE, 0xFD];
991 /// let call = client.post("/upload")?
992 /// .raw(binary_data, ContentType::octet_stream());
993 /// # Ok(())
994 /// # }
995 /// ```
996 pub fn raw(mut self, data: Vec<u8>, content_type: headers::ContentType) -> Self {
997 let body = CallBody::raw(data, content_type);
998 self.body = Some(body);
999 self
1000 }
1001
1002 /// Sets the request body to plain text.
1003 ///
1004 /// This is a convenience method for sending plain text data with
1005 /// `text/plain` content type.
1006 ///
1007 /// # Examples
1008 ///
1009 /// ```rust
1010 /// # use clawspec_core::ApiClient;
1011 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1012 /// let mut client = ApiClient::builder().build()?;
1013 /// let call = client.post("/notes")?.text("This is a plain text note");
1014 /// # Ok(())
1015 /// # }
1016 /// ```
1017 pub fn text(mut self, text: &str) -> Self {
1018 let body = CallBody::text(text);
1019 self.body = Some(body);
1020 self
1021 }
1022
1023 /// Sets the request body to multipart/form-data.
1024 ///
1025 /// This method creates a multipart body with a generated boundary and supports
1026 /// both text fields and file uploads. This is commonly used for file uploads
1027 /// or when combining different types of data in a single request.
1028 ///
1029 /// # Examples
1030 ///
1031 /// ```rust
1032 /// # use clawspec_core::ApiClient;
1033 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
1034 /// let mut client = ApiClient::builder().build()?;
1035 /// let parts = vec![
1036 /// ("title", "My Document"),
1037 /// ("file", "file content here"),
1038 /// ];
1039 /// let call = client.post("/upload")?.multipart(parts);
1040 /// # Ok(())
1041 /// # }
1042 /// ```
1043 pub fn multipart(mut self, parts: Vec<(&str, &str)>) -> Self {
1044 let body = CallBody::multipart(parts);
1045 self.body = Some(body);
1046 self
1047 }
1048}
1049
1050// Call