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//! # // `with_response_description` requires the `redaction` feature; gate the example so
59//! # // `cargo test --doc` (default features) still compiles, and keep a no-op `main` for it.
60//! # #[cfg(feature = "redaction")]
61//! # use clawspec_core::ApiClient;
62//! # #[cfg(feature = "redaction")]
63//! # #[tokio::main]
64//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
65//! # let mut client = ApiClient::builder().build()?;
66//! client.get("/users/123")?
67//! .with_response_description("User details or 404 if not found")
68//! .await?;
69//!
70//! client.post("/users")?
71//! .with_response_description("The newly created user with generated ID")
72//! .await?;
73//! # Ok(())
74//! # }
75//! # #[cfg(not(feature = "redaction"))]
76//! # fn main() {}
77//! ```
78//!
79//! ## API Info Configuration
80//!
81//! Configure the API's metadata when building the client:
82//!
83//! ```rust,no_run
84//! use clawspec_core::ApiClient;
85//! use utoipa::openapi::{ContactBuilder, InfoBuilder, LicenseBuilder};
86//!
87//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
88//! let info = InfoBuilder::new()
89//! .title("My API")
90//! .version("1.0.0")
91//! .description(Some("A comprehensive REST API for managing resources"))
92//! .contact(Some(
93//! ContactBuilder::new()
94//! .name(Some("API Support"))
95//! .email(Some("support@example.com"))
96//! .url(Some("https://example.com/support"))
97//! .build(),
98//! ))
99//! .license(Some(
100//! LicenseBuilder::new()
101//! .name("MIT")
102//! .url(Some("https://opensource.org/licenses/MIT"))
103//! .build(),
104//! ))
105//! .build();
106//!
107//! let client = ApiClient::builder()
108//! .with_host("api.example.com")
109//! .with_info(info)
110//! .build()?;
111//! # Ok(())
112//! # }
113//! ```
114//!
115//! ## Server Configuration
116//!
117//! Define servers in the OpenAPI spec:
118//!
119//! ```rust,no_run
120//! use clawspec_core::ApiClient;
121//! use utoipa::openapi::ServerBuilder;
122//!
123//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
124//! let client = ApiClient::builder()
125//! .with_host("api.example.com")
126//! .add_server(
127//! ServerBuilder::new()
128//! .url("https://api.example.com")
129//! .description(Some("Production server"))
130//! .build(),
131//! )
132//! .add_server(
133//! ServerBuilder::new()
134//! .url("https://staging-api.example.com")
135//! .description(Some("Staging server"))
136//! .build(),
137//! )
138//! .build()?;
139//! # Ok(())
140//! # }
141//! ```
142//!
143//! ## Manual Schema Registration
144//!
145//! Sometimes you need to register schemas that aren't automatically captured:
146//!
147//! ```rust,no_run
148//! use clawspec_core::{ApiClient, register_schemas};
149//! use serde::{Deserialize, Serialize};
150//! use utoipa::ToSchema;
151//!
152//! #[derive(Serialize, Deserialize, ToSchema)]
153//! struct Address {
154//! street: String,
155//! city: String,
156//! country: String,
157//! }
158//!
159//! #[derive(Serialize, Deserialize, ToSchema)]
160//! struct User {
161//! id: u64,
162//! name: String,
163//! address: Address, // Nested schema
164//! }
165//!
166//! #[derive(Serialize, Deserialize, ToSchema)]
167//! struct ApiError {
168//! code: String,
169//! message: String,
170//! }
171//!
172//! # #[tokio::main]
173//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
174//! let mut client = ApiClient::builder().build()?;
175//!
176//! // Register nested schemas and error types
177//! register_schemas!(client, User, Address, ApiError).await;
178//! # Ok(())
179//! # }
180//! ```
181//!
182//! This is particularly useful for:
183//! - Nested schemas that might not be fully resolved
184//! - Error response types
185//! - Schemas used in headers or other non-body locations
186//!
187//! ## Combining Everything
188//!
189//! Here's a complete example with all customizations:
190//!
191//! ```rust,no_run
192//! use clawspec_core::{ApiClient, register_schemas};
193//! use utoipa::openapi::{ContactBuilder, InfoBuilder, ServerBuilder};
194//! use serde::{Deserialize, Serialize};
195//! use utoipa::ToSchema;
196//!
197//! #[derive(Serialize, ToSchema)]
198//! struct CreateUser { name: String }
199//!
200//! #[derive(Deserialize, ToSchema)]
201//! struct User { id: u64, name: String }
202//!
203//! #[derive(Deserialize, ToSchema)]
204//! struct ApiError { code: String, message: String }
205//!
206//! # // Uses `with_response_description` (redaction feature); gate so `cargo test --doc`
207//! # // with default features compiles, with a no-op `main` fallback for that case.
208//! # #[cfg(feature = "redaction")]
209//! # #[tokio::main]
210//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
211//! // Configure client with full metadata
212//! let info = InfoBuilder::new()
213//! .title("User Management API")
214//! .version("2.0.0")
215//! .description(Some("API for managing user accounts"))
216//! .build();
217//!
218//! let mut client = ApiClient::builder()
219//! .with_host("api.example.com")
220//! .with_info(info)
221//! .add_server(
222//! ServerBuilder::new()
223//! .url("https://api.example.com/v2")
224//! .description(Some("Production"))
225//! .build(),
226//! )
227//! .build()?;
228//!
229//! // Register error schema
230//! register_schemas!(client, ApiError).await;
231//!
232//! // Make requests with full documentation
233//! let user: User = client.post("/users")?
234//! .json(&CreateUser { name: "Alice".to_string() })?
235//! .with_tag("users")
236//! .with_description("Create a new user account")
237//! .with_response_description("The created user with assigned ID")
238//! .await?
239//! .as_json()
240//! .await?;
241//!
242//! // Generate the OpenAPI spec
243//! let spec = client.collected_openapi().await;
244//! println!("{}", spec.to_pretty_json()?);
245//!
246//! // Or output as YAML (requires "yaml" feature, see Chapter 1)
247//! // use clawspec_core::ToYaml;
248//! // println!("{}", spec.to_yaml()?);
249//! # Ok(())
250//! # }
251//! # #[cfg(not(feature = "redaction"))]
252//! # fn main() {}
253//! ```
254//!
255//! ## Key Points
256//!
257//! - Use `.with_tag()` and `.with_tags()` to organize operations
258//! - Use `.with_description()` to document operations
259//! - Configure API info and servers at the client builder level
260//! - Use `register_schemas!` for nested or error schemas
261//! - For YAML output, enable the `yaml` feature (see [Chapter 1][super::chapter_1])
262//!
263//! Next: [Chapter 6: Redaction][super::chapter_6] - Creating stable examples with
264//! dynamic value redaction.