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}