clawspec_core/_tutorial/chapter_3.rs
1//! # Chapter 3: Response Handling
2//!
3//! This chapter covers the various ways to handle API responses, including
4//! error handling patterns.
5//!
6//! ## Response Methods Overview
7//!
8//! After sending a request, you have several options for handling the response:
9//!
10//! | Method | Returns | Use Case |
11//! |--------|---------|----------|
12//! | `as_json::<T>()` | `T` | Standard JSON response |
13//! | `as_optional_json::<T>()` | `Option<T>` | Resource that may not exist (404 → None) |
14//! | `as_result_json::<T, E>()` | `Result<T, E>` | API with typed error responses |
15//! | `as_result_option_json::<T, E>()` | `Result<Option<T>, E>` | Combined: 404 → Ok(None), errors → Err |
16//! | `as_raw()` | `RawResult` | Access status code and raw body |
17//! | `as_empty()` | `()` | Responses with no body (204, etc.) |
18//! | `as_text()` | `String` | Plain text responses |
19//!
20//! ## Standard JSON Response
21//!
22//! The most common pattern - parse JSON and fail on errors:
23//!
24//! ```rust,no_run
25//! # use clawspec_core::ApiClient;
26//! # use serde::Deserialize;
27//! # use utoipa::ToSchema;
28//! # #[derive(Deserialize, ToSchema)]
29//! # struct User { id: u64 }
30//! # #[tokio::main]
31//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
32//! # let mut client = ApiClient::builder().build()?;
33//! let user: User = client
34//! .get("/users/123")?
35//! .await?
36//! .as_json()
37//! .await?;
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! ## Optional JSON (404 as None)
43//!
44//! Use `as_optional_json()` when a resource might not exist:
45//!
46//! ```rust,no_run
47//! # use clawspec_core::ApiClient;
48//! # use serde::Deserialize;
49//! # use utoipa::ToSchema;
50//! # #[derive(Deserialize, ToSchema)]
51//! # struct User { id: u64, name: String }
52//! # #[tokio::main]
53//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
54//! # let mut client = ApiClient::builder().build()?;
55//! let user: Option<User> = client
56//! .get("/users/999")?
57//! .add_expected_status(404) // Tell client 404 is expected
58//! .await?
59//! .as_optional_json()
60//! .await?;
61//!
62//! match user {
63//! Some(u) => println!("Found: {}", u.name),
64//! None => println!("User not found"),
65//! }
66//! # Ok(())
67//! # }
68//! ```
69//!
70//! ## Result JSON (Typed Errors)
71//!
72//! When your API returns structured error responses:
73//!
74//! ```rust,no_run
75//! # use clawspec_core::ApiClient;
76//! # use serde::Deserialize;
77//! # use utoipa::ToSchema;
78//! # #[derive(Debug, Deserialize, ToSchema)]
79//! # struct User { id: u64 }
80//! #[derive(Debug, Deserialize, ToSchema)]
81//! struct ApiError {
82//! code: String,
83//! message: String,
84//! }
85//!
86//! # #[tokio::main]
87//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
88//! # let mut client = ApiClient::builder().build()?;
89//! let result: Result<User, ApiError> = client
90//! .get("/users/123")?
91//! .add_expected_status(404)
92//! .await?
93//! .as_result_json()
94//! .await?;
95//!
96//! match result {
97//! Ok(user) => println!("Got user: {:?}", user),
98//! Err(error) => println!("API error: {} - {}", error.code, error.message),
99//! }
100//! # Ok(())
101//! # }
102//! ```
103//!
104//! ## Result Option JSON (404 as Ok(None))
105//!
106//! Combines optional resources with typed errors:
107//!
108//! ```rust,no_run
109//! # use clawspec_core::ApiClient;
110//! # use serde::Deserialize;
111//! # use utoipa::ToSchema;
112//! # #[derive(Debug, Deserialize, ToSchema)]
113//! # struct User { id: u64, name: String }
114//! # #[derive(Debug, Deserialize, ToSchema)]
115//! # struct ApiError { message: String }
116//! # #[tokio::main]
117//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
118//! # let mut client = ApiClient::builder().build()?;
119//! // 2xx → Ok(Some(T))
120//! // 404 → Ok(None)
121//! // Other 4xx/5xx → Err(E)
122//! let result: Result<Option<User>, ApiError> = client
123//! .get("/users/maybe-exists")?
124//! .add_expected_status(404)
125//! .await?
126//! .as_result_option_json()
127//! .await?;
128//!
129//! match result {
130//! Ok(Some(user)) => println!("Found: {}", user.name),
131//! Ok(None) => println!("Not found (but not an error)"),
132//! Err(e) => println!("Actual error: {}", e.message),
133//! }
134//! # Ok(())
135//! # }
136//! ```
137//!
138//! ## Expected Status Codes
139//!
140//! By default, Clawspec expects 2xx-4xx status codes. Use these methods to
141//! customize expectations:
142//!
143//! ```rust,no_run
144//! use clawspec_core::{ApiClient, expected_status_codes};
145//! # use serde::Deserialize;
146//! # use utoipa::ToSchema;
147//! # #[derive(Deserialize, ToSchema)]
148//! # struct User { id: u64 }
149//!
150//! # #[tokio::main]
151//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
152//! # let mut client = ApiClient::builder().build()?;
153//! // Add a single expected status
154//! client.get("/users/123")?
155//! .add_expected_status(404)
156//! .await?;
157//!
158//! // Use specific status code
159//! client.post("/users")?
160//! .with_expected_status(201)
161//! .await?;
162//!
163//! // Use the macro for complex patterns
164//! client.get("/resource")?
165//! .with_expected_status_codes(expected_status_codes!(200, 201, 204))
166//! .await?;
167//!
168//! // Ranges are supported
169//! client.get("/resource")?
170//! .with_expected_status_codes(expected_status_codes!(200-299, 404))
171//! .await?;
172//! # Ok(())
173//! # }
174//! ```
175//!
176//! ## Raw Response Access
177//!
178//! When you need full control over the response:
179//!
180//! ```rust,no_run
181//! # use clawspec_core::ApiClient;
182//! # #[tokio::main]
183//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
184//! # let client = ApiClient::builder().build()?;
185//! let raw = client
186//! .get("/health")?
187//! .await?
188//! .as_raw()
189//! .await?;
190//!
191//! println!("Status: {}", raw.status_code());
192//! println!("Body: {:?}", raw.text());
193//!
194//! // Access as bytes
195//! let bytes: Option<&[u8]> = raw.bytes();
196//! # Ok(())
197//! # }
198//! ```
199//!
200//! ## Error Handling
201//!
202//! Clawspec uses [`ApiClientError`][crate::ApiClientError] for client-level errors:
203//!
204//! ```rust,no_run
205//! use clawspec_core::{ApiClient, ApiClientError};
206//!
207//! # #[tokio::main]
208//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
209//! # let client = ApiClient::builder().build()?;
210//! match client.get("/users/123")?.with_expected_status(200).await {
211//! Ok(response) => {
212//! // Handle success
213//! }
214//! Err(ApiClientError::UnexpectedStatusCode { status_code, body }) => {
215//! println!("Got status {}: {}", status_code, body);
216//! }
217//! Err(e) => {
218//! println!("Other error: {}", e);
219//! }
220//! }
221//! # Ok(())
222//! # }
223//! ```
224//!
225//! ## Key Points
226//!
227//! - Choose the response method that matches your API's behavior
228//! - Use `add_expected_status()` to tell Clawspec about expected non-2xx codes
229//! - `as_optional_json()` is great for "get or not found" patterns
230//! - `as_result_json()` captures typed error schemas in OpenAPI
231//!
232//! Next: [Chapter 4: Advanced Parameters][super::chapter_4] - Headers, cookies,
233//! and parameter styles.