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}