# Clawspec
[](https://crates.io/crates/clawspec-core) [](https://docs.rs/clawspec-core) [](https://github.com/ilaborie/clawspec/actions) [](https://opensource.org/licenses/MIT) [](https://opensource.org/licenses/Apache-2.0)
A Rust library for generating [OpenAPI] specifications from your HTTP client test code. Write tests, get documentation.
[OpenAPI]: https://www.openapis.org/
## Overview
Clawspec automatically generates OpenAPI documentation by observing HTTP client interactions in your tests. Instead of maintaining separate API documentation, your tests become the source of truth.
### Key Features
- ๐งช **Test-Driven Documentation** - Generate specs from integration tests
- ๐ **Type Safety** - Leverage Rust's type system for accurate schemas
- ๐ **Zero Runtime Overhead** - Documentation generation only runs during tests
- ๐ ๏ธ **Framework Agnostic** - Works with any async HTTP server
- ๐ **OpenAPI 3.1 Compliant** - Generate standard-compliant specifications
- ๐ **Authentication Support** - Bearer, Basic, and API Key authentication
- ๐ช **Cookie Support** - Full cookie parameter handling and documentation
- ๐ **Parameter Styles** - Complete OpenAPI 3.1.0 parameter style support
## Quick Start
Add to your `Cargo.toml`:
```toml
[dependencies]
clawspec-core = "0.1.4"
[dev-dependencies]
tokio = { version = "1", features = ["full"] }
```
### Basic Example with ApiClient
```rust
use clawspec_core::ApiClient;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Serialize, Deserialize, ToSchema)]
struct User {
id: u64,
name: String,
email: String,
}
#[tokio::test]
async fn test_user_api() -> Result<(), Box<dyn std::error::Error>> {
// Create API client
let mut client = ApiClient::builder()
.with_host("api.example.com")
.build()?;
// Make requests - schemas are automatically captured
let user: User = client
.get("/users/123")?
.await?
.as_json()
.await?;
// Generate OpenAPI specification
let spec = client.collected_openapi().await;
let yaml = serde_saphyr::to_string(&spec)?;
std::fs::write("openapi.yml", yaml)?;
Ok(())
}
```
### Test Server Example with TestClient
For testing complete web applications. See the [axum example](https://github.com/ilaborie/clawspec/tree/main/examples/axum-example) for a full working implementation:
```rust
use clawspec_core::test_client::{TestClient, TestServer};
use std::net::TcpListener;
#[derive(Debug)]
struct MyServer;
impl TestServer for MyServer {
type Error = std::io::Error;
async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
// Start your web server with the provided listener
// Works with Axum, Warp, Actix-web, etc.
todo!("Launch your server")
}
}
#[tokio::test]
async fn test_with_server() -> Result<(), Box<dyn std::error::Error>> {
// Start test server and client
let mut client = TestClient::start(MyServer).await?;
// Test your API
let response = client
.post("/users")?
.json(&User {
id: 1,
name: "Alice".into(),
email: "alice@example.com".into()
})
.await?;
assert_eq!(response.status_code(), 201);
// Write OpenAPI specification
client.write_openapi("docs/api.yml").await?;
Ok(())
}
```
## Core Concepts
### ApiClient
The main HTTP client that captures request/response schemas:
- **Builder pattern** for configuration
- **Automatic schema extraction** from Rust types
- **Flexible parameter handling** (path, query, headers, cookies)
- **Authentication support** (Bearer, Basic, API Key)
- **Status code validation** with ranges and specific codes
- **OpenAPI 3.1.0 parameter styles** support
### TestClient
A test-focused wrapper providing:
- **Automatic server lifecycle management**
- **Health checking with retries**
- **Integrated OpenAPI generation**
- **Framework-agnostic design**
## Advanced Usage
### Parameter Handling
```rust
use clawspec_core::{ApiClient, CallPath, CallQuery, CallHeaders, CallCookies, ParamValue};
let path = CallPath::from("/users/{id}/posts/{post_id}")
.add_param("id", ParamValue::new(123))
.add_param("post_id", ParamValue::new(456));
let query = CallQuery::new()
.add_param("page", ParamValue::new(1))
.add_param("limit", ParamValue::new(20));
let headers = CallHeaders::new()
.add_header("Authorization", "Bearer token")
.add_header("X-Request-ID", "abc123");
let cookies = CallCookies::new()
.add_cookie("session_id", "abc123")
.add_cookie("user_id", 456);
let response = client
.get(path)?
.with_query(query)
.with_headers(headers)
.with_cookies(cookies)
.exchange()
.await?;
```
### Status Code Validation
By default, requests expect status codes in the range 200-499. You can customize this:
```rust
use clawspec_core::{ApiClient, expected_status_codes};
// Accept specific codes
client.post("/users")?
.with_expected_status_codes(expected_status_codes!(201, 202))
.await?;
// Accept ranges
client.get("/health")?
.with_expected_status_codes(expected_status_codes!(200-299))
.await?;
// Complex patterns
client.delete("/users/123")?
.with_expected_status_codes(expected_status_codes!(204, 404, 400-403))
.await?;
```
### Schema Registration
```rust
use clawspec_core::{ApiClient, register_schemas};
#[derive(serde::Deserialize, utoipa::ToSchema)]
struct CreateUserRequest {
name: String,
email: String,
}
#[derive(serde::Deserialize, utoipa::ToSchema)]
struct ErrorResponse {
code: String,
message: String,
}
// Register schemas for better documentation
register_schemas!(client, CreateUserRequest, ErrorResponse);
```
### Authentication
Clawspec supports various authentication methods with enhanced security features:
```rust
use clawspec_core::{ApiClient, Authentication};
// Bearer token authentication
let client = ApiClient::builder()
.with_host("api.example.com")
.with_authentication(Authentication::Bearer("my-api-token".into()))
.build()?;
// Basic authentication
let client = ApiClient::builder()
.with_authentication(Authentication::Basic {
username: "user".to_string(),
password: "pass".into(),
})
.build()?;
// API key authentication
let client = ApiClient::builder()
.with_authentication(Authentication::ApiKey {
header_name: "X-API-Key".to_string(),
key: "secret-key".into(),
})
.build()?;
// Per-request authentication override
let response = client
.get("/admin/users")?
.with_authentication(Authentication::Bearer("admin-token".into()))
.await?;
// Disable authentication for public endpoints
let public_data = client
.get("/public/health")?
.with_authentication_none()
.await?;
```
#### Security Features
- **Memory Protection**: Sensitive credentials are automatically cleared from memory when no longer needed
- **Debug Safety**: Authentication data is redacted in debug output to prevent accidental logging
- **Display Masking**: Credentials are masked when displayed (e.g., `Bearer abcd...789`)
- **Granular Error Handling**: Detailed authentication error types for better debugging
#### Security Best Practices
- Store credentials securely using environment variables or secret management tools
- Rotate tokens regularly
- Use HTTPS for all authenticated requests
- Never log authentication headers or credentials
### Cookie Support
Handle cookie parameters with full OpenAPI documentation:
```rust
use clawspec_core::{ApiClient, CallCookies};
let cookies = CallCookies::new()
.add_cookie("session_id", "abc123")
.add_cookie("user_preferences", vec!["dark_mode", "notifications"])
.add_cookie("is_admin", true);
let response = client
.get("/dashboard")?
.with_cookies(cookies)
.await?;
```
## Integration Examples
### With Axum
For a complete working example, see the [axum example implementation](https://github.com/ilaborie/clawspec/tree/main/examples/axum-example).
```rust
use axum::{Router, routing::get};
use clawspec_core::test_client::{TestClient, TestServer, HealthStatus};
struct AxumTestServer {
router: Router,
}
impl TestServer for AxumTestServer {
type Error = std::io::Error;
async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
listener.set_nonblocking(true)?;
let listener = tokio::net::TcpListener::from_std(listener)?;
axum::serve(listener, self.router.clone())
.await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
}
async fn is_healthy(&self, client: &mut ApiClient) -> Result<HealthStatus, Self::Error> {
match client.get("/health").unwrap().await {
Ok(_) => Ok(HealthStatus::Healthy),
Err(_) => Ok(HealthStatus::Unhealthy),
}
}
}
```
## Configuration
### TestServerConfig
Configure test server behavior:
```rust
use clawspec_core::test_client::TestServerConfig;
use std::time::Duration;
let config = TestServerConfig {
min_backoff_delay: Duration::from_millis(10),
max_backoff_delay: Duration::from_secs(1),
backoff_jitter: true,
max_retry_attempts: 10,
..Default::default()
};
```
## Error Handling
The library provides comprehensive error types:
- `ApiClientError` - HTTP client errors (includes authentication errors)
- `AuthenticationError` - Granular authentication failure details
- `TestAppError` - Test server errors
All errors implement standard error traits and provide detailed context for debugging.
## Best Practices
1. **Write focused tests** - Each test should document specific endpoints
2. **Use descriptive types** - Well-named structs generate better documentation
3. **Register schemas** - Explicitly register types for complete documentation
4. **Validate status codes** - Be explicit about expected responses
5. **Organize tests** - Group related endpoint tests together
## Contributing
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
**Note**: This project has been developed with assistance from [Claude Code](https://claude.ai/code). All AI-generated code has been carefully reviewed, tested, and validated to ensure quality, security, and adherence to Rust best practices.
## License
This project is licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
- MIT license ([LICENSE-MIT](LICENSE-MIT))
at your option.
## Acknowledgments
Built with excellent crates from the Rust ecosystem:
- [utoipa](https://github.com/juhaku/utoipa) - OpenAPI schema generation
- [reqwest](https://github.com/seanmonstar/reqwest) - HTTP client
- [tokio](https://github.com/tokio-rs/tokio) - Async runtime