Skip to main content

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