clawspec-core

Crates.ioclawspec-core
lib.rsclawspec-core
version0.2.0
created_at2025-07-14 13:01:32.709704+00
updated_at2025-07-19 20:04:26.704851+00
descriptionCore library for generating OpenAPI specifications from tests
homepagehttps://github.com/ilaborie/clawspec
repositoryhttps://github.com/ilaborie/clawspec
max_upload_size
id1751670
size565,442
igor (ilaborie)

documentation

README

Clawspec

Crates.io Documentation CI License: MIT License: Apache 2.0

A Rust library for generating OpenAPI specifications from your HTTP client test code. Write tests, get documentation.

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:

[dependencies]
clawspec-core = "0.1.4"

[dev-dependencies]
tokio = { version = "1", features = ["full"] }

Basic Example with ApiClient

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_yaml::to_string(&spec)?;
    std::fs::write("openapi.yml", yaml)?;

    Ok(())
}

Test Server Example with TestClient

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: "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

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:

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

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:

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:

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.

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:

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 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.

License

This project is licensed under either of:

at your option.

Acknowledgments

Built with excellent crates from the Rust ecosystem:

Commit count: 0

cargo fmt