A Rust library for generating OpenAPI specifications from your HTTP client test code. Write tests, get documentation.
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.
- 🧪 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
Add to your Cargo.toml:
[dependencies]
clawspec-core = "0.1.4"
[dev-dependencies]
tokio = { version = "1", features = ["full"] }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(())
}For testing complete web applications. See the axum example for a full working implementation:
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: "[email protected]".into()
})
.await?;
assert_eq!(response.status_code(), 201);
// Write OpenAPI specification
client.write_openapi("docs/api.yml").await?;
Ok(())
}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
A test-focused wrapper providing:
- Automatic server lifecycle management
- Health checking with retries
- Integrated OpenAPI generation
- Framework-agnostic design
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?;By default, requests expect status codes in the range 200-499. You can customize this:
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?;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);Clawspec supports various authentication methods with enhanced security features:
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?;- 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
- 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
Handle cookie parameters with full OpenAPI documentation:
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?;For a complete working example, see the axum example implementation.
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),
}
}
}Configure test server behavior:
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()
};The library provides comprehensive error types:
ApiClientError- HTTP client errors (includes authentication errors)AuthenticationError- Granular authentication failure detailsTestAppError- Test server errors
All errors implement standard error traits and provide detailed context for debugging.
- Write focused tests - Each test should document specific endpoints
- Use descriptive types - Well-named structs generate better documentation
- Register schemas - Explicitly register types for complete documentation
- Validate status codes - Be explicit about expected responses
- Organize tests - Group related endpoint tests together
We welcome contributions! Please see our Contributing Guide for details.
Note: This project has been developed with assistance from Claude Code. All AI-generated code has been carefully reviewed, tested, and validated to ensure quality, security, and adherence to Rust best practices.
This project is licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Built with excellent crates from the Rust ecosystem: