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