clawspec_core/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! # Clawspec Core
4//!
5//! Generate OpenAPI specifications from your HTTP client test code.
6//!
7//! This crate provides two main ways to generate OpenAPI documentation:
8//! - **[`ApiClient`]** - Direct HTTP client for fine-grained control
9//! - **[`TestClient`](test_client::TestClient)** - Test server integration with automatic lifecycle management
10//!
11//! **New to Clawspec?** Start with the **[Tutorial][_tutorial]** for a step-by-step guide.
12//!
13//! ## Quick Start
14//!
15//! ### Using ApiClient directly
16//!
17//! ```rust,no_run
18//! use clawspec_core::ApiClient;
19//! # use serde::Deserialize;
20//! # use utoipa::ToSchema;
21//! # #[derive(Deserialize, ToSchema)]
22//! # struct User { id: u32, name: String }
23//!
24//! # #[tokio::main]
25//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
26//! let mut client = ApiClient::builder()
27//!     .with_host("api.example.com")
28//!     .build()?;
29//!
30//! // Make requests - schemas are captured automatically  
31//! let user: User = client
32//!     .get("/users/123")?
33//!     .await?  // ← Direct await using IntoFuture
34//!     .as_json()  // ← Important: Must consume result for OpenAPI generation!
35//!     .await?;
36//!
37//! // Generate OpenAPI specification
38//! let spec = client.collected_openapi().await;
39//! # Ok(())
40//! # }
41//! ```
42//!
43//! ### Using TestClient with a test server
44//!
45//! For a complete working example, see the [axum example](https://github.com/ilaborie/clawspec/tree/main/examples/axum-example).
46//!
47//! ```rust,no_run
48//! use clawspec_core::test_client::{TestClient, TestServer};
49//! use std::net::TcpListener;
50//!
51//! # #[derive(Debug)]
52//! # struct MyServer;
53//! # impl TestServer for MyServer {
54//! #     type Error = std::io::Error;
55//! #     async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
56//! #         Ok(())
57//! #     }
58//! # }
59//! #[tokio::test]
60//! async fn test_api() -> Result<(), Box<dyn std::error::Error>> {
61//!     let mut client = TestClient::start(MyServer).await?;
62//!     
63//!     // Test your API
64//!     let response = client.get("/users")?.await?.as_json::<serde_json::Value>().await?;
65//!     
66//!     // Write OpenAPI spec
67//!     client.write_openapi("api.yml").await?;
68//!     Ok(())
69//! }
70//! ```
71//!
72//! ## Working with Parameters
73//!
74//! ```rust
75//! use clawspec_core::{ApiClient, CallPath, CallQuery, CallHeaders, CallCookies, ParamValue, ParamStyle};
76//!
77//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
78//! // Path parameters  
79//! let path = CallPath::from("/users/{id}")
80//!     .add_param("id", ParamValue::new(123));
81//!
82//! // Query parameters
83//! let query = CallQuery::new()
84//!     .add_param("page", ParamValue::new(1))
85//!     .add_param("limit", ParamValue::new(10));
86//!
87//! // Headers
88//! let headers = CallHeaders::new()
89//!     .add_header("Authorization", "Bearer token");
90//!
91//! // Cookies
92//! let cookies = CallCookies::new()
93//!     .add_cookie("session_id", "abc123")
94//!     .add_cookie("user_id", 456);
95//!
96//! // Direct await with parameters:
97//! let response = client
98//!     .get(path)?
99//!     .with_query(query)
100//!     .with_headers(headers)
101//!     .with_cookies(cookies)
102//!     .await?;  // Direct await using IntoFuture
103//! # Ok(())
104//! # }
105//! ```
106//!
107//! ## OpenAPI 3.1.0 Parameter Styles
108//!
109//! This library supports all OpenAPI 3.1.0 parameter styles for different parameter types:
110//!
111//! ### Path Parameters
112//!
113//! ```rust
114//! use clawspec_core::{CallPath, ParamValue, ParamStyle};
115//!
116//! # async fn example() {
117//! // Simple style (default): /users/123
118//! let path = CallPath::from("/users/{id}")
119//!     .add_param("id", ParamValue::new(123));
120//!
121//! // Label style: /users/.123
122//! let path = CallPath::from("/users/{id}")
123//!     .add_param("id", ParamValue::with_style(123, ParamStyle::Label));
124//!
125//! // Matrix style: /users/;id=123
126//! let path = CallPath::from("/users/{id}")
127//!     .add_param("id", ParamValue::with_style(123, ParamStyle::Matrix));
128//!
129//! // Arrays with different styles
130//! let tags = vec!["rust", "web", "api"];
131//!
132//! // Simple: /search/rust,web,api
133//! let path = CallPath::from("/search/{tags}")
134//!     .add_param("tags", ParamValue::with_style(tags.clone(), ParamStyle::Simple));
135//!
136//! // Label: /search/.rust,web,api
137//! let path = CallPath::from("/search/{tags}")
138//!     .add_param("tags", ParamValue::with_style(tags.clone(), ParamStyle::Label));
139//!
140//! // Matrix: /search/;tags=rust,web,api
141//! let path = CallPath::from("/search/{tags}")
142//!     .add_param("tags", ParamValue::with_style(tags, ParamStyle::Matrix));
143//! # }
144//! ```
145//!
146//! ### Query Parameters
147//!
148//! ```rust
149//! use clawspec_core::{CallQuery, ParamValue, ParamStyle};
150//!
151//! # async fn example() {
152//! let tags = vec!["rust", "web", "api"];
153//!
154//! // Form style (default): ?tags=rust&tags=web&tags=api
155//! let query = CallQuery::new()
156//!     .add_param("tags", ParamValue::new(tags.clone()));
157//!
158//! // Space delimited: ?tags=rust%20web%20api
159//! let query = CallQuery::new()
160//!     .add_param("tags", ParamValue::with_style(tags.clone(), ParamStyle::SpaceDelimited));
161//!
162//! // Pipe delimited: ?tags=rust|web|api
163//! let query = CallQuery::new()
164//!     .add_param("tags", ParamValue::with_style(tags, ParamStyle::PipeDelimited));
165//!
166//! // Deep object style: ?user[name]=john&user[age]=30
167//! let user_data = serde_json::json!({"name": "john", "age": 30});
168//! let query = CallQuery::new()
169//!     .add_param("user", ParamValue::with_style(user_data, ParamStyle::DeepObject));
170//! # }
171//! ```
172//!
173//! ### Cookie Parameters
174//!
175//! ```rust
176//! use clawspec_core::{CallCookies, ParamValue};
177//!
178//! # async fn example() {
179//! // Simple cookie values
180//! let cookies = CallCookies::new()
181//!     .add_cookie("session_id", "abc123")
182//!     .add_cookie("user_id", 456)
183//!     .add_cookie("is_admin", true);
184//!
185//! // Array values in cookies (comma-separated)
186//! let cookies = CallCookies::new()
187//!     .add_cookie("preferences", vec!["dark_mode", "notifications"])
188//!     .add_cookie("selected_tags", vec!["rust", "web", "api"]);
189//!
190//! // Custom types with automatic serialization
191//! #[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
192//! struct UserId(u64);
193//!
194//! let cookies = CallCookies::new()
195//!     .add_cookie("user", UserId(12345));
196//! # }
197//! ```
198//!
199//! ## Authentication
200//!
201//! The library supports various authentication methods that can be configured at the client level
202//! or overridden for individual requests.
203//!
204//! ### Client-Level Authentication
205//!
206//! ```rust
207//! use clawspec_core::{ApiClient, Authentication};
208//!
209//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
210//! // Bearer token authentication
211//! let client = ApiClient::builder()
212//!     .with_host("api.example.com")
213//!     .with_authentication(Authentication::Bearer("my-api-token".into()))
214//!     .build()?;
215//!
216//! // Basic authentication
217//! let client = ApiClient::builder()
218//!     .with_host("api.example.com")
219//!     .with_authentication(Authentication::Basic {
220//!         username: "user".to_string(),
221//!         password: "pass".into(),
222//!     })
223//!     .build()?;
224//!
225//! // API key authentication
226//! let client = ApiClient::builder()
227//!     .with_host("api.example.com")
228//!     .with_authentication(Authentication::ApiKey {
229//!         header_name: "X-API-Key".to_string(),
230//!         key: "secret-key".into(),
231//!     })
232//!     .build()?;
233//! # Ok(())
234//! # }
235//! ```
236//!
237//! ### Per-Request Authentication Override
238//!
239//! ```rust
240//! use clawspec_core::{ApiClient, Authentication};
241//!
242//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
243//! // Client with default authentication
244//! let mut client = ApiClient::builder()
245//!     .with_host("api.example.com")
246//!     .with_authentication(Authentication::Bearer("default-token".into()))
247//!     .build()?;
248//!
249//! // Use different authentication for admin endpoints
250//! let admin_users = client
251//!     .get("/admin/users")?
252//!     .with_authentication(Authentication::Bearer("admin-token".into()))
253//!     .await?
254//!     .as_json::<serde_json::Value>()
255//!     .await?;
256//!
257//! // Remove authentication for public endpoints
258//! let public_data = client
259//!     .get("/public/health")?
260//!     .with_authentication_none()
261//!     .await?
262//!     .as_text()
263//!     .await?;
264//! # Ok(())
265//! # }
266//! ```
267//!
268//! ### Authentication Types
269//!
270//! - **Bearer**: Adds `Authorization: Bearer <token>` header
271//! - **Basic**: Adds `Authorization: Basic <base64(username:password)>` header  
272//! - **ApiKey**: Adds custom header with API key
273//!
274//! ### Security Best Practices
275//!
276//! - Store credentials securely using environment variables or secret management tools
277//! - Rotate tokens regularly
278//! - Use HTTPS for all authenticated requests
279//! - Avoid logging authentication headers
280//!
281//! ## Status Code Validation
282//!
283//! By default, requests expect status codes in the range 200-499 (inclusive of 200, exclusive of 500).
284//! You can customize this behavior:
285//!
286//! ```rust
287//! use clawspec_core::{ApiClient, expected_status_codes};
288//!
289//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
290//! // Single codes
291//! client.post("/users")?
292//!     .with_expected_status_codes(expected_status_codes!(201, 202))
293//!     .await?;
294//!
295//! // Ranges
296//! client.get("/health")?
297//!     .with_expected_status_codes(expected_status_codes!(200-299))
298//!     .await?;
299//! # Ok(())
300//! # }
301//! ```
302//!
303//! ## Response Descriptions
304//!
305//! Add descriptive text to your OpenAPI responses for better documentation:
306//!
307//! ```rust
308//! use clawspec_core::ApiClient;
309//!
310//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
311//! // Set a description for the actual returned status code
312//! client.get("/users/{id}")?
313//!     .with_response_description("User details if found, or error information")
314//!     .await?;
315//!
316//! // The description applies to whatever status code is actually returned
317//! client.post("/users")?
318//!     .with_response_description("User created successfully or validation error")
319//!     .await?;
320//! # Ok(())
321//! # }
322//! ```
323//!
324//! ## Response Redaction
325//!
326//! *Requires the `redaction` feature.*
327//!
328//! When generating OpenAPI examples from real API responses, dynamic values like UUIDs,
329//! timestamps, and tokens make examples unstable across test runs. The redaction feature
330//! allows you to replace these dynamic values with stable, predictable ones in the generated
331//! OpenAPI specification while preserving the actual values for assertions.
332//!
333//! This is particularly useful for:
334//! - **Snapshot testing**: Generated OpenAPI files remain stable across runs
335//! - **Documentation**: Examples show consistent, readable placeholder values
336//! - **Security**: Sensitive values can be masked in documentation
337//!
338//! ### Basic Usage
339//!
340#![cfg_attr(feature = "redaction", doc = "```rust")]
341#![cfg_attr(not(feature = "redaction"), doc = "```rust,ignore")]
342//! use clawspec_core::ApiClient;
343//! # use serde::Deserialize;
344//! # use utoipa::ToSchema;
345//!
346//! #[derive(Deserialize, ToSchema)]
347//! struct User {
348//!     id: String,           // Dynamic UUID
349//!     name: String,
350//!     created_at: String,   // Dynamic timestamp
351//! }
352//!
353//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
354//! let user: User = client
355//!     .post("/users")?
356//!     .json(&serde_json::json!({"name": "Alice"}))?
357//!     .await?
358//!     .as_json_redacted()
359//!     .await?
360//!     // Replace dynamic UUID with stable value
361//!     .redact("/id", "00000000-0000-0000-0000-000000000001")?
362//!     // Replace timestamp with stable value
363//!     .redact("/created_at", "2024-01-01T00:00:00Z")?
364//!     .finish()
365//!     .await
366//!     .value;
367//!
368//! // The actual user has real dynamic values for assertions
369//! assert!(!user.id.is_empty());
370//! // But the OpenAPI example shows the redacted stable values
371//! # Ok(())
372//! # }
373//! ```
374//!
375//! ### Redaction Operations
376//!
377//! The redaction builder supports two operations using [JSON Pointer (RFC 6901)](https://tools.ietf.org/html/rfc6901):
378//!
379//! - **`redact(pointer, redactor)`**: Replace a value at the given path with a stable value or transformation
380//! - **`redact_remove(pointer)`**: Remove a value entirely from the OpenAPI example
381//!
382#![cfg_attr(feature = "redaction", doc = "```rust")]
383#![cfg_attr(not(feature = "redaction"), doc = "```rust,ignore")]
384//! # use clawspec_core::ApiClient;
385//! # use serde::Deserialize;
386//! # use utoipa::ToSchema;
387//! # #[derive(Deserialize, ToSchema)]
388//! # struct Response { token: String, session_id: String, internal_ref: String }
389//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
390//! let response: Response = client
391//!     .post("/auth/login")?
392//!     .json(&serde_json::json!({"username": "test", "password": "secret"}))?
393//!     .await?
394//!     .as_json_redacted()
395//!     .await?
396//!     .redact("/token", "[REDACTED_TOKEN]")?
397//!     .redact("/session_id", "session-00000000")?
398//!     .redact_remove("/internal_ref")?  // Remove internal field from docs
399//!     .finish()
400//!     .await
401//!     .value;
402//! # Ok(())
403//! # }
404//! ```
405//!
406//! ### Path Syntax
407//!
408//! Paths are auto-detected based on their prefix:
409//! - `/...` → JSON Pointer (RFC 6901) - exact paths only
410//! - `$...` → JSONPath (RFC 9535) - supports wildcards
411//!
412//! ### JSON Pointer Syntax
413//!
414//! JSON Pointers use `/` as a path separator. Special characters are escaped:
415//! - `~0` represents `~`
416//! - `~1` represents `/`
417//!
418//! Examples:
419//! - `/id` - Top-level field named "id"
420//! - `/user/name` - Nested field "name" inside "user"
421//! - `/items/0/id` - First element's "id" in an array
422//! - `/foo~1bar` - Field named "foo/bar"
423//!
424//! ### JSONPath Wildcards
425//!
426//! For arrays, use JSONPath syntax (starting with `$`) to redact all elements:
427//!
428#![cfg_attr(feature = "redaction", doc = "```rust")]
429#![cfg_attr(not(feature = "redaction"), doc = "```rust,ignore")]
430//! # use clawspec_core::ApiClient;
431//! # use serde::Deserialize;
432//! # use utoipa::ToSchema;
433//! # #[derive(Deserialize, ToSchema)]
434//! # struct User { id: String, created_at: String }
435//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
436//! let users: Vec<User> = client
437//!     .get("/users")?
438//!     .await?
439//!     .as_json_redacted()
440//!     .await?
441//!     .redact("$[*].id", "stable-uuid")?        // All IDs in array
442//!     .redact("$[*].created_at", "2024-01-01T00:00:00Z")?  // All timestamps
443//!     .finish()
444//!     .await
445//!     .value;
446//! # Ok(())
447//! # }
448//! ```
449//!
450//! ### Dynamic Transformations
451//!
452//! Pass a closure for dynamic redaction. The closure receives the concrete
453//! JSON Pointer path and current value:
454//!
455#![cfg_attr(feature = "redaction", doc = "```rust")]
456#![cfg_attr(not(feature = "redaction"), doc = "```rust,ignore")]
457//! # use clawspec_core::ApiClient;
458//! # use serde::Deserialize;
459//! # use serde_json::Value;
460//! # use utoipa::ToSchema;
461//! # #[derive(Deserialize, ToSchema)]
462//! # struct User { id: String }
463//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
464//! let users: Vec<User> = client
465//!     .get("/users")?
466//!     .await?
467//!     .as_json_redacted()
468//!     .await?
469//!     // Create stable index-based IDs: user-0, user-1, user-2, ...
470//!     .redact("$[*].id", |path: &str, _val: &Value| {
471//!         let idx = path.split('/').nth(1).unwrap_or("0");
472//!         serde_json::json!(format!("user-{idx}"))
473//!     })?
474//!     .finish()
475//!     .await
476//!     .value;
477//! # Ok(())
478//! # }
479//! ```
480//!
481#![cfg_attr(feature = "redaction", doc = "### Getting Both Values")]
482#![cfg_attr(feature = "redaction", doc = "")]
483#![cfg_attr(
484    feature = "redaction",
485    doc = "The [`RedactedResult`] returned by `finish()` contains both:"
486)]
487#![cfg_attr(
488    feature = "redaction",
489    doc = "- `value`: The actual deserialized response (with real dynamic values)"
490)]
491#![cfg_attr(
492    feature = "redaction",
493    doc = "- `redacted`: The JSON with redacted values (as stored in OpenAPI)"
494)]
495//!
496#![cfg_attr(feature = "redaction", doc = "```rust")]
497#![cfg_attr(not(feature = "redaction"), doc = "```rust,ignore")]
498//! # use clawspec_core::ApiClient;
499//! # use serde::Deserialize;
500//! # use utoipa::ToSchema;
501//! # #[derive(Deserialize, ToSchema)]
502//! # struct User { id: String }
503//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
504//! let result = client
505//!     .get("/users/123")?
506//!     .await?
507//!     .as_json_redacted::<User>()
508//!     .await?
509//!     .redact("/id", "user-00000000")?
510//!     .finish()
511//!     .await;
512//!
513//! // Use actual value for test assertions
514//! let user = result.value;
515//! assert!(!user.id.is_empty());
516//!
517//! // Access redacted JSON if needed
518//! let redacted_json = result.redacted;
519//! assert_eq!(redacted_json["id"], "user-00000000");
520//! # Ok(())
521//! # }
522//! ```
523//!
524//! ## Schema Registration
525//!
526//! ### Automatic Schema Capture
527//!
528//! JSON request and response body schemas are **automatically captured** when using `.json()` and `.as_json()` methods:
529//!
530//! ```rust
531//! use clawspec_core::ApiClient;
532//! # use serde::{Serialize, Deserialize};
533//! # use utoipa::ToSchema;
534//!
535//! #[derive(Serialize, Deserialize, ToSchema)]
536//! struct CreateUser { name: String, email: String }
537//!
538//! #[derive(Deserialize, ToSchema)]
539//! struct User { id: u32, name: String, email: String }
540//!
541//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
542//! // Schemas are captured automatically - no explicit registration needed
543//! let user: User = client
544//!     .post("/users")?
545//!     .json(&CreateUser { name: "Alice".to_string(), email: "alice@example.com".to_string() })?
546//!     .await?
547//!     .as_json()
548//!     .await?;
549//! # Ok(())
550//! # }
551//! ```
552//!
553//! ### Manual Schema Registration
554//!
555//! For nested schemas or when you need to ensure all dependencies are included, use the `register_schemas!` macro:
556//!
557//! ```rust
558//! use clawspec_core::{ApiClient, register_schemas};
559//! # use serde::{Serialize, Deserialize};
560//! # use utoipa::ToSchema;
561//!
562//! #[derive(Serialize, Deserialize, ToSchema)]
563//! struct Address { street: String, city: String }
564//!
565//! #[derive(Serialize, Deserialize, ToSchema)]
566//! struct CreateUser { name: String, email: String, address: Address }
567//!
568//! #[derive(Deserialize, ToSchema)]
569//! struct ErrorResponse { code: String, message: String }
570//!
571//! # async fn example(client: &mut ApiClient) {
572//! // Register nested schemas and error types for complete documentation
573//! register_schemas!(client, CreateUser, Address, ErrorResponse).await;
574//! # }
575//! ```
576//!
577//! ### ⚠️ Nested Schema Limitation
578//!
579//! **Current Limitation**: While main JSON body schemas are captured automatically, nested schemas may not be fully resolved. If you encounter missing nested schemas in your OpenAPI specification, use the `register_schemas!` macro to explicitly register them:
580//!
581//! ```rust
582//! use clawspec_core::{ApiClient, register_schemas};
583//! # use serde::{Serialize, Deserialize};
584//! # use utoipa::ToSchema;
585//!
586//! #[derive(Serialize, Deserialize, ToSchema)]
587//! struct Position { lat: f64, lng: f64 }
588//!
589//! #[derive(Serialize, Deserialize, ToSchema)]
590//! struct Location { name: String, position: Position }  // Position is nested
591//!
592//! # async fn example(client: &mut ApiClient) {
593//! // Register both main and nested schemas to ensure complete OpenAPI generation
594//! register_schemas!(client, Location, Position).await;
595//! # }
596//! ```
597//!
598//! **Workaround**: Always register nested schemas explicitly when you need complete OpenAPI documentation with all referenced types properly defined.
599//!
600//! ## Error Handling
601//!
602//! The library provides two main error types:
603//! - [`ApiClientError`] - HTTP client errors (network, parsing, validation)
604//! - [`TestAppError`](test_client::TestAppError) - Test server lifecycle errors
605//!
606//! ## See Also
607//!
608//! - [`ApiClient`] - HTTP client with OpenAPI collection
609//! - [`ApiCall`] - Request builder with parameter support
610//! - [`test_client`] - Test server integration module
611//! - [`ExpectedStatusCodes`] - Status code validation
612#![cfg_attr(
613    feature = "redaction",
614    doc = "- [`RedactionBuilder`] - Builder for redacting response values in OpenAPI examples"
615)]
616#![cfg_attr(
617    feature = "redaction",
618    doc = "- [`RedactedResult`] - Result containing both actual and redacted values"
619)]
620#![cfg_attr(
621    feature = "redaction",
622    doc = "- [`RedactOptions`] - Options for configuring redaction behavior"
623)]
624#![cfg_attr(
625    feature = "redaction",
626    doc = "- [`Redactor`] - Trait for types that can be used to redact values"
627)]
628//!
629//! ## Re-exports
630//!
631//! All commonly used types are re-exported from the crate root for convenience.
632
633// TODO: Add comprehensive unit tests for all modules - https://github.com/ilaborie/clawspec/issues/30
634
635pub mod _tutorial;
636
637mod client;
638
639pub mod test_client;
640
641// Public API - only expose user-facing types and functions
642pub use self::client::{
643    ApiCall, ApiClient, ApiClientBuilder, ApiClientError, Authentication, AuthenticationError,
644    CallBody, CallCookies, CallHeaders, CallPath, CallQuery, CallResult, ExpectedStatusCodes,
645    ParamStyle, ParamValue, ParameterValue, RawBody, RawResult, SecureString,
646};
647
648// Re-export external types so users don't need to add these crates to their Cargo.toml.
649//
650// With these re-exports, users can write:
651//   use clawspec_core::{ApiClient, OpenApi, ToSchema, StatusCode};
652// Instead of:
653//   use clawspec_core::ApiClient;
654//   use utoipa::openapi::OpenApi;
655//   use utoipa::ToSchema;
656//   use http::StatusCode;
657
658/// OpenAPI types re-exported from utoipa for convenience.
659pub use utoipa::openapi::{Info, InfoBuilder, OpenApi, Paths, Server, ServerBuilder};
660
661/// The `ToSchema` derive macro for generating OpenAPI schemas.
662/// Types used in JSON request/response bodies should derive this trait.
663pub use utoipa::ToSchema;
664
665/// HTTP status codes re-exported from the `http` crate.
666pub use http::StatusCode;
667
668#[cfg(feature = "redaction")]
669pub use self::client::{RedactOptions, RedactedResult, RedactionBuilder, Redactor};
670
671// Convenience macro re-exports are handled by the macro_rules! definitions below
672
673/// Creates an [`ExpectedStatusCodes`] instance with the specified status codes and ranges.
674///
675/// This macro provides a convenient syntax for defining expected HTTP status codes
676/// with support for individual codes, inclusive ranges, and exclusive ranges.
677///
678/// # Syntax
679///
680/// - Single codes: `200`, `201`, `404`
681/// - Inclusive ranges: `200-299` (includes both endpoints)
682/// - Exclusive ranges: `200..300` (excludes 300)
683/// - Mixed: `200, 201-204, 400..500`
684///
685/// # Examples
686///
687/// ```rust
688/// use clawspec_core::expected_status_codes;
689///
690/// // Single status codes
691/// let codes = expected_status_codes!(200, 201, 204);
692///
693/// // Ranges
694/// let success_codes = expected_status_codes!(200-299);
695/// let client_errors = expected_status_codes!(400..500);
696///
697/// // Mixed
698/// let mixed = expected_status_codes!(200-204, 301, 302, 400-404);
699/// ```
700#[macro_export]
701macro_rules! expected_status_codes {
702    // Empty case
703    () => {
704        $crate::ExpectedStatusCodes::default()
705    };
706
707    // Single element
708    ($single:literal) => {
709        $crate::ExpectedStatusCodes::from_single($single)
710    };
711
712    // Single range (inclusive)
713    ($start:literal - $end:literal) => {
714        $crate::ExpectedStatusCodes::from_inclusive_range($start..=$end)
715    };
716
717    // Single range (exclusive)
718    ($start:literal .. $end:literal) => {
719        $crate::ExpectedStatusCodes::from_exclusive_range($start..$end)
720    };
721
722    // Multiple elements - single code followed by more
723    ($first:literal, $($rest:tt)*) => {{
724        #[allow(unused_mut)]
725        let mut codes = $crate::ExpectedStatusCodes::from_single($first);
726        $crate::expected_status_codes!(@accumulate codes, $($rest)*);
727        codes
728    }};
729
730    // Multiple elements - inclusive range followed by more
731    ($start:literal - $end:literal, $($rest:tt)*) => {{
732        #[allow(unused_mut)]
733        let mut codes = $crate::ExpectedStatusCodes::from_inclusive_range($start..=$end);
734        $crate::expected_status_codes!(@accumulate codes, $($rest)*);
735        codes
736    }};
737
738    // Multiple elements - exclusive range followed by more
739    ($start:literal .. $end:literal, $($rest:tt)*) => {{
740        #[allow(unused_mut)]
741        let mut codes = $crate::ExpectedStatusCodes::from_exclusive_range($start..$end);
742        $crate::expected_status_codes!(@accumulate codes, $($rest)*);
743        codes
744    }};
745
746    // Internal accumulator - empty (base case for trailing commas)
747    (@accumulate $codes:ident,) => {
748        // Do nothing for trailing commas
749    };
750
751    // Internal accumulator - single code
752    (@accumulate $codes:ident, $single:literal) => {
753        $codes = $codes.add_single($single);
754    };
755
756    // Internal accumulator - single code followed by more
757    (@accumulate $codes:ident, $single:literal, $($rest:tt)*) => {
758        $codes = $codes.add_single($single);
759        $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
760    };
761
762    // Internal accumulator - inclusive range
763    (@accumulate $codes:ident, $start:literal - $end:literal) => {
764        $codes = $codes.add_inclusive_range($start..=$end);
765    };
766
767    // Internal accumulator - inclusive range followed by more
768    (@accumulate $codes:ident, $start:literal - $end:literal, $($rest:tt)*) => {
769        $codes = $codes.add_inclusive_range($start..=$end);
770        $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
771    };
772
773    // Internal accumulator - exclusive range
774    (@accumulate $codes:ident, $start:literal .. $end:literal) => {
775        $codes = $codes.add_exclusive_range($start..$end);
776    };
777
778    // Internal accumulator - exclusive range followed by more
779    (@accumulate $codes:ident, $start:literal .. $end:literal, $($rest:tt)*) => {
780        $codes = $codes.add_exclusive_range($start..$end);
781        $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
782    };
783
784    // Internal accumulator - empty (catch all for trailing cases)
785    (@accumulate $codes:ident) => {
786        // Base case - do nothing
787    };
788}
789
790/// Registers multiple schema types with the ApiClient for OpenAPI documentation.
791///
792/// This macro simplifies the process of registering multiple types that implement
793/// [`utoipa::ToSchema`] with an [`ApiClient`] instance.
794///
795/// # When to Use
796///
797/// - **Nested Schemas**: When your JSON types contain nested structures that need to be fully resolved
798/// - **Error Types**: To ensure error response schemas are included in the OpenAPI specification
799/// - **Complex Dependencies**: When automatic schema capture doesn't include all referenced types
800///
801/// # Automatic vs Manual Registration
802///
803/// Most JSON request/response schemas are captured automatically when using `.json()` and `.as_json()` methods.
804/// Use this macro when you need to ensure complete schema coverage, especially for nested types.
805///
806/// # Examples
807///
808/// ## Basic Usage
809///
810/// ```rust
811/// use clawspec_core::{ApiClient, register_schemas};
812/// use serde::{Serialize, Deserialize};
813/// use utoipa::ToSchema;
814///
815/// #[derive(Serialize, Deserialize, ToSchema)]
816/// struct User {
817///     id: u64,
818///     name: String,
819/// }
820///
821/// #[derive(Serialize, Deserialize, ToSchema)]
822/// struct Post {
823///     id: u64,
824///     title: String,
825///     author_id: u64,
826/// }
827///
828/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
829/// let mut client = ApiClient::builder().build()?;
830///
831/// // Register multiple schemas at once
832/// register_schemas!(client, User, Post).await;
833/// # Ok(())
834/// # }
835/// ```
836///
837/// ## Nested Schemas
838///
839/// ```rust
840/// use clawspec_core::{ApiClient, register_schemas};
841/// use serde::{Serialize, Deserialize};
842/// use utoipa::ToSchema;
843///
844/// #[derive(Serialize, Deserialize, ToSchema)]
845/// struct Address {
846///     street: String,
847///     city: String,
848/// }
849///
850/// #[derive(Serialize, Deserialize, ToSchema)]
851/// struct User {
852///     id: u64,
853///     name: String,
854///     address: Address,  // Nested schema
855/// }
856///
857/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
858/// let mut client = ApiClient::builder().build()?;
859///
860/// // Register both main and nested schemas for complete OpenAPI generation
861/// register_schemas!(client, User, Address).await;
862/// # Ok(())
863/// # }
864/// ```
865#[macro_export]
866macro_rules! register_schemas {
867    ($client:expr, $($schema:ty),+ $(,)?) => {
868        async {
869            $(
870                $client.register_schema::<$schema>().await;
871            )+
872        }
873    };
874}
875
876#[cfg(test)]
877mod macro_tests {
878    use super::*;
879
880    #[test]
881    fn test_expected_status_codes_single() {
882        let codes = expected_status_codes!(200);
883        assert!(codes.contains(200));
884        assert!(!codes.contains(201));
885    }
886
887    #[test]
888    fn test_expected_status_codes_multiple_single() {
889        let codes = expected_status_codes!(200, 201, 204);
890        assert!(codes.contains(200));
891        assert!(codes.contains(201));
892        assert!(codes.contains(204));
893        assert!(!codes.contains(202));
894    }
895
896    #[test]
897    fn test_expected_status_codes_range() {
898        let codes = expected_status_codes!(200 - 204);
899        assert!(codes.contains(200));
900        assert!(codes.contains(202));
901        assert!(codes.contains(204));
902        assert!(!codes.contains(205));
903    }
904
905    #[test]
906    fn test_expected_status_codes_mixed() {
907        let codes = expected_status_codes!(200, 201 - 204, 301, 400 - 404);
908        assert!(codes.contains(200));
909        assert!(codes.contains(202));
910        assert!(codes.contains(301));
911        assert!(codes.contains(402));
912        assert!(!codes.contains(305));
913    }
914
915    #[test]
916    fn test_expected_status_codes_trailing_comma() {
917        let codes = expected_status_codes!(200, 201,);
918        assert!(codes.contains(200));
919        assert!(codes.contains(201));
920    }
921
922    #[test]
923    fn test_expected_status_codes_range_trailing_comma() {
924        let codes = expected_status_codes!(200 - 204,);
925        assert!(codes.contains(202));
926    }
927
928    #[test]
929    fn test_expected_status_codes_five_elements() {
930        let codes = expected_status_codes!(200, 201, 202, 203, 204);
931        assert!(codes.contains(200));
932        assert!(codes.contains(201));
933        assert!(codes.contains(202));
934        assert!(codes.contains(203));
935        assert!(codes.contains(204));
936    }
937
938    #[test]
939    fn test_expected_status_codes_eight_elements() {
940        let codes = expected_status_codes!(200, 201, 202, 203, 204, 205, 206, 207);
941        assert!(codes.contains(200));
942        assert!(codes.contains(204));
943        assert!(codes.contains(207));
944    }
945
946    #[test]
947    fn test_expected_status_codes_multiple_ranges() {
948        let codes = expected_status_codes!(200 - 204, 300 - 304, 400 - 404);
949        assert!(codes.contains(202));
950        assert!(codes.contains(302));
951        assert!(codes.contains(402));
952        assert!(!codes.contains(205));
953        assert!(!codes.contains(305));
954    }
955
956    #[test]
957    fn test_expected_status_codes_edge_cases() {
958        // Empty should work
959        let _codes = expected_status_codes!();
960
961        // Single range should work
962        let codes = expected_status_codes!(200 - 299);
963        assert!(codes.contains(250));
964    }
965
966    #[test]
967    fn test_expected_status_codes_common_patterns() {
968        // Success codes
969        let success = expected_status_codes!(200 - 299);
970        assert!(success.contains(200));
971        assert!(success.contains(201));
972        assert!(success.contains(204));
973
974        // Client errors
975        let client_errors = expected_status_codes!(400 - 499);
976        assert!(client_errors.contains(400));
977        assert!(client_errors.contains(404));
978        assert!(client_errors.contains(422));
979
980        // Specific success codes
981        let specific = expected_status_codes!(200, 201, 204);
982        assert!(specific.contains(200));
983        assert!(!specific.contains(202));
984    }
985
986    #[test]
987    fn test_expected_status_codes_builder_alternative() {
988        // Using macro
989        let macro_codes = expected_status_codes!(200 - 204, 301, 302, 400 - 404);
990
991        // Using builder (should be equivalent)
992        let builder_codes = ExpectedStatusCodes::default()
993            .add_inclusive_range(200..=204)
994            .add_single(301)
995            .add_single(302)
996            .add_inclusive_range(400..=404);
997
998        // Both should have same results
999        for code in [200, 202, 204, 301, 302, 400, 402, 404] {
1000            assert_eq!(macro_codes.contains(code), builder_codes.contains(code));
1001        }
1002    }
1003}
1004
1005#[cfg(test)]
1006mod integration_tests {
1007    use super::*;
1008
1009    #[test]
1010    fn test_expected_status_codes_real_world_patterns() {
1011        // REST API common patterns
1012        let rest_success = expected_status_codes!(200, 201, 204);
1013        assert!(rest_success.contains(200)); // GET success
1014        assert!(rest_success.contains(201)); // POST created
1015        assert!(rest_success.contains(204)); // DELETE success
1016
1017        // GraphQL typically uses 200 for everything
1018        let graphql = expected_status_codes!(200);
1019        assert!(graphql.contains(200));
1020        assert!(!graphql.contains(201));
1021
1022        // Health check endpoints
1023        let health = expected_status_codes!(200, 503);
1024        assert!(health.contains(200)); // Healthy
1025        assert!(health.contains(503)); // Unhealthy
1026
1027        // Authentication endpoints
1028        let auth = expected_status_codes!(200, 201, 401, 403);
1029        assert!(auth.contains(200)); // Login success
1030        assert!(auth.contains(401)); // Unauthorized
1031        assert!(auth.contains(403)); // Forbidden
1032    }
1033
1034    #[tokio::test]
1035    async fn test_expected_status_codes_with_api_call() {
1036        // This tests that the macro works correctly with actual API calls
1037        let client = ApiClient::builder().build().expect("should build client");
1038        let codes = expected_status_codes!(200 - 299, 404);
1039
1040        // Should compile and be usable
1041        let _call = client
1042            .get("/test")
1043            .expect("should create call")
1044            .with_expected_status_codes(codes);
1045    }
1046
1047    #[test]
1048    fn test_expected_status_codes_method_chaining() {
1049        let codes = expected_status_codes!(200)
1050            .add_single(201)
1051            .add_inclusive_range(300..=304);
1052
1053        assert!(codes.contains(200));
1054        assert!(codes.contains(201));
1055        assert!(codes.contains(302));
1056    }
1057
1058    #[test]
1059    fn test_expected_status_codes_vs_manual_creation() {
1060        // Macro version
1061        let macro_version = expected_status_codes!(200 - 204, 301, 400);
1062
1063        // Manual version
1064        let manual_version = ExpectedStatusCodes::from_inclusive_range(200..=204)
1065            .add_single(301)
1066            .add_single(400);
1067
1068        // Should behave identically
1069        for code in 100..600 {
1070            assert_eq!(
1071                macro_version.contains(code),
1072                manual_version.contains(code),
1073                "Mismatch for status code {code}"
1074            );
1075        }
1076    }
1077}