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//! ## Parameter Styles
108//!
109//! The library supports OpenAPI 3.1.0 parameter styles. Use [`ParamStyle`] for advanced serialization:
110//!
111//! ```rust
112//! use clawspec_core::{CallPath, CallQuery, ParamValue, ParamStyle};
113//!
114//! // Path: simple (default), label, matrix
115//! let path = CallPath::from("/users/{id}").add_param("id", ParamValue::new(123));
116//!
117//! // Query: form (default), spaceDelimited, pipeDelimited, deepObject
118//! let query = CallQuery::new()
119//! .add_param("tags", ParamValue::with_style(vec!["a", "b"], ParamStyle::PipeDelimited));
120//! ```
121//!
122//! See [Chapter 4: Advanced Parameters](crate::_tutorial::chapter_4) for detailed examples.
123//!
124//! ## Authentication
125//!
126//! Configure authentication at the client or per-request level:
127//!
128//! ```rust
129//! use clawspec_core::{ApiClient, Authentication};
130//!
131//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
132//! let mut client = ApiClient::builder()
133//! .with_host("api.example.com")
134//! .with_authentication(Authentication::Bearer("token".into()))
135//! .build()?;
136//!
137//! // Override per-request
138//! client.get("/admin")?.with_authentication(Authentication::Bearer("admin-token".into())).await?;
139//! # Ok(())
140//! # }
141//! ```
142//!
143//! Supported types: `Bearer`, `Basic`, `ApiKey`. See [Chapter 4](crate::_tutorial::chapter_4) for details.
144//!
145//! ## Status Code Validation
146//!
147//! By default, requests expect status codes in the range 200-499 (inclusive of 200, exclusive of 500).
148//! You can customize this behavior:
149//!
150//! ```rust
151//! use clawspec_core::{ApiClient, expected_status_codes};
152//!
153//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
154//! // Single codes
155//! client.post("/users")?
156//! .with_expected_status_codes(expected_status_codes!(201, 202))
157//! .await?;
158//!
159//! // Ranges
160//! client.get("/health")?
161//! .with_expected_status_codes(expected_status_codes!(200-299))
162//! .await?;
163//! # Ok(())
164//! # }
165//! ```
166//!
167//! ## Response Descriptions
168//!
169//! Add descriptive text to your OpenAPI responses for better documentation:
170//!
171//! ```rust
172//! use clawspec_core::ApiClient;
173//!
174//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
175//! // Set a description for the actual returned status code
176//! client.get("/users/{id}")?
177//! .with_response_description("User details if found, or error information")
178//! .await?;
179//!
180//! // The description applies to whatever status code is actually returned
181//! client.post("/users")?
182//! .with_response_description("User created successfully or validation error")
183//! .await?;
184//! # Ok(())
185//! # }
186//! ```
187//!
188//! ## Response Redaction
189//!
190//! *Requires the `redaction` feature.*
191//!
192//! When generating OpenAPI examples from real API responses, dynamic values like UUIDs,
193//! timestamps, and tokens make examples unstable across test runs. The redaction feature
194//! allows you to replace these dynamic values with stable, predictable ones in the generated
195//! OpenAPI specification while preserving the actual values for assertions.
196//!
197//! This is particularly useful for:
198//! - **Snapshot testing**: Generated OpenAPI files remain stable across runs
199//! - **Documentation**: Examples show consistent, readable placeholder values
200//! - **Security**: Sensitive values can be masked in documentation
201//!
202//! ### Basic Usage
203//!
204#![cfg_attr(feature = "redaction", doc = "```rust")]
205#![cfg_attr(not(feature = "redaction"), doc = "```rust,ignore")]
206//! use clawspec_core::ApiClient;
207//! # use serde::Deserialize;
208//! # use utoipa::ToSchema;
209//!
210//! #[derive(Deserialize, ToSchema)]
211//! struct User {
212//! id: String, // Dynamic UUID
213//! name: String,
214//! created_at: String, // Dynamic timestamp
215//! }
216//!
217//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
218//! let user: User = client
219//! .post("/users")?
220//! .json(&serde_json::json!({"name": "Alice"}))?
221//! .await?
222//! .as_json_redacted()
223//! .await?
224//! // Replace dynamic UUID with stable value
225//! .redact("/id", "00000000-0000-0000-0000-000000000001")?
226//! // Replace timestamp with stable value
227//! .redact("/created_at", "2024-01-01T00:00:00Z")?
228//! .finish()
229//! .await
230//! .value;
231//!
232//! // The actual user has real dynamic values for assertions
233//! assert!(!user.id.is_empty());
234//! // But the OpenAPI example shows the redacted stable values
235//! # Ok(())
236//! # }
237//! ```
238//!
239//! ### Redaction Operations
240//!
241//! The redaction builder supports two operations using [JSON Pointer (RFC 6901)](https://tools.ietf.org/html/rfc6901):
242//!
243//! - **`redact(pointer, redactor)`**: Replace a value at the given path with a stable value or transformation
244//! - **`redact_remove(pointer)`**: Remove a value entirely from the OpenAPI example
245//!
246#![cfg_attr(feature = "redaction", doc = "```rust")]
247#![cfg_attr(not(feature = "redaction"), doc = "```rust,ignore")]
248//! # use clawspec_core::ApiClient;
249//! # use serde::Deserialize;
250//! # use utoipa::ToSchema;
251//! # #[derive(Deserialize, ToSchema)]
252//! # struct Response { token: String, session_id: String, internal_ref: String }
253//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
254//! let response: Response = client
255//! .post("/auth/login")?
256//! .json(&serde_json::json!({"username": "test", "password": "secret"}))?
257//! .await?
258//! .as_json_redacted()
259//! .await?
260//! .redact("/token", "[REDACTED_TOKEN]")?
261//! .redact("/session_id", "session-00000000")?
262//! .redact_remove("/internal_ref")? // Remove internal field from docs
263//! .finish()
264//! .await
265//! .value;
266//! # Ok(())
267//! # }
268//! ```
269//!
270//! ### Path Syntax
271//!
272//! Paths are auto-detected based on their prefix:
273//! - `/...` → JSON Pointer (RFC 6901) - exact paths only
274//! - `$...` → JSONPath (RFC 9535) - supports wildcards
275//!
276//! ### JSON Pointer Syntax
277//!
278//! JSON Pointers use `/` as a path separator. Special characters are escaped:
279//! - `~0` represents `~`
280//! - `~1` represents `/`
281//!
282//! Examples:
283//! - `/id` - Top-level field named "id"
284//! - `/user/name` - Nested field "name" inside "user"
285//! - `/items/0/id` - First element's "id" in an array
286//! - `/foo~1bar` - Field named "foo/bar"
287//!
288//! ### JSONPath Wildcards
289//!
290//! For arrays, use JSONPath syntax (starting with `$`) to redact all elements:
291//!
292#![cfg_attr(feature = "redaction", doc = "```rust")]
293#![cfg_attr(not(feature = "redaction"), doc = "```rust,ignore")]
294//! # use clawspec_core::ApiClient;
295//! # use serde::Deserialize;
296//! # use utoipa::ToSchema;
297//! # #[derive(Deserialize, ToSchema)]
298//! # struct User { id: String, created_at: String }
299//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
300//! let users: Vec<User> = client
301//! .get("/users")?
302//! .await?
303//! .as_json_redacted()
304//! .await?
305//! .redact("$[*].id", "stable-uuid")? // All IDs in array
306//! .redact("$[*].created_at", "2024-01-01T00:00:00Z")? // All timestamps
307//! .finish()
308//! .await
309//! .value;
310//! # Ok(())
311//! # }
312//! ```
313//!
314//! ### Dynamic Transformations
315//!
316//! Pass a closure for dynamic redaction. The closure receives the concrete
317//! JSON Pointer path and current value:
318//!
319#![cfg_attr(feature = "redaction", doc = "```rust")]
320#![cfg_attr(not(feature = "redaction"), doc = "```rust,ignore")]
321//! # use clawspec_core::ApiClient;
322//! # use serde::Deserialize;
323//! # use serde_json::Value;
324//! # use utoipa::ToSchema;
325//! # #[derive(Deserialize, ToSchema)]
326//! # struct User { id: String }
327//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
328//! let users: Vec<User> = client
329//! .get("/users")?
330//! .await?
331//! .as_json_redacted()
332//! .await?
333//! // Create stable index-based IDs: user-0, user-1, user-2, ...
334//! .redact("$[*].id", |path: &str, _val: &Value| {
335//! let idx = path.split('/').nth(1).unwrap_or("0");
336//! serde_json::json!(format!("user-{idx}"))
337//! })?
338//! .finish()
339//! .await
340//! .value;
341//! # Ok(())
342//! # }
343//! ```
344//!
345#![cfg_attr(feature = "redaction", doc = "### Getting Both Values")]
346#![cfg_attr(feature = "redaction", doc = "")]
347#![cfg_attr(
348 feature = "redaction",
349 doc = "The [`RedactedResult`] returned by `finish()` contains both:"
350)]
351#![cfg_attr(
352 feature = "redaction",
353 doc = "- `value`: The actual deserialized response (with real dynamic values)"
354)]
355#![cfg_attr(
356 feature = "redaction",
357 doc = "- `redacted`: The JSON with redacted values (as stored in OpenAPI)"
358)]
359//!
360#![cfg_attr(feature = "redaction", doc = "```rust")]
361#![cfg_attr(not(feature = "redaction"), doc = "```rust,ignore")]
362//! # use clawspec_core::ApiClient;
363//! # use serde::Deserialize;
364//! # use utoipa::ToSchema;
365//! # #[derive(Deserialize, ToSchema)]
366//! # struct User { id: String }
367//! # async fn example(client: &mut ApiClient) -> Result<(), Box<dyn std::error::Error>> {
368//! let result = client
369//! .get("/users/123")?
370//! .await?
371//! .as_json_redacted::<User>()
372//! .await?
373//! .redact("/id", "user-00000000")?
374//! .finish()
375//! .await;
376//!
377//! // Use actual value for test assertions
378//! let user = result.value;
379//! assert!(!user.id.is_empty());
380//!
381//! // Access redacted JSON if needed
382//! let redacted_json = result.redacted;
383//! assert_eq!(redacted_json["id"], "user-00000000");
384//! # Ok(())
385//! # }
386//! ```
387//!
388//! ## Schema Registration
389//!
390//! Schemas are **automatically captured** when using `.json()` and `.as_json()` methods.
391//! For nested schemas or error types, use `register_schemas!`:
392//!
393//! ```rust
394//! use clawspec_core::{ApiClient, register_schemas};
395//! # use serde::{Serialize, Deserialize};
396//! # use utoipa::ToSchema;
397//! # #[derive(Serialize, Deserialize, ToSchema)]
398//! # struct Address { street: String }
399//! # #[derive(Deserialize, ToSchema)]
400//! # struct ErrorResponse { code: String }
401//!
402//! # async fn example(client: &mut ApiClient) {
403//! register_schemas!(client, Address, ErrorResponse).await;
404//! # }
405//! ```
406//!
407//! **Note**: Nested schemas may not be fully resolved automatically. Register them explicitly
408//! if they're missing from your OpenAPI output.
409//!
410//! ## Error Handling
411//!
412//! The library provides two main error types:
413//! - [`ApiClientError`] - HTTP client errors (network, parsing, validation)
414//! - [`TestAppError`](test_client::TestAppError) - Test server lifecycle errors
415//!
416//! ## YAML Serialization
417//!
418//! *Requires the `yaml` feature.*
419//!
420//! The library provides YAML serialization support using [serde-saphyr](https://github.com/saphyr-rs/serde_saphyr),
421//! the modern replacement for the deprecated `serde_yaml` crate.
422//!
423#![cfg_attr(feature = "yaml", doc = "```rust")]
424#![cfg_attr(not(feature = "yaml"), doc = "```rust,ignore")]
425//! use clawspec_core::{ApiClient, ToYaml};
426//!
427//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
428//! let mut client = ApiClient::builder()
429//! .with_host("api.example.com")
430//! .build()?;
431//!
432//! // ... make API calls ...
433//!
434//! let spec = client.collected_openapi().await;
435//! let yaml = spec.to_yaml()?;
436//!
437//! std::fs::write("openapi.yml", yaml)?;
438//! # Ok(())
439//! # }
440//! ```
441//!
442//! ## See Also
443//!
444//! - [`ApiClient`] - HTTP client with OpenAPI collection
445//! - [`ApiCall`] - Request builder with parameter support
446//! - [`test_client`] - Test server integration module
447//! - [`ExpectedStatusCodes`] - Status code validation
448#![cfg_attr(
449 feature = "redaction",
450 doc = "- [`RedactionBuilder`] - Builder for redacting response values in OpenAPI examples"
451)]
452#![cfg_attr(
453 feature = "redaction",
454 doc = "- [`RedactedResult`] - Result containing both actual and redacted values"
455)]
456#![cfg_attr(
457 feature = "redaction",
458 doc = "- [`RedactOptions`] - Options for configuring redaction behavior"
459)]
460#![cfg_attr(
461 feature = "redaction",
462 doc = "- [`Redactor`] - Trait for types that can be used to redact values"
463)]
464#![cfg_attr(
465 feature = "redaction",
466 doc = "- [`redact_value`] - Entry point for redacting arbitrary JSON values"
467)]
468#![cfg_attr(
469 feature = "redaction",
470 doc = "- [`ValueRedactionBuilder`] - Builder for redacting arbitrary JSON values (e.g., OpenAPI specs)"
471)]
472#![cfg_attr(
473 feature = "yaml",
474 doc = "- [`ToYaml`] - Extension trait for YAML serialization"
475)]
476#![cfg_attr(
477 feature = "yaml",
478 doc = "- [`YamlError`] - Error type for YAML serialization"
479)]
480//!
481//! ## Re-exports
482//!
483//! All commonly used types are re-exported from the crate root for convenience.
484
485// TODO: Add comprehensive unit tests for all modules - https://github.com/ilaborie/clawspec/issues/30
486
487pub mod _tutorial;
488
489mod client;
490
491pub mod split;
492
493#[cfg(feature = "yaml")]
494#[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
495mod yaml;
496
497pub mod test_client;
498
499// Public API - only expose user-facing types and functions
500pub use self::client::{
501 ApiCall, ApiClient, ApiClientBuilder, ApiClientError, ApiKeyLocation, Authentication,
502 AuthenticationError, CallBody, CallCookies, CallHeaders, CallPath, CallQuery, CallResult,
503 ExpectedStatusCodes, OAuth2Flow, OAuth2Flows, OAuth2ImplicitFlow, ParamStyle, ParamValue,
504 ParameterValue, RawBody, RawResult, SecureString, SecurityRequirement, SecurityScheme,
505};
506
507// Re-export external types so users don't need to add these crates to their Cargo.toml.
508//
509// With these re-exports, users can write:
510// use clawspec_core::{ApiClient, OpenApi, ToSchema, StatusCode};
511// Instead of:
512// use clawspec_core::ApiClient;
513// use utoipa::openapi::OpenApi;
514// use utoipa::ToSchema;
515// use http::StatusCode;
516
517/// OpenAPI types re-exported from utoipa for convenience.
518pub use utoipa::openapi::{Info, InfoBuilder, OpenApi, Paths, Server, ServerBuilder};
519
520/// The `ToSchema` derive macro for generating OpenAPI schemas.
521/// Types used in JSON request/response bodies should derive this trait.
522pub use utoipa::ToSchema;
523
524/// HTTP status codes re-exported from the `http` crate.
525pub use http::StatusCode;
526
527#[cfg(feature = "redaction")]
528pub use self::client::{
529 RedactOptions, RedactedResult, RedactionBuilder, Redactor, ValueRedactionBuilder, redact_value,
530};
531
532#[cfg(feature = "oauth2")]
533pub use self::client::{OAuth2Config, OAuth2ConfigBuilder, OAuth2Error, OAuth2Token};
534
535#[cfg(feature = "yaml")]
536#[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
537pub use self::yaml::{ToYaml, YamlError};
538
539// Convenience macro re-exports are handled by the macro_rules! definitions below
540
541/// Creates an [`ExpectedStatusCodes`] instance with the specified status codes and ranges.
542///
543/// This macro provides a convenient syntax for defining expected HTTP status codes
544/// with support for individual codes, inclusive ranges, and exclusive ranges.
545///
546/// # Syntax
547///
548/// - Single codes: `200`, `201`, `404`
549/// - Inclusive ranges: `200-299` (includes both endpoints)
550/// - Exclusive ranges: `200..300` (excludes 300)
551/// - Mixed: `200, 201-204, 400..500`
552///
553/// # Examples
554///
555/// ```rust
556/// use clawspec_core::expected_status_codes;
557///
558/// // Single status codes
559/// let codes = expected_status_codes!(200, 201, 204);
560///
561/// // Ranges
562/// let success_codes = expected_status_codes!(200-299);
563/// let client_errors = expected_status_codes!(400..500);
564///
565/// // Mixed
566/// let mixed = expected_status_codes!(200-204, 301, 302, 400-404);
567/// ```
568#[macro_export]
569macro_rules! expected_status_codes {
570 // Empty case
571 () => {
572 $crate::ExpectedStatusCodes::default()
573 };
574
575 // Single element
576 ($single:literal) => {
577 $crate::ExpectedStatusCodes::from_single($single)
578 };
579
580 // Single range (inclusive)
581 ($start:literal - $end:literal) => {
582 $crate::ExpectedStatusCodes::from_inclusive_range($start..=$end)
583 };
584
585 // Single range (exclusive)
586 ($start:literal .. $end:literal) => {
587 $crate::ExpectedStatusCodes::from_exclusive_range($start..$end)
588 };
589
590 // Multiple elements - single code followed by more
591 ($first:literal, $($rest:tt)*) => {{
592 #[allow(unused_mut)]
593 let mut codes = $crate::ExpectedStatusCodes::from_single($first);
594 $crate::expected_status_codes!(@accumulate codes, $($rest)*);
595 codes
596 }};
597
598 // Multiple elements - inclusive range followed by more
599 ($start:literal - $end:literal, $($rest:tt)*) => {{
600 #[allow(unused_mut)]
601 let mut codes = $crate::ExpectedStatusCodes::from_inclusive_range($start..=$end);
602 $crate::expected_status_codes!(@accumulate codes, $($rest)*);
603 codes
604 }};
605
606 // Multiple elements - exclusive range followed by more
607 ($start:literal .. $end:literal, $($rest:tt)*) => {{
608 #[allow(unused_mut)]
609 let mut codes = $crate::ExpectedStatusCodes::from_exclusive_range($start..$end);
610 $crate::expected_status_codes!(@accumulate codes, $($rest)*);
611 codes
612 }};
613
614 // Internal accumulator - empty (base case for trailing commas)
615 (@accumulate $codes:ident,) => {
616 // Do nothing for trailing commas
617 };
618
619 // Internal accumulator - single code
620 (@accumulate $codes:ident, $single:literal) => {
621 $codes = $codes.add_single($single);
622 };
623
624 // Internal accumulator - single code followed by more
625 (@accumulate $codes:ident, $single:literal, $($rest:tt)*) => {
626 $codes = $codes.add_single($single);
627 $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
628 };
629
630 // Internal accumulator - inclusive range
631 (@accumulate $codes:ident, $start:literal - $end:literal) => {
632 $codes = $codes.add_inclusive_range($start..=$end);
633 };
634
635 // Internal accumulator - inclusive range followed by more
636 (@accumulate $codes:ident, $start:literal - $end:literal, $($rest:tt)*) => {
637 $codes = $codes.add_inclusive_range($start..=$end);
638 $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
639 };
640
641 // Internal accumulator - exclusive range
642 (@accumulate $codes:ident, $start:literal .. $end:literal) => {
643 $codes = $codes.add_exclusive_range($start..$end);
644 };
645
646 // Internal accumulator - exclusive range followed by more
647 (@accumulate $codes:ident, $start:literal .. $end:literal, $($rest:tt)*) => {
648 $codes = $codes.add_exclusive_range($start..$end);
649 $crate::expected_status_codes!(@accumulate $codes, $($rest)*);
650 };
651
652 // Internal accumulator - empty (catch all for trailing cases)
653 (@accumulate $codes:ident) => {
654 // Base case - do nothing
655 };
656}
657
658/// Registers multiple schema types with the ApiClient for OpenAPI documentation.
659///
660/// This macro simplifies the process of registering multiple types that implement
661/// [`utoipa::ToSchema`] with an [`ApiClient`] instance.
662///
663/// # When to Use
664///
665/// - **Nested Schemas**: When your JSON types contain nested structures that need to be fully resolved
666/// - **Error Types**: To ensure error response schemas are included in the OpenAPI specification
667/// - **Complex Dependencies**: When automatic schema capture doesn't include all referenced types
668///
669/// # Automatic vs Manual Registration
670///
671/// Most JSON request/response schemas are captured automatically when using `.json()` and `.as_json()` methods.
672/// Use this macro when you need to ensure complete schema coverage, especially for nested types.
673///
674/// # Examples
675///
676/// ## Basic Usage
677///
678/// ```rust
679/// use clawspec_core::{ApiClient, register_schemas};
680/// use serde::{Serialize, Deserialize};
681/// use utoipa::ToSchema;
682///
683/// #[derive(Serialize, Deserialize, ToSchema)]
684/// struct User {
685/// id: u64,
686/// name: String,
687/// }
688///
689/// #[derive(Serialize, Deserialize, ToSchema)]
690/// struct Post {
691/// id: u64,
692/// title: String,
693/// author_id: u64,
694/// }
695///
696/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
697/// let mut client = ApiClient::builder().build()?;
698///
699/// // Register multiple schemas at once
700/// register_schemas!(client, User, Post).await;
701/// # Ok(())
702/// # }
703/// ```
704///
705/// ## Nested Schemas
706///
707/// ```rust
708/// use clawspec_core::{ApiClient, register_schemas};
709/// use serde::{Serialize, Deserialize};
710/// use utoipa::ToSchema;
711///
712/// #[derive(Serialize, Deserialize, ToSchema)]
713/// struct Address {
714/// street: String,
715/// city: String,
716/// }
717///
718/// #[derive(Serialize, Deserialize, ToSchema)]
719/// struct User {
720/// id: u64,
721/// name: String,
722/// address: Address, // Nested schema
723/// }
724///
725/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
726/// let mut client = ApiClient::builder().build()?;
727///
728/// // Register both main and nested schemas for complete OpenAPI generation
729/// register_schemas!(client, User, Address).await;
730/// # Ok(())
731/// # }
732/// ```
733#[macro_export]
734macro_rules! register_schemas {
735 ($client:expr, $($schema:ty),+ $(,)?) => {
736 async {
737 $(
738 $client.register_schema::<$schema>().await;
739 )+
740 }
741 };
742}
743
744#[cfg(test)]
745mod macro_tests {
746 use super::*;
747
748 #[test]
749 fn test_expected_status_codes_single() {
750 let codes = expected_status_codes!(200);
751 assert!(codes.contains(200));
752 assert!(!codes.contains(201));
753 }
754
755 #[test]
756 fn test_expected_status_codes_multiple_single() {
757 let codes = expected_status_codes!(200, 201, 204);
758 assert!(codes.contains(200));
759 assert!(codes.contains(201));
760 assert!(codes.contains(204));
761 assert!(!codes.contains(202));
762 }
763
764 #[test]
765 fn test_expected_status_codes_range() {
766 let codes = expected_status_codes!(200 - 204);
767 assert!(codes.contains(200));
768 assert!(codes.contains(202));
769 assert!(codes.contains(204));
770 assert!(!codes.contains(205));
771 }
772
773 #[test]
774 fn test_expected_status_codes_mixed() {
775 let codes = expected_status_codes!(200, 201 - 204, 301, 400 - 404);
776 assert!(codes.contains(200));
777 assert!(codes.contains(202));
778 assert!(codes.contains(301));
779 assert!(codes.contains(402));
780 assert!(!codes.contains(305));
781 }
782
783 #[test]
784 fn test_expected_status_codes_trailing_comma() {
785 let codes = expected_status_codes!(200, 201,);
786 assert!(codes.contains(200));
787 assert!(codes.contains(201));
788 }
789
790 #[test]
791 fn test_expected_status_codes_range_trailing_comma() {
792 let codes = expected_status_codes!(200 - 204,);
793 assert!(codes.contains(202));
794 }
795
796 #[test]
797 fn test_expected_status_codes_five_elements() {
798 let codes = expected_status_codes!(200, 201, 202, 203, 204);
799 assert!(codes.contains(200));
800 assert!(codes.contains(201));
801 assert!(codes.contains(202));
802 assert!(codes.contains(203));
803 assert!(codes.contains(204));
804 }
805
806 #[test]
807 fn test_expected_status_codes_eight_elements() {
808 let codes = expected_status_codes!(200, 201, 202, 203, 204, 205, 206, 207);
809 assert!(codes.contains(200));
810 assert!(codes.contains(204));
811 assert!(codes.contains(207));
812 }
813
814 #[test]
815 fn test_expected_status_codes_multiple_ranges() {
816 let codes = expected_status_codes!(200 - 204, 300 - 304, 400 - 404);
817 assert!(codes.contains(202));
818 assert!(codes.contains(302));
819 assert!(codes.contains(402));
820 assert!(!codes.contains(205));
821 assert!(!codes.contains(305));
822 }
823
824 #[test]
825 fn test_expected_status_codes_edge_cases() {
826 // Empty should work
827 let _codes = expected_status_codes!();
828
829 // Single range should work
830 let codes = expected_status_codes!(200 - 299);
831 assert!(codes.contains(250));
832 }
833
834 #[test]
835 fn test_expected_status_codes_common_patterns() {
836 // Success codes
837 let success = expected_status_codes!(200 - 299);
838 assert!(success.contains(200));
839 assert!(success.contains(201));
840 assert!(success.contains(204));
841
842 // Client errors
843 let client_errors = expected_status_codes!(400 - 499);
844 assert!(client_errors.contains(400));
845 assert!(client_errors.contains(404));
846 assert!(client_errors.contains(422));
847
848 // Specific success codes
849 let specific = expected_status_codes!(200, 201, 204);
850 assert!(specific.contains(200));
851 assert!(!specific.contains(202));
852 }
853
854 #[test]
855 fn test_expected_status_codes_builder_alternative() {
856 // Using macro
857 let macro_codes = expected_status_codes!(200 - 204, 301, 302, 400 - 404);
858
859 // Using builder (should be equivalent)
860 let builder_codes = ExpectedStatusCodes::default()
861 .add_inclusive_range(200..=204)
862 .add_single(301)
863 .add_single(302)
864 .add_inclusive_range(400..=404);
865
866 // Both should have same results
867 for code in [200, 202, 204, 301, 302, 400, 402, 404] {
868 assert_eq!(macro_codes.contains(code), builder_codes.contains(code));
869 }
870 }
871}
872
873#[cfg(test)]
874mod integration_tests {
875 use super::*;
876
877 #[test]
878 fn test_expected_status_codes_real_world_patterns() {
879 // REST API common patterns
880 let rest_success = expected_status_codes!(200, 201, 204);
881 assert!(rest_success.contains(200)); // GET success
882 assert!(rest_success.contains(201)); // POST created
883 assert!(rest_success.contains(204)); // DELETE success
884
885 // GraphQL typically uses 200 for everything
886 let graphql = expected_status_codes!(200);
887 assert!(graphql.contains(200));
888 assert!(!graphql.contains(201));
889
890 // Health check endpoints
891 let health = expected_status_codes!(200, 503);
892 assert!(health.contains(200)); // Healthy
893 assert!(health.contains(503)); // Unhealthy
894
895 // Authentication endpoints
896 let auth = expected_status_codes!(200, 201, 401, 403);
897 assert!(auth.contains(200)); // Login success
898 assert!(auth.contains(401)); // Unauthorized
899 assert!(auth.contains(403)); // Forbidden
900 }
901
902 #[tokio::test]
903 async fn test_expected_status_codes_with_api_call() {
904 // This tests that the macro works correctly with actual API calls
905 let client = ApiClient::builder().build().expect("should build client");
906 let codes = expected_status_codes!(200 - 299, 404);
907
908 // Should compile and be usable
909 let _call = client
910 .get("/test")
911 .expect("should create call")
912 .with_expected_status_codes(codes);
913 }
914
915 #[test]
916 fn test_expected_status_codes_method_chaining() {
917 let codes = expected_status_codes!(200)
918 .add_single(201)
919 .add_inclusive_range(300..=304);
920
921 assert!(codes.contains(200));
922 assert!(codes.contains(201));
923 assert!(codes.contains(302));
924 }
925
926 #[test]
927 fn test_expected_status_codes_vs_manual_creation() {
928 // Macro version
929 let macro_version = expected_status_codes!(200 - 204, 301, 400);
930
931 // Manual version
932 let manual_version = ExpectedStatusCodes::from_inclusive_range(200..=204)
933 .add_single(301)
934 .add_single(400);
935
936 // Should behave identically
937 for code in 100..600 {
938 assert_eq!(
939 macro_version.contains(code),
940 manual_version.contains(code),
941 "Mismatch for status code {code}"
942 );
943 }
944 }
945}