clawspec-core 0.1.2

Core library for generating OpenAPI specifications from tests
Documentation

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

Quick Start

Add to your Cargo.toml:

[dependencies]
clawspec = "0.1.0"

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

Basic Example with ApiClient

use clawspec::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")?
        .exchange()
        .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::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() 
        })
        .exchange()
        .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)
  • Status code validation with ranges and specific codes

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::{ApiClient, CallPath, CallQuery, CallHeaders, ParamValue};

let mut path = CallPath::from("/users/{id}/posts/{post_id}");
path.add_param("id", ParamValue::new(123));
path.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 response = client
    .get(path)?
    .with_query(query)
    .with_headers(headers)
    .exchange()
    .await?;

Status Code Validation

By default, requests expect status codes in the range 200-499. You can customize this:

use clawspec::{ApiClient, expected_status_codes};

// Accept specific codes
client.post("/users")?
    .with_expected_status_codes(expected_status_codes!(201, 202))
    .exchange()
    .await?;

// Accept ranges
client.get("/health")?
    .with_expected_status_codes(expected_status_codes!(200-299))
    .exchange()
    .await?;

// Complex patterns
client.delete("/users/123")?
    .with_expected_status_codes(expected_status_codes!(204, 404, 400-403))
    .exchange()
    .await?;

Schema Registration

use clawspec::{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);

Integration Examples

With Axum

For a complete working example, see the axum example implementation.

use axum::{Router, routing::get};
use clawspec::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().exchange().await {
            Ok(_) => Ok(HealthStatus::Healthy),
            Err(_) => Ok(HealthStatus::Unhealthy),
        }
    }
}

Configuration

TestServerConfig

Configure test server behavior:

use clawspec::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
  • 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: