clawspec_core/client/openapi/
result.rs

1use std::any::{TypeId, type_name};
2
3use headers::{ContentType, Header};
4use http::StatusCode;
5use http::header::CONTENT_TYPE;
6use reqwest::Response;
7use serde::de::DeserializeOwned;
8use utoipa::ToSchema;
9use utoipa::openapi::{RefOr, Schema};
10
11use super::channel::{CollectorMessage, CollectorSender};
12use super::schema::{SchemaEntry, compute_schema_ref};
13use crate::client::ApiClientError;
14use crate::client::response::output::Output;
15
16/// Represents the result of an API call with response processing capabilities.
17///
18/// This struct contains the response from an HTTP request along with methods to
19/// process the response in various formats (JSON, text, bytes, etc.) while
20/// automatically collecting OpenAPI schema information.
21///
22/// # ⚠️ Important: Response Consumption Required
23///
24/// **You must consume this `CallResult` by calling one of the response processing methods**
25/// to ensure proper OpenAPI documentation generation. Simply calling `exchange()` and not
26/// processing the result will result in incomplete OpenAPI specifications.
27///
28/// ## Required Response Processing
29///
30/// Choose the appropriate method based on your expected response:
31///
32/// - **Empty responses** (204 No Content, etc.): [`as_empty()`](Self::as_empty)
33/// - **JSON responses**: [`as_json::<T>()`](Self::as_json)
34/// - **Optional JSON responses** (204/404 → None): [`as_optional_json::<T>()`](Self::as_optional_json)
35/// - **Type-safe error handling**: [`as_result_json::<T, E>()`](Self::as_result_json) (2xx → Ok(T), 4xx/5xx → Err(E))
36/// - **Optional with errors**: [`as_result_option_json::<T, E>()`](Self::as_result_option_json) (combines optional and error handling)
37/// - **Text responses**: [`as_text()`](Self::as_text)
38/// - **Binary responses**: [`as_bytes()`](Self::as_bytes)
39/// - **Raw response access**: [`as_raw()`](Self::as_raw) (includes status code, content-type, and body)
40///
41/// ## Example: Correct Usage
42///
43/// ```rust
44/// use clawspec_core::ApiClient;
45/// # use serde::Deserialize;
46/// # use utoipa::ToSchema;
47/// # #[derive(Deserialize, ToSchema)]
48/// # struct User { id: u32, name: String }
49///
50/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
51/// let mut client = ApiClient::builder().build()?;
52///
53/// // ✅ CORRECT: Always consume the CallResult
54/// let user: User = client
55///     .get("/users/123")?
56///
57///     .await?
58///     .as_json()  // ← This is required!
59///     .await?;
60///
61/// // ✅ CORRECT: For empty responses (like DELETE)
62/// client
63///     .delete("/users/123")?
64///
65///     .await?
66///     .as_empty()  // ← This is required!
67///     .await?;
68///
69/// // ❌ INCORRECT: This will not generate proper OpenAPI documentation
70/// // let _result = client.get("/users/123")?.await?;
71/// // // Missing .as_json() or other consumption method! This will not generate proper OpenAPI documentation
72/// # Ok(())
73/// # }
74/// ```
75///
76/// ## Why This Matters
77///
78/// The OpenAPI schema generation relies on observing how responses are processed.
79/// Without calling a consumption method:
80/// - Response schemas won't be captured
81/// - Content-Type information may be incomplete
82/// - Operation examples won't be generated
83/// - The resulting OpenAPI spec will be missing crucial response documentation
84#[derive(Debug, Clone)]
85pub struct CallResult {
86    operation_id: String,
87    status: StatusCode,
88    content_type: Option<ContentType>,
89    output: Output,
90    pub(in crate::client) collector_sender: CollectorSender,
91}
92
93/// Represents the raw response data from an HTTP request.
94///
95/// This struct provides complete access to the HTTP response including status code,
96/// content type, and body data. It supports both text and binary response bodies.
97///
98/// # Example
99///
100/// ```rust
101/// use clawspec_core::{ApiClient, RawBody};
102/// use http::StatusCode;
103///
104/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
105/// let mut client = ApiClient::builder().build()?;
106/// let raw_result = client
107///     .get("/api/data")?
108///
109///     .await?
110///     .as_raw()
111///     .await?;
112///
113/// println!("Status: {}", raw_result.status_code());
114/// if let Some(content_type) = raw_result.content_type() {
115///     println!("Content-Type: {}", content_type);
116/// }
117/// match raw_result.body() {
118///     RawBody::Text(text) => println!("Text body: {}", text),
119///     RawBody::Binary(bytes) => println!("Binary body: {} bytes", bytes.len()),
120///     RawBody::Empty => println!("Empty body"),
121/// }
122/// # Ok(())
123/// # }
124/// ```
125#[derive(Debug, Clone)]
126pub struct RawResult {
127    status: StatusCode,
128    content_type: Option<String>,
129    body: RawBody,
130}
131
132/// Represents the body content of a raw HTTP response.
133///
134/// This enum handles different types of response bodies:
135/// - Text content (including JSON, HTML, XML, etc.)
136/// - Binary content (images, files, etc.)
137/// - Empty responses
138#[derive(Debug, Clone)]
139pub enum RawBody {
140    /// Text-based content (UTF-8 encoded)
141    Text(String),
142    /// Binary content
143    Binary(Vec<u8>),
144    /// Empty response body
145    Empty,
146}
147
148impl RawResult {
149    /// Returns the HTTP status code of the response.
150    pub fn status_code(&self) -> StatusCode {
151        self.status
152    }
153
154    /// Returns the content type of the response as a string, if present.
155    ///
156    /// # Returns
157    /// - `Some(&str)` with the MIME type (e.g., "application/json")
158    /// - `None` if no Content-Type header was present
159    pub fn content_type(&self) -> Option<&str> {
160        self.content_type.as_deref()
161    }
162
163    /// Returns the response body.
164    pub fn body(&self) -> &RawBody {
165        &self.body
166    }
167
168    /// Returns the response body as text if it's text content.
169    ///
170    /// # Returns
171    /// - `Some(&str)` if the body contains text
172    /// - `None` if the body is binary or empty
173    pub fn text(&self) -> Option<&str> {
174        match &self.body {
175            RawBody::Text(text) => Some(text),
176            _ => None,
177        }
178    }
179
180    /// Returns the response body as binary data if it's binary content.
181    ///
182    /// # Returns
183    /// - `Some(&[u8])` if the body contains binary data
184    /// - `None` if the body is text or empty
185    pub fn bytes(&self) -> Option<&[u8]> {
186        match &self.body {
187            RawBody::Binary(bytes) => Some(bytes),
188            _ => None,
189        }
190    }
191
192    /// Returns true if the response body is empty.
193    pub fn is_empty(&self) -> bool {
194        matches!(self.body, RawBody::Empty)
195    }
196}
197
198impl CallResult {
199    /// Returns the HTTP status code of the response.
200    pub(in crate::client) fn status(&self) -> StatusCode {
201        self.status
202    }
203
204    /// Returns the content type of the response, if present.
205    pub(in crate::client) fn content_type(&self) -> Option<&ContentType> {
206        self.content_type.as_ref()
207    }
208
209    /// Returns the operation ID for this result.
210    pub(in crate::client) fn operation_id(&self) -> &str {
211        &self.operation_id
212    }
213
214    /// Returns a reference to the output.
215    pub(in crate::client) fn output(&self) -> &Output {
216        &self.output
217    }
218
219    /// Extracts and parses the Content-Type header from the HTTP response.
220    fn extract_content_type(response: &Response) -> Result<Option<ContentType>, ApiClientError> {
221        let content_type = response
222            .headers()
223            .get_all(CONTENT_TYPE)
224            .iter()
225            .collect::<Vec<_>>();
226
227        if content_type.is_empty() {
228            Ok(None)
229        } else {
230            let ct = ContentType::decode(&mut content_type.into_iter())?;
231            Ok(Some(ct))
232        }
233    }
234
235    /// Processes the response body based on content type and status code.
236    async fn process_response_body(
237        response: Response,
238        content_type: &Option<ContentType>,
239        status: StatusCode,
240    ) -> Result<Output, ApiClientError> {
241        if let Some(content_type) = content_type
242            && status != StatusCode::NO_CONTENT
243        {
244            if *content_type == ContentType::json() {
245                let json = response.text().await?;
246                Ok(Output::Json(json))
247            } else if *content_type == ContentType::octet_stream() {
248                let bytes = response.bytes().await?;
249                Ok(Output::Bytes(bytes.to_vec()))
250            } else if content_type.to_string().starts_with("text/") {
251                let text = response.text().await?;
252                Ok(Output::Text(text))
253            } else {
254                let body = response.text().await?;
255                Ok(Output::Other { body })
256            }
257        } else {
258            Ok(Output::Empty)
259        }
260    }
261
262    pub(in crate::client) async fn new(
263        operation_id: String,
264        collector_sender: CollectorSender,
265        response: Response,
266    ) -> Result<Self, ApiClientError> {
267        let status = response.status();
268        let content_type = Self::extract_content_type(&response)?;
269        let output = Self::process_response_body(response, &content_type, status).await?;
270
271        Ok(Self {
272            operation_id,
273            status,
274            content_type,
275            output,
276            collector_sender,
277        })
278    }
279
280    pub(in crate::client) async fn new_without_collection(
281        response: Response,
282    ) -> Result<Self, ApiClientError> {
283        let status = response.status();
284        let content_type = Self::extract_content_type(&response)?;
285        let output = Self::process_response_body(response, &content_type, status).await?;
286
287        Ok(Self {
288            operation_id: String::new(), // Empty operation_id since it won't be used
289            status,
290            content_type,
291            output,
292            collector_sender: CollectorSender::dummy(),
293        })
294    }
295
296    pub(in crate::client) async fn get_output(
297        &self,
298        schema: Option<RefOr<Schema>>,
299    ) -> Result<&Output, ApiClientError> {
300        // Skip if operation_id is empty (skip_collection case)
301        if self.operation_id.is_empty() {
302            return Ok(&self.output);
303        }
304
305        // Send message to register the response
306        let status_code = self.status.as_u16();
307        let description = format!("Status code {status_code}");
308
309        self.collector_sender
310            .send(CollectorMessage::RegisterResponse {
311                operation_id: self.operation_id.clone(),
312                status: self.status,
313                content_type: self.content_type.clone(),
314                schema,
315                description,
316            })
317            .await;
318
319        Ok(&self.output)
320    }
321
322    /// Processes the response as JSON and deserializes it to the specified type.
323    ///
324    /// This method automatically records the response schema in the OpenAPI specification
325    /// and processes the response body as JSON. The type parameter must implement
326    /// `DeserializeOwned` and `ToSchema` for proper JSON parsing and schema generation.
327    ///
328    /// # Type Parameters
329    ///
330    /// - `T`: The target type for deserialization, must implement `DeserializeOwned`, `ToSchema`, and `'static`
331    ///
332    /// # Returns
333    ///
334    /// - `Ok(T)`: The deserialized response object
335    /// - `Err(ApiClientError)`: If the response is not JSON or deserialization fails
336    ///
337    /// # Example
338    ///
339    /// ```rust
340    /// # use clawspec_core::ApiClient;
341    /// # use serde::{Deserialize, Serialize};
342    /// # use utoipa::ToSchema;
343    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
344    /// #[derive(Deserialize, ToSchema)]
345    /// struct User {
346    ///     id: u32,
347    ///     name: String,
348    /// }
349    ///
350    /// let mut client = ApiClient::builder().build()?;
351    /// let user: User = client
352    ///     .get("/users/123")?
353    ///
354    ///     .await?
355    ///     .as_json()
356    ///     .await?;
357    /// # Ok(())
358    /// # }
359    /// ```
360    pub async fn as_json<T>(&mut self) -> Result<T, ApiClientError>
361    where
362        T: DeserializeOwned + ToSchema + 'static,
363    {
364        // Compute schema reference locally (no lock needed)
365        let schema = compute_schema_ref::<T>();
366
367        // Register the schema entry via channel
368        let entry = SchemaEntry::of::<T>();
369        self.collector_sender
370            .send(CollectorMessage::AddSchemaEntry(entry))
371            .await;
372
373        let output = self.get_output(Some(schema)).await?;
374
375        let Output::Json(json) = output else {
376            return Err(ApiClientError::UnsupportedJsonOutput {
377                output: output.clone(),
378                name: type_name::<T>(),
379            });
380        };
381
382        self.deserialize_and_record::<T>(json).await
383    }
384
385    /// Processes the response as optional JSON, treating 204 and 404 status codes as `None`.
386    ///
387    /// This method provides ergonomic handling of optional REST API responses by automatically
388    /// treating 204 (No Content) and 404 (Not Found) status codes as `None`, while deserializing
389    /// other successful responses as `Some(T)`. This is particularly useful for APIs that use
390    /// HTTP status codes to indicate the absence of data rather than errors.
391    ///
392    /// The method automatically records the response schema in the OpenAPI specification,
393    /// maintaining proper documentation generation.
394    ///
395    /// # Type Parameters
396    ///
397    /// - `T`: The target type for deserialization, must implement `DeserializeOwned`, `ToSchema`, and `'static`
398    ///
399    /// # Returns
400    ///
401    /// - `Ok(None)`: If the status code is 204 or 404
402    /// - `Ok(Some(T))`: The deserialized response object for other successful responses
403    /// - `Err(ApiClientError)`: If the response is not JSON or deserialization fails
404    ///
405    /// # Example
406    ///
407    /// ```rust
408    /// # use clawspec_core::ApiClient;
409    /// # use serde::{Deserialize, Serialize};
410    /// # use utoipa::ToSchema;
411    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
412    /// #[derive(Deserialize, ToSchema)]
413    /// struct User {
414    ///     id: u32,
415    ///     name: String,
416    /// }
417    ///
418    /// let mut client = ApiClient::builder().build()?;
419    ///
420    /// // Returns None for 404
421    /// let user: Option<User> = client
422    ///     .get("/users/nonexistent")?
423    ///
424    ///     .await?
425    ///     .as_optional_json()
426    ///     .await?;
427    /// assert!(user.is_none());
428    ///
429    /// // Returns Some(User) for successful response
430    /// let user: Option<User> = client
431    ///     .get("/users/123")?
432    ///
433    ///     .await?
434    ///     .as_optional_json()
435    ///     .await?;
436    /// assert!(user.is_some());
437    /// # Ok(())
438    /// # }
439    /// ```
440    pub async fn as_optional_json<T>(&mut self) -> Result<Option<T>, ApiClientError>
441    where
442        T: DeserializeOwned + ToSchema + 'static,
443    {
444        // Check if status code indicates absence of data
445        if self.status == StatusCode::NO_CONTENT || self.status == StatusCode::NOT_FOUND {
446            // Record the response without a schema
447            self.get_output(None).await?;
448            return Ok(None);
449        }
450
451        // Compute schema reference locally (no lock needed)
452        let schema = compute_schema_ref::<T>();
453
454        // Register the schema entry via channel
455        let entry = SchemaEntry::of::<T>();
456        self.collector_sender
457            .send(CollectorMessage::AddSchemaEntry(entry))
458            .await;
459
460        let output = self.get_output(Some(schema)).await?;
461
462        let Output::Json(json) = output else {
463            return Err(ApiClientError::UnsupportedJsonOutput {
464                output: output.clone(),
465                name: type_name::<T>(),
466            });
467        };
468
469        let result = self.deserialize_and_record::<T>(json).await?;
470        Ok(Some(result))
471    }
472
473    /// Processes the response as a `Result<T, E>` based on HTTP status code.
474    ///
475    /// This method provides type-safe error handling for REST APIs that return structured
476    /// error responses. It automatically deserializes the response body to either the
477    /// success type `T` (for 2xx status codes) or the error type `E` (for 4xx/5xx status codes).
478    ///
479    /// Both success and error schemas are automatically recorded in the OpenAPI specification,
480    /// providing complete documentation of your API's response patterns.
481    ///
482    /// # Type Parameters
483    ///
484    /// - `T`: The success response type, must implement `DeserializeOwned`, `ToSchema`, and `'static`
485    /// - `E`: The error response type, must implement `DeserializeOwned`, `ToSchema`, and `'static`
486    ///
487    /// # Returns
488    ///
489    /// - `Ok(T)`: The deserialized success response for 2xx status codes
490    /// - `Err(E)`: The deserialized error response for 4xx/5xx status codes
491    ///
492    /// # Errors
493    ///
494    /// Returns `ApiClientError` if:
495    /// - The response is not JSON
496    /// - JSON deserialization fails for either type
497    /// - The response body is empty when content is expected
498    ///
499    /// # Example
500    ///
501    /// ```rust
502    /// # use clawspec_core::ApiClient;
503    /// # use serde::{Deserialize, Serialize};
504    /// # use utoipa::ToSchema;
505    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
506    /// #[derive(Deserialize, ToSchema)]
507    /// struct User {
508    ///     id: u32,
509    ///     name: String,
510    /// }
511    ///
512    /// #[derive(Deserialize, ToSchema)]
513    /// struct ApiError {
514    ///     code: String,
515    ///     message: String,
516    /// }
517    ///
518    /// let mut client = ApiClient::builder().build()?;
519    ///
520    /// // Returns Ok(User) for 2xx responses
521    /// let result: Result<User, ApiError> = client
522    ///     .get("/users/123")?
523    ///
524    ///     .await?
525    ///     .as_result_json()
526    ///     .await?;
527    ///
528    /// match result {
529    ///     Ok(user) => println!("User: {}", user.name),
530    ///     Err(err) => println!("Error: {} - {}", err.code, err.message),
531    /// }
532    /// # Ok(())
533    /// # }
534    /// ```
535    pub async fn as_result_json<T, E>(&mut self) -> Result<Result<T, E>, ApiClientError>
536    where
537        T: DeserializeOwned + ToSchema + 'static,
538        E: DeserializeOwned + ToSchema + 'static,
539    {
540        Ok(self
541            .process_result_json_internal::<T, E>(false)
542            .await?
543            .map(|opt| opt.expect("BUG: 404 handling disabled but got None")))
544    }
545
546    /// Processes the response as a `Result<Option<T>, E>` based on HTTP status code.
547    ///
548    /// This method combines optional response handling with type-safe error handling,
549    /// providing comprehensive support for REST APIs that:
550    /// - Return structured error responses for failures (4xx/5xx)
551    /// - Use 204 (No Content) or 404 (Not Found) to indicate absence of data
552    /// - Return data for other successful responses (2xx)
553    ///
554    /// Both success and error schemas are automatically recorded in the OpenAPI specification.
555    ///
556    /// # Type Parameters
557    ///
558    /// - `T`: The success response type, must implement `DeserializeOwned`, `ToSchema`, and `'static`
559    /// - `E`: The error response type, must implement `DeserializeOwned`, `ToSchema`, and `'static`
560    ///
561    /// # Returns
562    ///
563    /// - `Ok(None)`: For 204 (No Content) or 404 (Not Found) status codes
564    /// - `Ok(Some(T))`: The deserialized success response for other 2xx status codes
565    /// - `Err(E)`: The deserialized error response for 4xx/5xx status codes
566    ///
567    /// # Errors
568    ///
569    /// Returns `ApiClientError` if:
570    /// - The response is not JSON (when content is expected)
571    /// - JSON deserialization fails for either type
572    ///
573    /// # Example
574    ///
575    /// ```rust
576    /// # use clawspec_core::ApiClient;
577    /// # use serde::{Deserialize, Serialize};
578    /// # use utoipa::ToSchema;
579    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
580    /// #[derive(Deserialize, ToSchema)]
581    /// struct User {
582    ///     id: u32,
583    ///     name: String,
584    /// }
585    ///
586    /// #[derive(Deserialize, ToSchema)]
587    /// struct ApiError {
588    ///     code: String,
589    ///     message: String,
590    /// }
591    ///
592    /// let mut client = ApiClient::builder().build()?;
593    ///
594    /// // Returns Ok(None) for 404
595    /// let result: Result<Option<User>, ApiError> = client
596    ///     .get("/users/nonexistent")?
597    ///
598    ///     .await?
599    ///     .as_result_option_json()
600    ///     .await?;
601    ///
602    /// match result {
603    ///     Ok(Some(user)) => println!("User: {}", user.name),
604    ///     Ok(None) => println!("User not found"),
605    ///     Err(err) => println!("Error: {} - {}", err.code, err.message),
606    /// }
607    /// # Ok(())
608    /// # }
609    /// ```
610    pub async fn as_result_option_json<T, E>(
611        &mut self,
612    ) -> Result<Result<Option<T>, E>, ApiClientError>
613    where
614        T: DeserializeOwned + ToSchema + 'static,
615        E: DeserializeOwned + ToSchema + 'static,
616    {
617        self.process_result_json_internal::<T, E>(true).await
618    }
619
620    /// Internal helper for processing Result<Option<T>, E> responses.
621    ///
622    /// Handles the common logic for both `as_result_json` and `as_result_option_json`.
623    async fn process_result_json_internal<T, E>(
624        &mut self,
625        treat_404_as_none: bool,
626    ) -> Result<Result<Option<T>, E>, ApiClientError>
627    where
628        T: DeserializeOwned + ToSchema + 'static,
629        E: DeserializeOwned + ToSchema + 'static,
630    {
631        // Check for 204/404 which indicate absence of data (when enabled)
632        if treat_404_as_none
633            && (self.status == StatusCode::NO_CONTENT || self.status == StatusCode::NOT_FOUND)
634        {
635            // Register both schema entries via channel
636            let success_entry = SchemaEntry::of::<T>();
637            let error_entry = SchemaEntry::of::<E>();
638            self.collector_sender
639                .send(CollectorMessage::AddSchemaEntry(success_entry))
640                .await;
641            self.collector_sender
642                .send(CollectorMessage::AddSchemaEntry(error_entry))
643                .await;
644
645            self.get_output(None).await?;
646            return Ok(Ok(None));
647        }
648
649        let is_success = self.status.is_success();
650
651        // Compute schema references locally (no lock needed)
652        let success_schema = compute_schema_ref::<T>();
653        let error_schema = compute_schema_ref::<E>();
654
655        // Register both schema entries via channel
656        let success_entry = SchemaEntry::of::<T>();
657        let error_entry = SchemaEntry::of::<E>();
658        self.collector_sender
659            .send(CollectorMessage::AddSchemaEntry(success_entry))
660            .await;
661        self.collector_sender
662            .send(CollectorMessage::AddSchemaEntry(error_entry))
663            .await;
664
665        // Get the appropriate schema based on status code
666        let schema = if is_success {
667            success_schema
668        } else {
669            error_schema
670        };
671
672        let output = self.get_output(Some(schema)).await?;
673
674        let Output::Json(json) = output else {
675            return Err(ApiClientError::UnsupportedJsonOutput {
676                output: output.clone(),
677                name: if is_success {
678                    type_name::<T>()
679                } else {
680                    type_name::<E>()
681                },
682            });
683        };
684
685        if is_success {
686            let value = self.deserialize_and_record::<T>(json).await?;
687            Ok(Ok(Some(value)))
688        } else {
689            let error = self.deserialize_and_record::<E>(json).await?;
690            Ok(Err(error))
691        }
692    }
693
694    /// Helper to deserialize JSON and record examples.
695    async fn deserialize_and_record<T>(&self, json: &str) -> Result<T, ApiClientError>
696    where
697        T: DeserializeOwned + ToSchema + 'static,
698    {
699        let deserializer = &mut serde_json::Deserializer::from_str(json);
700        let result: T = serde_path_to_error::deserialize(deserializer).map_err(|err| {
701            ApiClientError::JsonError {
702                path: err.path().to_string(),
703                error: err.into_inner(),
704                body: json.to_string(),
705            }
706        })?;
707
708        if let Ok(example) = serde_json::to_value(json) {
709            self.collector_sender
710                .send(CollectorMessage::AddExample {
711                    type_id: TypeId::of::<T>(),
712                    type_name: type_name::<T>(),
713                    example,
714                })
715                .await;
716        }
717
718        Ok(result)
719    }
720
721    /// Processes the response as plain text.
722    ///
723    /// This method records the response in the OpenAPI specification and returns
724    /// the response body as a string slice. The response must have a text content type.
725    ///
726    /// # Returns
727    ///
728    /// - `Ok(&str)`: The response body as a string slice
729    /// - `Err(ApiClientError)`: If the response is not text
730    ///
731    /// # Example
732    ///
733    /// ```rust
734    /// # use clawspec_core::ApiClient;
735    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
736    /// let mut client = ApiClient::builder().build()?;
737    /// let text = client
738    ///     .get("/api/status")?
739    ///
740    ///     .await?
741    ///     .as_text()
742    ///     .await?;
743    /// # Ok(())
744    /// # }
745    /// ```
746    pub async fn as_text(&mut self) -> Result<&str, ApiClientError> {
747        let output = self.get_output(None).await?;
748
749        let Output::Text(text) = &output else {
750            return Err(ApiClientError::UnsupportedTextOutput {
751                output: output.clone(),
752            });
753        };
754
755        Ok(text)
756    }
757
758    /// Processes the response as binary data.
759    ///
760    /// This method records the response in the OpenAPI specification and returns
761    /// the response body as a byte slice. The response must have a binary content type.
762    ///
763    /// # Returns
764    ///
765    /// - `Ok(&[u8])`: The response body as a byte slice
766    /// - `Err(ApiClientError)`: If the response is not binary
767    ///
768    /// # Example
769    ///
770    /// ```rust
771    /// # use clawspec_core::ApiClient;
772    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
773    /// let mut client = ApiClient::builder().build()?;
774    /// let bytes = client
775    ///     .get("/api/download")?
776    ///
777    ///     .await?
778    ///     .as_bytes()
779    ///     .await?;
780    /// # Ok(())
781    /// # }
782    /// ```
783    pub async fn as_bytes(&mut self) -> Result<&[u8], ApiClientError> {
784        let output = self.get_output(None).await?;
785
786        let Output::Bytes(bytes) = &output else {
787            return Err(ApiClientError::UnsupportedBytesOutput {
788                output: output.clone(),
789            });
790        };
791
792        Ok(bytes.as_slice())
793    }
794
795    /// Processes the response as raw content with complete HTTP response information.
796    ///
797    /// This method records the response in the OpenAPI specification and returns
798    /// a [`RawResult`] containing the HTTP status code, content type, and response body.
799    /// This method supports both text and binary response content.
800    ///
801    /// # Returns
802    ///
803    /// - `Ok(RawResult)`: Complete raw response data including status, content type, and body
804    /// - `Err(ApiClientError)`: If processing fails
805    ///
806    /// # Example
807    ///
808    /// ```rust
809    /// use clawspec_core::{ApiClient, RawBody};
810    /// use http::StatusCode;
811    ///
812    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
813    /// let mut client = ApiClient::builder().build()?;
814    /// let raw_result = client
815    ///     .get("/api/data")?
816    ///
817    ///     .await?
818    ///     .as_raw()
819    ///     .await?;
820    ///
821    /// println!("Status: {}", raw_result.status_code());
822    /// if let Some(content_type) = raw_result.content_type() {
823    ///     println!("Content-Type: {}", content_type);
824    /// }
825    ///
826    /// match raw_result.body() {
827    ///     RawBody::Text(text) => println!("Text body: {}", text),
828    ///     RawBody::Binary(bytes) => println!("Binary body: {} bytes", bytes.len()),
829    ///     RawBody::Empty => println!("Empty body"),
830    /// }
831    /// # Ok(())
832    /// # }
833    /// ```
834    pub async fn as_raw(&mut self) -> Result<RawResult, ApiClientError> {
835        let output = self.get_output(None).await?;
836
837        let body = match output {
838            Output::Empty => RawBody::Empty,
839            Output::Json(body) | Output::Text(body) | Output::Other { body, .. } => {
840                RawBody::Text(body.clone())
841            }
842            Output::Bytes(bytes) => RawBody::Binary(bytes.clone()),
843        };
844
845        Ok(RawResult {
846            status: self.status,
847            content_type: self.content_type.as_ref().map(|ct| ct.to_string()),
848            body,
849        })
850    }
851
852    /// Records this response as an empty response in the OpenAPI specification.
853    ///
854    /// This method should be used for endpoints that return no content (e.g., DELETE operations,
855    /// PUT operations that don't return a response body).
856    ///
857    /// # Example
858    ///
859    /// ```rust
860    /// # use clawspec_core::ApiClient;
861    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
862    /// let mut client = ApiClient::builder().build()?;
863    ///
864    /// client
865    ///     .delete("/items/123")?
866    ///
867    ///     .await?
868    ///     .as_empty()
869    ///     .await?;
870    /// # Ok(())
871    /// # }
872    /// ```
873    pub async fn as_empty(&mut self) -> Result<(), ApiClientError> {
874        self.get_output(None).await?;
875        Ok(())
876    }
877}
878
879#[cfg(test)]
880mod tests {
881    use super::*;
882
883    #[test]
884    fn test_raw_body_text_variant() {
885        let body = RawBody::Text("Hello, World!".to_string());
886
887        match body {
888            RawBody::Text(text) => assert_eq!(text, "Hello, World!"),
889            _ => panic!("Expected Text variant"),
890        }
891    }
892
893    #[test]
894    fn test_raw_body_binary_variant() {
895        let data = vec![0x01, 0x02, 0x03, 0x04];
896        let body = RawBody::Binary(data.clone());
897
898        match body {
899            RawBody::Binary(bytes) => assert_eq!(bytes, data),
900            _ => panic!("Expected Binary variant"),
901        }
902    }
903
904    #[test]
905    fn test_raw_body_empty_variant() {
906        let body = RawBody::Empty;
907
908        assert!(matches!(body, RawBody::Empty));
909    }
910
911    #[test]
912    fn test_raw_result_status_code() {
913        let result = RawResult {
914            status: StatusCode::OK,
915            content_type: Some("application/json".to_string()),
916            body: RawBody::Text("{}".to_string()),
917        };
918
919        assert_eq!(result.status_code(), StatusCode::OK);
920    }
921
922    #[test]
923    fn test_raw_result_content_type_some() {
924        let result = RawResult {
925            status: StatusCode::OK,
926            content_type: Some("text/plain".to_string()),
927            body: RawBody::Text("Hello".to_string()),
928        };
929
930        assert_eq!(result.content_type(), Some("text/plain"));
931    }
932
933    #[test]
934    fn test_raw_result_content_type_none() {
935        let result = RawResult {
936            status: StatusCode::NO_CONTENT,
937            content_type: None,
938            body: RawBody::Empty,
939        };
940
941        assert_eq!(result.content_type(), None);
942    }
943
944    #[test]
945    fn test_raw_result_body() {
946        let result = RawResult {
947            status: StatusCode::OK,
948            content_type: Some("application/json".to_string()),
949            body: RawBody::Text("{\"key\": \"value\"}".to_string()),
950        };
951
952        assert!(matches!(result.body(), RawBody::Text(_)));
953    }
954
955    #[test]
956    fn test_raw_result_text_with_text_body() {
957        let result = RawResult {
958            status: StatusCode::OK,
959            content_type: Some("text/plain".to_string()),
960            body: RawBody::Text("Hello, World!".to_string()),
961        };
962
963        assert_eq!(result.text(), Some("Hello, World!"));
964    }
965
966    #[test]
967    fn test_raw_result_text_with_binary_body() {
968        let result = RawResult {
969            status: StatusCode::OK,
970            content_type: Some("application/octet-stream".to_string()),
971            body: RawBody::Binary(vec![0x00, 0x01, 0x02]),
972        };
973
974        assert_eq!(result.text(), None);
975    }
976
977    #[test]
978    fn test_raw_result_text_with_empty_body() {
979        let result = RawResult {
980            status: StatusCode::NO_CONTENT,
981            content_type: None,
982            body: RawBody::Empty,
983        };
984
985        assert_eq!(result.text(), None);
986    }
987
988    #[test]
989    fn test_raw_result_bytes_with_binary_body() {
990        let data = vec![0x48, 0x65, 0x6c, 0x6c, 0x6f]; // "Hello" in ASCII
991        let result = RawResult {
992            status: StatusCode::OK,
993            content_type: Some("application/octet-stream".to_string()),
994            body: RawBody::Binary(data.clone()),
995        };
996
997        assert_eq!(result.bytes(), Some(data.as_slice()));
998    }
999
1000    #[test]
1001    fn test_raw_result_bytes_with_text_body() {
1002        let result = RawResult {
1003            status: StatusCode::OK,
1004            content_type: Some("text/plain".to_string()),
1005            body: RawBody::Text("Hello".to_string()),
1006        };
1007
1008        assert_eq!(result.bytes(), None);
1009    }
1010
1011    #[test]
1012    fn test_raw_result_bytes_with_empty_body() {
1013        let result = RawResult {
1014            status: StatusCode::NO_CONTENT,
1015            content_type: None,
1016            body: RawBody::Empty,
1017        };
1018
1019        assert_eq!(result.bytes(), None);
1020    }
1021
1022    #[test]
1023    fn test_raw_result_is_empty_true() {
1024        let result = RawResult {
1025            status: StatusCode::NO_CONTENT,
1026            content_type: None,
1027            body: RawBody::Empty,
1028        };
1029
1030        assert!(result.is_empty());
1031    }
1032
1033    #[test]
1034    fn test_raw_result_is_empty_false_text() {
1035        let result = RawResult {
1036            status: StatusCode::OK,
1037            content_type: Some("text/plain".to_string()),
1038            body: RawBody::Text("content".to_string()),
1039        };
1040
1041        assert!(!result.is_empty());
1042    }
1043
1044    #[test]
1045    fn test_raw_result_is_empty_false_binary() {
1046        let result = RawResult {
1047            status: StatusCode::OK,
1048            content_type: Some("application/octet-stream".to_string()),
1049            body: RawBody::Binary(vec![1, 2, 3]),
1050        };
1051
1052        assert!(!result.is_empty());
1053    }
1054
1055    #[test]
1056    fn test_raw_body_debug_impl() {
1057        let text_body = RawBody::Text("Hello".to_string());
1058        let debug_str = format!("{text_body:?}");
1059        assert!(debug_str.contains("Text"));
1060        assert!(debug_str.contains("Hello"));
1061
1062        let binary_body = RawBody::Binary(vec![1, 2, 3]);
1063        let debug_str = format!("{binary_body:?}");
1064        assert!(debug_str.contains("Binary"));
1065
1066        let empty_body = RawBody::Empty;
1067        let debug_str = format!("{empty_body:?}");
1068        assert!(debug_str.contains("Empty"));
1069    }
1070
1071    #[test]
1072    fn test_raw_body_clone() {
1073        let original = RawBody::Text("test".to_string());
1074        let cloned = original.clone();
1075
1076        match (original, cloned) {
1077            (RawBody::Text(a), RawBody::Text(b)) => assert_eq!(a, b),
1078            _ => panic!("Clone should preserve variant"),
1079        }
1080    }
1081
1082    #[test]
1083    fn test_raw_result_debug_impl() {
1084        let result = RawResult {
1085            status: StatusCode::OK,
1086            content_type: Some("application/json".to_string()),
1087            body: RawBody::Text("{}".to_string()),
1088        };
1089
1090        let debug_str = format!("{result:?}");
1091        assert!(debug_str.contains("RawResult"));
1092        assert!(debug_str.contains("200"));
1093    }
1094
1095    #[test]
1096    fn test_raw_result_clone() {
1097        let original = RawResult {
1098            status: StatusCode::CREATED,
1099            content_type: Some("text/plain".to_string()),
1100            body: RawBody::Text("Created".to_string()),
1101        };
1102
1103        let cloned = original.clone();
1104
1105        assert_eq!(cloned.status_code(), StatusCode::CREATED);
1106        assert_eq!(cloned.content_type(), Some("text/plain"));
1107        assert_eq!(cloned.text(), Some("Created"));
1108    }
1109}