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