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