clawspec_core/client/openapi/result.rs
1use std::any::type_name;
2use std::mem;
3use std::sync::Arc;
4
5use headers::{ContentType, Header};
6use http::StatusCode;
7use http::header::CONTENT_TYPE;
8use reqwest::Response;
9use serde::de::DeserializeOwned;
10use tokio::sync::RwLock;
11use utoipa::ToSchema;
12use utoipa::openapi::{Content, RefOr, ResponseBuilder, Schema};
13
14use super::Collectors;
15use crate::client::ApiClientError;
16use crate::client::response::output::Output;
17
18/// Represents the result of an API call with response processing capabilities.
19///
20/// This struct contains the response from an HTTP request along with methods to
21/// process the response in various formats (JSON, text, bytes, etc.) while
22/// automatically collecting OpenAPI schema information.
23///
24/// # ⚠️ Important: Response Consumption Required
25///
26/// **You must consume this `CallResult` by calling one of the response processing methods**
27/// to ensure proper OpenAPI documentation generation. Simply calling `exchange()` and not
28/// processing the result will result in incomplete OpenAPI specifications.
29///
30/// ## Required Response Processing
31///
32/// Choose the appropriate method based on your expected response:
33///
34/// - **Empty responses** (204 No Content, etc.): [`as_empty()`](Self::as_empty)
35/// - **JSON responses**: [`as_json::<T>()`](Self::as_json)
36/// - **Optional JSON responses** (204/404 → None): [`as_optional_json::<T>()`](Self::as_optional_json)
37/// - **Type-safe error handling**: [`as_result_json::<T, E>()`](Self::as_result_json) (2xx → Ok(T), 4xx/5xx → Err(E))
38/// - **Optional with errors**: [`as_result_option_json::<T, E>()`](Self::as_result_option_json) (combines optional and error handling)
39/// - **Text responses**: [`as_text()`](Self::as_text)
40/// - **Binary responses**: [`as_bytes()`](Self::as_bytes)
41/// - **Raw response access**: [`as_raw()`](Self::as_raw) (includes status code, content-type, and body)
42///
43/// ## Example: Correct Usage
44///
45/// ```rust
46/// use clawspec_core::ApiClient;
47/// # use serde::Deserialize;
48/// # use utoipa::ToSchema;
49/// # #[derive(Deserialize, ToSchema)]
50/// # struct User { id: u32, name: String }
51///
52/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
53/// let mut client = ApiClient::builder().build()?;
54///
55/// // ✅ CORRECT: Always consume the CallResult
56/// let user: User = client
57/// .get("/users/123")?
58///
59/// .await?
60/// .as_json() // ← This is required!
61/// .await?;
62///
63/// // ✅ CORRECT: For empty responses (like DELETE)
64/// client
65/// .delete("/users/123")?
66///
67/// .await?
68/// .as_empty() // ← This is required!
69/// .await?;
70///
71/// // ❌ INCORRECT: This will not generate proper OpenAPI documentation
72/// // let _result = client.get("/users/123")?.await?;
73/// // // Missing .as_json() or other consumption method! This will not generate proper OpenAPI documentation
74/// # Ok(())
75/// # }
76/// ```
77///
78/// ## Why This Matters
79///
80/// The OpenAPI schema generation relies on observing how responses are processed.
81/// Without calling a consumption method:
82/// - Response schemas won't be captured
83/// - Content-Type information may be incomplete
84/// - Operation examples won't be generated
85/// - The resulting OpenAPI spec will be missing crucial response documentation
86#[derive(Debug, Clone)]
87pub struct CallResult {
88 operation_id: String,
89 status: StatusCode,
90 content_type: Option<ContentType>,
91 output: Output,
92 pub(in crate::client) collectors: Arc<RwLock<Collectors>>,
93}
94
95/// Represents the raw response data from an HTTP request.
96///
97/// This struct provides complete access to the HTTP response including status code,
98/// content type, and body data. It supports both text and binary response bodies.
99///
100/// # Example
101///
102/// ```rust
103/// use clawspec_core::{ApiClient, RawBody};
104/// use http::StatusCode;
105///
106/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
107/// let mut client = ApiClient::builder().build()?;
108/// let raw_result = client
109/// .get("/api/data")?
110///
111/// .await?
112/// .as_raw()
113/// .await?;
114///
115/// println!("Status: {}", raw_result.status_code());
116/// if let Some(content_type) = raw_result.content_type() {
117/// println!("Content-Type: {}", content_type);
118/// }
119/// match raw_result.body() {
120/// RawBody::Text(text) => println!("Text body: {}", text),
121/// RawBody::Binary(bytes) => println!("Binary body: {} bytes", bytes.len()),
122/// RawBody::Empty => println!("Empty body"),
123/// }
124/// # Ok(())
125/// # }
126/// ```
127#[derive(Debug, Clone)]
128pub struct RawResult {
129 status: StatusCode,
130 content_type: Option<ContentType>,
131 body: RawBody,
132}
133
134/// Represents the body content of a raw HTTP response.
135///
136/// This enum handles different types of response bodies:
137/// - Text content (including JSON, HTML, XML, etc.)
138/// - Binary content (images, files, etc.)
139/// - Empty responses
140#[derive(Debug, Clone)]
141pub enum RawBody {
142 /// Text-based content (UTF-8 encoded)
143 Text(String),
144 /// Binary content
145 Binary(Vec<u8>),
146 /// Empty response body
147 Empty,
148}
149
150impl RawResult {
151 /// Returns the HTTP status code of the response.
152 pub fn status_code(&self) -> StatusCode {
153 self.status
154 }
155
156 /// Returns the content type of the response, if present.
157 pub fn content_type(&self) -> Option<&ContentType> {
158 self.content_type.as_ref()
159 }
160
161 /// Returns the response body.
162 pub fn body(&self) -> &RawBody {
163 &self.body
164 }
165
166 /// Returns the response body as text if it's text content.
167 ///
168 /// # Returns
169 /// - `Some(&str)` if the body contains text
170 /// - `None` if the body is binary or empty
171 pub fn text(&self) -> Option<&str> {
172 match &self.body {
173 RawBody::Text(text) => Some(text),
174 _ => None,
175 }
176 }
177
178 /// Returns the response body as binary data if it's binary content.
179 ///
180 /// # Returns
181 /// - `Some(&[u8])` if the body contains binary data
182 /// - `None` if the body is text or empty
183 pub fn bytes(&self) -> Option<&[u8]> {
184 match &self.body {
185 RawBody::Binary(bytes) => Some(bytes),
186 _ => None,
187 }
188 }
189
190 /// Returns true if the response body is empty.
191 pub fn is_empty(&self) -> bool {
192 matches!(self.body, RawBody::Empty)
193 }
194}
195
196impl CallResult {
197 /// Extracts and parses the Content-Type header from the HTTP response.
198 fn extract_content_type(response: &Response) -> Result<Option<ContentType>, ApiClientError> {
199 let content_type = response
200 .headers()
201 .get_all(CONTENT_TYPE)
202 .iter()
203 .collect::<Vec<_>>();
204
205 if content_type.is_empty() {
206 Ok(None)
207 } else {
208 let ct = ContentType::decode(&mut content_type.into_iter())?;
209 Ok(Some(ct))
210 }
211 }
212
213 /// Processes the response body based on content type and status code.
214 async fn process_response_body(
215 response: Response,
216 content_type: &Option<ContentType>,
217 status: StatusCode,
218 ) -> Result<Output, ApiClientError> {
219 if let Some(content_type) = content_type
220 && status != StatusCode::NO_CONTENT
221 {
222 if *content_type == ContentType::json() {
223 let json = response.text().await?;
224 Ok(Output::Json(json))
225 } else if *content_type == ContentType::octet_stream() {
226 let bytes = response.bytes().await?;
227 Ok(Output::Bytes(bytes.to_vec()))
228 } else if content_type.to_string().starts_with("text/") {
229 let text = response.text().await?;
230 Ok(Output::Text(text))
231 } else {
232 let body = response.text().await?;
233 Ok(Output::Other { body })
234 }
235 } else {
236 Ok(Output::Empty)
237 }
238 }
239
240 pub(in crate::client) async fn new(
241 operation_id: String,
242 collectors: Arc<RwLock<Collectors>>,
243 response: Response,
244 ) -> Result<Self, ApiClientError> {
245 let status = response.status();
246 let content_type = Self::extract_content_type(&response)?;
247 let output = Self::process_response_body(response, &content_type, status).await?;
248
249 Ok(Self {
250 operation_id,
251 status,
252 content_type,
253 output,
254 collectors,
255 })
256 }
257
258 pub(in crate::client) async fn new_without_collection(
259 response: Response,
260 ) -> Result<Self, ApiClientError> {
261 let status = response.status();
262 let content_type = Self::extract_content_type(&response)?;
263 let output = Self::process_response_body(response, &content_type, status).await?;
264
265 // Create a dummy collectors instance that won't be used
266 let collectors = Arc::new(RwLock::new(Collectors::default()));
267
268 Ok(Self {
269 operation_id: String::new(), // Empty operation_id since it won't be used
270 status,
271 content_type,
272 output,
273 collectors,
274 })
275 }
276
277 pub(in crate::client) async fn get_output(
278 &self,
279 schema: Option<RefOr<Schema>>,
280 ) -> Result<&Output, ApiClientError> {
281 // add operation response desc
282 let mut cs = self.collectors.write().await;
283 let Some(operation) = cs.operations.get_mut(&self.operation_id) else {
284 return Err(ApiClientError::MissingOperation {
285 id: self.operation_id.clone(),
286 });
287 };
288
289 let Some(operation) = operation.last_mut() else {
290 return Err(ApiClientError::MissingOperation {
291 id: self.operation_id.clone(),
292 });
293 };
294
295 // Get response description from the operation, if available
296 let status_code = self.status.as_u16();
297 let description = operation
298 .response_description
299 .clone()
300 .unwrap_or_else(|| format!("Status code {status_code}"));
301
302 let response = if let Some(content_type) = &self.content_type {
303 // Create content
304 let content = Content::builder().schema(schema).build();
305 ResponseBuilder::new()
306 .description(description)
307 .content(content_type.to_string(), content)
308 .build()
309 } else {
310 // Empty response
311 ResponseBuilder::new().description(description).build()
312 };
313
314 operation
315 .operation
316 .responses
317 .responses
318 .insert(self.status.as_u16().to_string(), RefOr::T(response));
319
320 Ok(&self.output)
321 }
322
323 /// Processes the response as JSON and deserializes it to the specified type.
324 ///
325 /// This method automatically records the response schema in the OpenAPI specification
326 /// and processes the response body as JSON. The type parameter must implement
327 /// `DeserializeOwned` and `ToSchema` for proper JSON parsing and schema generation.
328 ///
329 /// # Type Parameters
330 ///
331 /// - `T`: The target type for deserialization, must implement `DeserializeOwned`, `ToSchema`, and `'static`
332 ///
333 /// # Returns
334 ///
335 /// - `Ok(T)`: The deserialized response object
336 /// - `Err(ApiClientError)`: If the response is not JSON or deserialization fails
337 ///
338 /// # Example
339 ///
340 /// ```rust
341 /// # use clawspec_core::ApiClient;
342 /// # use serde::{Deserialize, Serialize};
343 /// # use utoipa::ToSchema;
344 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
345 /// #[derive(Deserialize, ToSchema)]
346 /// struct User {
347 /// id: u32,
348 /// name: String,
349 /// }
350 ///
351 /// let mut client = ApiClient::builder().build()?;
352 /// let user: User = client
353 /// .get("/users/123")?
354 ///
355 /// .await?
356 /// .as_json()
357 /// .await?;
358 /// # Ok(())
359 /// # }
360 /// ```
361 pub async fn as_json<T>(&mut self) -> Result<T, ApiClientError>
362 where
363 T: DeserializeOwned + ToSchema + 'static,
364 {
365 let mut cs = self.collectors.write().await;
366 let schema = cs.schemas.add::<T>();
367 mem::drop(cs);
368 let output = self.get_output(Some(schema)).await?;
369
370 let Output::Json(json) = output else {
371 return Err(ApiClientError::UnsupportedJsonOutput {
372 output: output.clone(),
373 name: type_name::<T>(),
374 });
375 };
376
377 self.deserialize_and_record::<T>(json).await
378 }
379
380 /// Processes the response as optional JSON, treating 204 and 404 status codes as `None`.
381 ///
382 /// This method provides ergonomic handling of optional REST API responses by automatically
383 /// treating 204 (No Content) and 404 (Not Found) status codes as `None`, while deserializing
384 /// other successful responses as `Some(T)`. This is particularly useful for APIs that use
385 /// HTTP status codes to indicate the absence of data rather than errors.
386 ///
387 /// The method automatically records the response schema in the OpenAPI specification,
388 /// maintaining proper documentation generation.
389 ///
390 /// # Type Parameters
391 ///
392 /// - `T`: The target type for deserialization, must implement `DeserializeOwned`, `ToSchema`, and `'static`
393 ///
394 /// # Returns
395 ///
396 /// - `Ok(None)`: If the status code is 204 or 404
397 /// - `Ok(Some(T))`: The deserialized response object for other successful responses
398 /// - `Err(ApiClientError)`: If the response is not JSON or deserialization fails
399 ///
400 /// # Example
401 ///
402 /// ```rust
403 /// # use clawspec_core::ApiClient;
404 /// # use serde::{Deserialize, Serialize};
405 /// # use utoipa::ToSchema;
406 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
407 /// #[derive(Deserialize, ToSchema)]
408 /// struct User {
409 /// id: u32,
410 /// name: String,
411 /// }
412 ///
413 /// let mut client = ApiClient::builder().build()?;
414 ///
415 /// // Returns None for 404
416 /// let user: Option<User> = client
417 /// .get("/users/nonexistent")?
418 ///
419 /// .await?
420 /// .as_optional_json()
421 /// .await?;
422 /// assert!(user.is_none());
423 ///
424 /// // Returns Some(User) for successful response
425 /// let user: Option<User> = client
426 /// .get("/users/123")?
427 ///
428 /// .await?
429 /// .as_optional_json()
430 /// .await?;
431 /// assert!(user.is_some());
432 /// # Ok(())
433 /// # }
434 /// ```
435 pub async fn as_optional_json<T>(&mut self) -> Result<Option<T>, ApiClientError>
436 where
437 T: DeserializeOwned + ToSchema + 'static,
438 {
439 // Check if status code indicates absence of data
440 if self.status == StatusCode::NO_CONTENT || self.status == StatusCode::NOT_FOUND {
441 // Record the response without a schema
442 self.get_output(None).await?;
443 return Ok(None);
444 }
445
446 // For other status codes, deserialize as JSON
447 let mut cs = self.collectors.write().await;
448 let schema = cs.schemas.add::<T>();
449 mem::drop(cs);
450 let output = self.get_output(Some(schema)).await?;
451
452 let Output::Json(json) = output else {
453 return Err(ApiClientError::UnsupportedJsonOutput {
454 output: output.clone(),
455 name: type_name::<T>(),
456 });
457 };
458
459 let result = self.deserialize_and_record::<T>(json).await?;
460 Ok(Some(result))
461 }
462
463 /// Processes the response as a `Result<T, E>` based on HTTP status code.
464 ///
465 /// This method provides type-safe error handling for REST APIs that return structured
466 /// error responses. It automatically deserializes the response body to either the
467 /// success type `T` (for 2xx status codes) or the error type `E` (for 4xx/5xx status codes).
468 ///
469 /// Both success and error schemas are automatically recorded in the OpenAPI specification,
470 /// providing complete documentation of your API's response patterns.
471 ///
472 /// # Type Parameters
473 ///
474 /// - `T`: The success response type, must implement `DeserializeOwned`, `ToSchema`, and `'static`
475 /// - `E`: The error response type, must implement `DeserializeOwned`, `ToSchema`, and `'static`
476 ///
477 /// # Returns
478 ///
479 /// - `Ok(T)`: The deserialized success response for 2xx status codes
480 /// - `Err(E)`: The deserialized error response for 4xx/5xx status codes
481 ///
482 /// # Errors
483 ///
484 /// Returns `ApiClientError` if:
485 /// - The response is not JSON
486 /// - JSON deserialization fails for either type
487 /// - The response body is empty when content is expected
488 ///
489 /// # Example
490 ///
491 /// ```rust
492 /// # use clawspec_core::ApiClient;
493 /// # use serde::{Deserialize, Serialize};
494 /// # use utoipa::ToSchema;
495 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
496 /// #[derive(Deserialize, ToSchema)]
497 /// struct User {
498 /// id: u32,
499 /// name: String,
500 /// }
501 ///
502 /// #[derive(Deserialize, ToSchema)]
503 /// struct ApiError {
504 /// code: String,
505 /// message: String,
506 /// }
507 ///
508 /// let mut client = ApiClient::builder().build()?;
509 ///
510 /// // Returns Ok(User) for 2xx responses
511 /// let result: Result<User, ApiError> = client
512 /// .get("/users/123")?
513 ///
514 /// .await?
515 /// .as_result_json()
516 /// .await?;
517 ///
518 /// match result {
519 /// Ok(user) => println!("User: {}", user.name),
520 /// Err(err) => println!("Error: {} - {}", err.code, err.message),
521 /// }
522 /// # Ok(())
523 /// # }
524 /// ```
525 pub async fn as_result_json<T, E>(&mut self) -> Result<Result<T, E>, ApiClientError>
526 where
527 T: DeserializeOwned + ToSchema + 'static,
528 E: DeserializeOwned + ToSchema + 'static,
529 {
530 Ok(self
531 .process_result_json_internal::<T, E>(false)
532 .await?
533 .map(|opt| opt.expect("BUG: 404 handling disabled but got None")))
534 }
535
536 /// Processes the response as a `Result<Option<T>, E>` based on HTTP status code.
537 ///
538 /// This method combines optional response handling with type-safe error handling,
539 /// providing comprehensive support for REST APIs that:
540 /// - Return structured error responses for failures (4xx/5xx)
541 /// - Use 204 (No Content) or 404 (Not Found) to indicate absence of data
542 /// - Return data for other successful responses (2xx)
543 ///
544 /// Both success and error schemas are automatically recorded in the OpenAPI specification.
545 ///
546 /// # Type Parameters
547 ///
548 /// - `T`: The success response type, must implement `DeserializeOwned`, `ToSchema`, and `'static`
549 /// - `E`: The error response type, must implement `DeserializeOwned`, `ToSchema`, and `'static`
550 ///
551 /// # Returns
552 ///
553 /// - `Ok(None)`: For 204 (No Content) or 404 (Not Found) status codes
554 /// - `Ok(Some(T))`: The deserialized success response for other 2xx status codes
555 /// - `Err(E)`: The deserialized error response for 4xx/5xx status codes
556 ///
557 /// # Errors
558 ///
559 /// Returns `ApiClientError` if:
560 /// - The response is not JSON (when content is expected)
561 /// - JSON deserialization fails for either type
562 ///
563 /// # Example
564 ///
565 /// ```rust
566 /// # use clawspec_core::ApiClient;
567 /// # use serde::{Deserialize, Serialize};
568 /// # use utoipa::ToSchema;
569 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
570 /// #[derive(Deserialize, ToSchema)]
571 /// struct User {
572 /// id: u32,
573 /// name: String,
574 /// }
575 ///
576 /// #[derive(Deserialize, ToSchema)]
577 /// struct ApiError {
578 /// code: String,
579 /// message: String,
580 /// }
581 ///
582 /// let mut client = ApiClient::builder().build()?;
583 ///
584 /// // Returns Ok(None) for 404
585 /// let result: Result<Option<User>, ApiError> = client
586 /// .get("/users/nonexistent")?
587 ///
588 /// .await?
589 /// .as_result_option_json()
590 /// .await?;
591 ///
592 /// match result {
593 /// Ok(Some(user)) => println!("User: {}", user.name),
594 /// Ok(None) => println!("User not found"),
595 /// Err(err) => println!("Error: {} - {}", err.code, err.message),
596 /// }
597 /// # Ok(())
598 /// # }
599 /// ```
600 pub async fn as_result_option_json<T, E>(
601 &mut self,
602 ) -> Result<Result<Option<T>, E>, ApiClientError>
603 where
604 T: DeserializeOwned + ToSchema + 'static,
605 E: DeserializeOwned + ToSchema + 'static,
606 {
607 self.process_result_json_internal::<T, E>(true).await
608 }
609
610 /// Internal helper for processing Result<Option<T>, E> responses.
611 ///
612 /// Handles the common logic for both `as_result_json` and `as_result_option_json`.
613 async fn process_result_json_internal<T, E>(
614 &mut self,
615 treat_404_as_none: bool,
616 ) -> Result<Result<Option<T>, E>, ApiClientError>
617 where
618 T: DeserializeOwned + ToSchema + 'static,
619 E: DeserializeOwned + ToSchema + 'static,
620 {
621 // Check for 204/404 which indicate absence of data (when enabled)
622 if treat_404_as_none
623 && (self.status == StatusCode::NO_CONTENT || self.status == StatusCode::NOT_FOUND)
624 {
625 let mut cs = self.collectors.write().await;
626 cs.schemas.add::<T>();
627 cs.schemas.add::<E>();
628 mem::drop(cs);
629
630 self.get_output(None).await?;
631 return Ok(Ok(None));
632 }
633
634 let is_success = self.status.is_success();
635
636 // Register both schemas in the OpenAPI spec
637 let mut cs = self.collectors.write().await;
638 let success_schema = cs.schemas.add::<T>();
639 let error_schema = cs.schemas.add::<E>();
640 mem::drop(cs);
641
642 // Get the appropriate schema based on status code
643 let schema = if is_success {
644 success_schema
645 } else {
646 error_schema
647 };
648
649 let output = self.get_output(Some(schema)).await?;
650
651 let Output::Json(json) = output else {
652 return Err(ApiClientError::UnsupportedJsonOutput {
653 output: output.clone(),
654 name: if is_success {
655 type_name::<T>()
656 } else {
657 type_name::<E>()
658 },
659 });
660 };
661
662 if is_success {
663 let value = self.deserialize_and_record::<T>(json).await?;
664 Ok(Ok(Some(value)))
665 } else {
666 let error = self.deserialize_and_record::<E>(json).await?;
667 Ok(Err(error))
668 }
669 }
670
671 /// Helper to deserialize JSON and record examples.
672 async fn deserialize_and_record<T>(&self, json: &str) -> Result<T, ApiClientError>
673 where
674 T: DeserializeOwned + ToSchema + 'static,
675 {
676 let deserializer = &mut serde_json::Deserializer::from_str(json);
677 let result: T = serde_path_to_error::deserialize(deserializer).map_err(|err| {
678 ApiClientError::JsonError {
679 path: err.path().to_string(),
680 error: err.into_inner(),
681 body: json.to_string(),
682 }
683 })?;
684
685 if let Ok(example) = serde_json::to_value(json) {
686 let mut cs = self.collectors.write().await;
687 cs.schemas.add_example::<T>(example);
688 }
689
690 Ok(result)
691 }
692
693 /// Processes the response as plain text.
694 ///
695 /// This method records the response in the OpenAPI specification and returns
696 /// the response body as a string slice. The response must have a text content type.
697 ///
698 /// # Returns
699 ///
700 /// - `Ok(&str)`: The response body as a string slice
701 /// - `Err(ApiClientError)`: If the response is not text
702 ///
703 /// # Example
704 ///
705 /// ```rust
706 /// # use clawspec_core::ApiClient;
707 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
708 /// let mut client = ApiClient::builder().build()?;
709 /// let text = client
710 /// .get("/api/status")?
711 ///
712 /// .await?
713 /// .as_text()
714 /// .await?;
715 /// # Ok(())
716 /// # }
717 /// ```
718 pub async fn as_text(&mut self) -> Result<&str, ApiClientError> {
719 let output = self.get_output(None).await?;
720
721 let Output::Text(text) = &output else {
722 return Err(ApiClientError::UnsupportedTextOutput {
723 output: output.clone(),
724 });
725 };
726
727 Ok(text)
728 }
729
730 /// Processes the response as binary data.
731 ///
732 /// This method records the response in the OpenAPI specification and returns
733 /// the response body as a byte slice. The response must have a binary content type.
734 ///
735 /// # Returns
736 ///
737 /// - `Ok(&[u8])`: The response body as a byte slice
738 /// - `Err(ApiClientError)`: If the response is not binary
739 ///
740 /// # Example
741 ///
742 /// ```rust
743 /// # use clawspec_core::ApiClient;
744 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
745 /// let mut client = ApiClient::builder().build()?;
746 /// let bytes = client
747 /// .get("/api/download")?
748 ///
749 /// .await?
750 /// .as_bytes()
751 /// .await?;
752 /// # Ok(())
753 /// # }
754 /// ```
755 pub async fn as_bytes(&mut self) -> Result<&[u8], ApiClientError> {
756 let output = self.get_output(None).await?;
757
758 let Output::Bytes(bytes) = &output else {
759 return Err(ApiClientError::UnsupportedBytesOutput {
760 output: output.clone(),
761 });
762 };
763
764 Ok(bytes.as_slice())
765 }
766
767 /// Processes the response as raw content with complete HTTP response information.
768 ///
769 /// This method records the response in the OpenAPI specification and returns
770 /// a [`RawResult`] containing the HTTP status code, content type, and response body.
771 /// This method supports both text and binary response content.
772 ///
773 /// # Returns
774 ///
775 /// - `Ok(RawResult)`: Complete raw response data including status, content type, and body
776 /// - `Err(ApiClientError)`: If processing fails
777 ///
778 /// # Example
779 ///
780 /// ```rust
781 /// use clawspec_core::{ApiClient, RawBody};
782 /// use http::StatusCode;
783 ///
784 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
785 /// let mut client = ApiClient::builder().build()?;
786 /// let raw_result = client
787 /// .get("/api/data")?
788 ///
789 /// .await?
790 /// .as_raw()
791 /// .await?;
792 ///
793 /// println!("Status: {}", raw_result.status_code());
794 /// if let Some(content_type) = raw_result.content_type() {
795 /// println!("Content-Type: {}", content_type);
796 /// }
797 ///
798 /// match raw_result.body() {
799 /// RawBody::Text(text) => println!("Text body: {}", text),
800 /// RawBody::Binary(bytes) => println!("Binary body: {} bytes", bytes.len()),
801 /// RawBody::Empty => println!("Empty body"),
802 /// }
803 /// # Ok(())
804 /// # }
805 /// ```
806 pub async fn as_raw(&mut self) -> Result<RawResult, ApiClientError> {
807 let output = self.get_output(None).await?;
808
809 let body = match output {
810 Output::Empty => RawBody::Empty,
811 Output::Json(body) | Output::Text(body) | Output::Other { body, .. } => {
812 RawBody::Text(body.clone())
813 }
814 Output::Bytes(bytes) => RawBody::Binary(bytes.clone()),
815 };
816
817 Ok(RawResult {
818 status: self.status,
819 content_type: self.content_type.clone(),
820 body,
821 })
822 }
823
824 /// Records this response as an empty response in the OpenAPI specification.
825 ///
826 /// This method should be used for endpoints that return no content (e.g., DELETE operations,
827 /// PUT operations that don't return a response body).
828 ///
829 /// # Example
830 ///
831 /// ```rust
832 /// # use clawspec_core::ApiClient;
833 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
834 /// let mut client = ApiClient::builder().build()?;
835 ///
836 /// client
837 /// .delete("/items/123")?
838 ///
839 /// .await?
840 /// .as_empty()
841 /// .await?;
842 /// # Ok(())
843 /// # }
844 /// ```
845 pub async fn as_empty(&mut self) -> Result<(), ApiClientError> {
846 self.get_output(None).await?;
847 Ok(())
848 }
849}