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_pretty_json()?);
236//!
237//! // Or output as YAML (requires "yaml" feature, see Chapter 1)
238//! // use clawspec_core::ToYaml;
239//! // println!("{}", spec.to_yaml()?);
240//! # Ok(())
241//! # }
242//! ```
243//!
244//! ## Key Points
245//!
246//! - Use `.with_tag()` and `.with_tags()` to organize operations
247//! - Use `.with_description()` to document operations
248//! - Configure API info and servers at the client builder level
249//! - Use `register_schemas!` for nested or error schemas
250//! - For YAML output, enable the `yaml` feature (see [Chapter 1][super::chapter_1])
251//!
252//! Next: [Chapter 6: Redaction][super::chapter_6] - Creating stable examples with
253//! dynamic value redaction.