clawspec_core/_tutorial/
chapter_5.rs

1//! # Chapter 5: OpenAPI Customization
2//!
3//! This chapter covers how to customize the generated OpenAPI specification with
4//! tags, descriptions, and metadata.
5//!
6//! ## Adding Operation Tags
7//!
8//! Tags help organize operations in the generated documentation:
9//!
10//! ```rust,no_run
11//! # use clawspec_core::ApiClient;
12//! # #[tokio::main]
13//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
14//! # let mut client = ApiClient::builder().build()?;
15//! // Single tag
16//! client.get("/users")?
17//!     .with_tag("users")
18//!     .await?;
19//!
20//! // Multiple tags
21//! client.post("/admin/users")?
22//!     .with_tags(["users", "admin"])
23//!     .await?;
24//! # Ok(())
25//! # }
26//! ```
27//!
28//! Tags appear in the OpenAPI spec and are used by documentation tools to group
29//! related endpoints.
30//!
31//! ## Operation Descriptions
32//!
33//! Add descriptions to document what operations do:
34//!
35//! ```rust,no_run
36//! # use clawspec_core::ApiClient;
37//! # #[tokio::main]
38//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
39//! # let mut client = ApiClient::builder().build()?;
40//! client.get("/users")?
41//!     .with_tag("users")
42//!     .with_description("List all users with optional pagination")
43//!     .await?;
44//!
45//! client.post("/users")?
46//!     .with_tag("users")
47//!     .with_description("Create a new user account")
48//!     .await?;
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! ## Response Descriptions
54//!
55//! Document what responses mean:
56//!
57//! ```rust,no_run
58//! # use clawspec_core::ApiClient;
59//! # #[tokio::main]
60//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
61//! # let mut client = ApiClient::builder().build()?;
62//! client.get("/users/123")?
63//!     .with_response_description("User details or 404 if not found")
64//!     .await?;
65//!
66//! client.post("/users")?
67//!     .with_response_description("The newly created user with generated ID")
68//!     .await?;
69//! # Ok(())
70//! # }
71//! ```
72//!
73//! ## API Info Configuration
74//!
75//! Configure the API's metadata when building the client:
76//!
77//! ```rust,no_run
78//! use clawspec_core::ApiClient;
79//! use utoipa::openapi::{ContactBuilder, InfoBuilder, LicenseBuilder};
80//!
81//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
82//! let info = InfoBuilder::new()
83//!     .title("My API")
84//!     .version("1.0.0")
85//!     .description(Some("A comprehensive REST API for managing resources"))
86//!     .contact(Some(
87//!         ContactBuilder::new()
88//!             .name(Some("API Support"))
89//!             .email(Some("support@example.com"))
90//!             .url(Some("https://example.com/support"))
91//!             .build(),
92//!     ))
93//!     .license(Some(
94//!         LicenseBuilder::new()
95//!             .name("MIT")
96//!             .url(Some("https://opensource.org/licenses/MIT"))
97//!             .build(),
98//!     ))
99//!     .build();
100//!
101//! let client = ApiClient::builder()
102//!     .with_host("api.example.com")
103//!     .with_info(info)
104//!     .build()?;
105//! # Ok(())
106//! # }
107//! ```
108//!
109//! ## Server Configuration
110//!
111//! Define servers in the OpenAPI spec:
112//!
113//! ```rust,no_run
114//! use clawspec_core::ApiClient;
115//! use utoipa::openapi::ServerBuilder;
116//!
117//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
118//! let client = ApiClient::builder()
119//!     .with_host("api.example.com")
120//!     .add_server(
121//!         ServerBuilder::new()
122//!             .url("https://api.example.com")
123//!             .description(Some("Production server"))
124//!             .build(),
125//!     )
126//!     .add_server(
127//!         ServerBuilder::new()
128//!             .url("https://staging-api.example.com")
129//!             .description(Some("Staging server"))
130//!             .build(),
131//!     )
132//!     .build()?;
133//! # Ok(())
134//! # }
135//! ```
136//!
137//! ## Manual Schema Registration
138//!
139//! Sometimes you need to register schemas that aren't automatically captured:
140//!
141//! ```rust,no_run
142//! use clawspec_core::{ApiClient, register_schemas};
143//! use serde::{Deserialize, Serialize};
144//! use utoipa::ToSchema;
145//!
146//! #[derive(Serialize, Deserialize, ToSchema)]
147//! struct Address {
148//!     street: String,
149//!     city: String,
150//!     country: String,
151//! }
152//!
153//! #[derive(Serialize, Deserialize, ToSchema)]
154//! struct User {
155//!     id: u64,
156//!     name: String,
157//!     address: Address,  // Nested schema
158//! }
159//!
160//! #[derive(Serialize, Deserialize, ToSchema)]
161//! struct ApiError {
162//!     code: String,
163//!     message: String,
164//! }
165//!
166//! # #[tokio::main]
167//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
168//! let mut client = ApiClient::builder().build()?;
169//!
170//! // Register nested schemas and error types
171//! register_schemas!(client, User, Address, ApiError).await;
172//! # Ok(())
173//! # }
174//! ```
175//!
176//! This is particularly useful for:
177//! - Nested schemas that might not be fully resolved
178//! - Error response types
179//! - Schemas used in headers or other non-body locations
180//!
181//! ## Combining Everything
182//!
183//! Here's a complete example with all customizations:
184//!
185//! ```rust,no_run
186//! use clawspec_core::{ApiClient, register_schemas};
187//! use utoipa::openapi::{ContactBuilder, InfoBuilder, ServerBuilder};
188//! use serde::{Deserialize, Serialize};
189//! use utoipa::ToSchema;
190//!
191//! #[derive(Serialize, ToSchema)]
192//! struct CreateUser { name: String }
193//!
194//! #[derive(Deserialize, ToSchema)]
195//! struct User { id: u64, name: String }
196//!
197//! #[derive(Deserialize, ToSchema)]
198//! struct ApiError { code: String, message: String }
199//!
200//! # #[tokio::main]
201//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
202//! // Configure client with full metadata
203//! let info = InfoBuilder::new()
204//!     .title("User Management API")
205//!     .version("2.0.0")
206//!     .description(Some("API for managing user accounts"))
207//!     .build();
208//!
209//! let mut client = ApiClient::builder()
210//!     .with_host("api.example.com")
211//!     .with_info(info)
212//!     .add_server(
213//!         ServerBuilder::new()
214//!             .url("https://api.example.com/v2")
215//!             .description(Some("Production"))
216//!             .build(),
217//!     )
218//!     .build()?;
219//!
220//! // Register error schema
221//! register_schemas!(client, ApiError).await;
222//!
223//! // Make requests with full documentation
224//! let user: User = client.post("/users")?
225//!     .json(&CreateUser { name: "Alice".to_string() })?
226//!     .with_tag("users")
227//!     .with_description("Create a new user account")
228//!     .with_response_description("The created user with assigned ID")
229//!     .await?
230//!     .as_json()
231//!     .await?;
232//!
233//! // Generate the OpenAPI spec
234//! let spec = client.collected_openapi().await;
235//! println!("{}", spec.to_yaml()?);
236//! # Ok(())
237//! # }
238//! ```
239//!
240//! ## Key Points
241//!
242//! - Use `.with_tag()` and `.with_tags()` to organize operations
243//! - Use `.with_description()` to document operations
244//! - Configure API info and servers at the client builder level
245//! - Use `register_schemas!` for nested or error schemas
246//!
247//! Next: [Chapter 6: Redaction][super::chapter_6] - Creating stable examples with
248//! dynamic value redaction.