Crate clawspec_core

Crate clawspec_core 

Source
Expand description

§Clawspec Core

Generate OpenAPI specifications from your HTTP client test code.

This crate provides two main ways to generate OpenAPI documentation:

  • ApiClient - Direct HTTP client for fine-grained control
  • TestClient - Test server integration with automatic lifecycle management

New to Clawspec? Start with the Tutorial for a step-by-step guide.

§Quick Start

§Using ApiClient directly

use clawspec_core::ApiClient;

let mut client = ApiClient::builder()
    .with_host("api.example.com")
    .build()?;

// Make requests - schemas are captured automatically  
let user: User = client
    .get("/users/123")?
    .await?  // ← Direct await using IntoFuture
    .as_json()  // ← Important: Must consume result for OpenAPI generation!
    .await?;

// Generate OpenAPI specification
let spec = client.collected_openapi().await;

§Using TestClient with a test server

For a complete working example, see the axum example.

use clawspec_core::test_client::{TestClient, TestServer};
use std::net::TcpListener;

#[tokio::test]
async fn test_api() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = TestClient::start(MyServer).await?;
     
    // Test your API
    let response = client.get("/users")?.await?.as_json::<serde_json::Value>().await?;
     
    // Write OpenAPI spec
    client.write_openapi("api.yml").await?;
    Ok(())
}

§Working with Parameters

use clawspec_core::{ApiClient, CallPath, CallQuery, CallHeaders, CallCookies, ParamValue, ParamStyle};

// Path parameters  
let path = CallPath::from("/users/{id}")
    .add_param("id", ParamValue::new(123));

// Query parameters
let query = CallQuery::new()
    .add_param("page", ParamValue::new(1))
    .add_param("limit", ParamValue::new(10));

// Headers
let headers = CallHeaders::new()
    .add_header("Authorization", "Bearer token");

// Cookies
let cookies = CallCookies::new()
    .add_cookie("session_id", "abc123")
    .add_cookie("user_id", 456);

// Direct await with parameters:
let response = client
    .get(path)?
    .with_query(query)
    .with_headers(headers)
    .with_cookies(cookies)
    .await?;  // Direct await using IntoFuture

§OpenAPI 3.1.0 Parameter Styles

This library supports all OpenAPI 3.1.0 parameter styles for different parameter types:

§Path Parameters

use clawspec_core::{CallPath, ParamValue, ParamStyle};

// Simple style (default): /users/123
let path = CallPath::from("/users/{id}")
    .add_param("id", ParamValue::new(123));

// Label style: /users/.123
let path = CallPath::from("/users/{id}")
    .add_param("id", ParamValue::with_style(123, ParamStyle::Label));

// Matrix style: /users/;id=123
let path = CallPath::from("/users/{id}")
    .add_param("id", ParamValue::with_style(123, ParamStyle::Matrix));

// Arrays with different styles
let tags = vec!["rust", "web", "api"];

// Simple: /search/rust,web,api
let path = CallPath::from("/search/{tags}")
    .add_param("tags", ParamValue::with_style(tags.clone(), ParamStyle::Simple));

// Label: /search/.rust,web,api
let path = CallPath::from("/search/{tags}")
    .add_param("tags", ParamValue::with_style(tags.clone(), ParamStyle::Label));

// Matrix: /search/;tags=rust,web,api
let path = CallPath::from("/search/{tags}")
    .add_param("tags", ParamValue::with_style(tags, ParamStyle::Matrix));

§Query Parameters

use clawspec_core::{CallQuery, ParamValue, ParamStyle};

let tags = vec!["rust", "web", "api"];

// Form style (default): ?tags=rust&tags=web&tags=api
let query = CallQuery::new()
    .add_param("tags", ParamValue::new(tags.clone()));

// Space delimited: ?tags=rust%20web%20api
let query = CallQuery::new()
    .add_param("tags", ParamValue::with_style(tags.clone(), ParamStyle::SpaceDelimited));

// Pipe delimited: ?tags=rust|web|api
let query = CallQuery::new()
    .add_param("tags", ParamValue::with_style(tags, ParamStyle::PipeDelimited));

// Deep object style: ?user[name]=john&user[age]=30
let user_data = serde_json::json!({"name": "john", "age": 30});
let query = CallQuery::new()
    .add_param("user", ParamValue::with_style(user_data, ParamStyle::DeepObject));
use clawspec_core::{CallCookies, ParamValue};

// Simple cookie values
let cookies = CallCookies::new()
    .add_cookie("session_id", "abc123")
    .add_cookie("user_id", 456)
    .add_cookie("is_admin", true);

// Array values in cookies (comma-separated)
let cookies = CallCookies::new()
    .add_cookie("preferences", vec!["dark_mode", "notifications"])
    .add_cookie("selected_tags", vec!["rust", "web", "api"]);

// Custom types with automatic serialization
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
struct UserId(u64);

let cookies = CallCookies::new()
    .add_cookie("user", UserId(12345));

§Authentication

The library supports various authentication methods that can be configured at the client level or overridden for individual requests.

§Client-Level Authentication

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_host("api.example.com")
    .with_authentication(Authentication::Basic {
        username: "user".to_string(),
        password: "pass".into(),
    })
    .build()?;

// API key authentication
let client = ApiClient::builder()
    .with_host("api.example.com")
    .with_authentication(Authentication::ApiKey {
        header_name: "X-API-Key".to_string(),
        key: "secret-key".into(),
    })
    .build()?;

§Per-Request Authentication Override

use clawspec_core::{ApiClient, Authentication};

// Client with default authentication
let mut client = ApiClient::builder()
    .with_host("api.example.com")
    .with_authentication(Authentication::Bearer("default-token".into()))
    .build()?;

// Use different authentication for admin endpoints
let admin_users = client
    .get("/admin/users")?
    .with_authentication(Authentication::Bearer("admin-token".into()))
    .await?
    .as_json::<serde_json::Value>()
    .await?;

// Remove authentication for public endpoints
let public_data = client
    .get("/public/health")?
    .with_authentication_none()
    .await?
    .as_text()
    .await?;

§Authentication Types

  • Bearer: Adds Authorization: Bearer <token> header
  • Basic: Adds Authorization: Basic <base64(username:password)> header
  • ApiKey: Adds custom header with API key

§Security Best Practices

  • Store credentials securely using environment variables or secret management tools
  • Rotate tokens regularly
  • Use HTTPS for all authenticated requests
  • Avoid logging authentication headers

§Status Code Validation

By default, requests expect status codes in the range 200-499 (inclusive of 200, exclusive of 500). You can customize this behavior:

use clawspec_core::{ApiClient, expected_status_codes};

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

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

§Response Descriptions

Add descriptive text to your OpenAPI responses for better documentation:

use clawspec_core::ApiClient;

// Set a description for the actual returned status code
client.get("/users/{id}")?
    .with_response_description("User details if found, or error information")
    .await?;

// The description applies to whatever status code is actually returned
client.post("/users")?
    .with_response_description("User created successfully or validation error")
    .await?;

§Response Redaction

Requires the redaction feature.

When generating OpenAPI examples from real API responses, dynamic values like UUIDs, timestamps, and tokens make examples unstable across test runs. The redaction feature allows you to replace these dynamic values with stable, predictable ones in the generated OpenAPI specification while preserving the actual values for assertions.

This is particularly useful for:

  • Snapshot testing: Generated OpenAPI files remain stable across runs
  • Documentation: Examples show consistent, readable placeholder values
  • Security: Sensitive values can be masked in documentation

§Basic Usage

use clawspec_core::ApiClient;

#[derive(Deserialize, ToSchema)]
struct User {
    id: String,           // Dynamic UUID
    name: String,
    created_at: String,   // Dynamic timestamp
}

let user: User = client
    .post("/users")?
    .json(&serde_json::json!({"name": "Alice"}))?
    .await?
    .as_json_redacted()
    .await?
    // Replace dynamic UUID with stable value
    .redact("/id", "00000000-0000-0000-0000-000000000001")?
    // Replace timestamp with stable value
    .redact("/created_at", "2024-01-01T00:00:00Z")?
    .finish()
    .await
    .value;

// The actual user has real dynamic values for assertions
assert!(!user.id.is_empty());
// But the OpenAPI example shows the redacted stable values

§Redaction Operations

The redaction builder supports two operations using JSON Pointer (RFC 6901):

  • redact(pointer, redactor): Replace a value at the given path with a stable value or transformation
  • redact_remove(pointer): Remove a value entirely from the OpenAPI example
let response: Response = client
    .post("/auth/login")?
    .json(&serde_json::json!({"username": "test", "password": "secret"}))?
    .await?
    .as_json_redacted()
    .await?
    .redact("/token", "[REDACTED_TOKEN]")?
    .redact("/session_id", "session-00000000")?
    .redact_remove("/internal_ref")?  // Remove internal field from docs
    .finish()
    .await
    .value;

§Path Syntax

Paths are auto-detected based on their prefix:

  • /... → JSON Pointer (RFC 6901) - exact paths only
  • $... → JSONPath (RFC 9535) - supports wildcards

§JSON Pointer Syntax

JSON Pointers use / as a path separator. Special characters are escaped:

  • ~0 represents ~
  • ~1 represents /

Examples:

  • /id - Top-level field named “id”
  • /user/name - Nested field “name” inside “user”
  • /items/0/id - First element’s “id” in an array
  • /foo~1bar - Field named “foo/bar”

§JSONPath Wildcards

For arrays, use JSONPath syntax (starting with $) to redact all elements:

let users: Vec<User> = client
    .get("/users")?
    .await?
    .as_json_redacted()
    .await?
    .redact("$[*].id", "stable-uuid")?        // All IDs in array
    .redact("$[*].created_at", "2024-01-01T00:00:00Z")?  // All timestamps
    .finish()
    .await
    .value;

§Dynamic Transformations

Pass a closure for dynamic redaction. The closure receives the concrete JSON Pointer path and current value:

let users: Vec<User> = client
    .get("/users")?
    .await?
    .as_json_redacted()
    .await?
    // Create stable index-based IDs: user-0, user-1, user-2, ...
    .redact("$[*].id", |path: &str, _val: &Value| {
        let idx = path.split('/').nth(1).unwrap_or("0");
        serde_json::json!(format!("user-{idx}"))
    })?
    .finish()
    .await
    .value;

§Getting Both Values

The RedactedResult returned by finish() contains both:

  • value: The actual deserialized response (with real dynamic values)
  • redacted: The JSON with redacted values (as stored in OpenAPI)
let result = client
    .get("/users/123")?
    .await?
    .as_json_redacted::<User>()
    .await?
    .redact("/id", "user-00000000")?
    .finish()
    .await;

// Use actual value for test assertions
let user = result.value;
assert!(!user.id.is_empty());

// Access redacted JSON if needed
let redacted_json = result.redacted;
assert_eq!(redacted_json["id"], "user-00000000");

§Schema Registration

§Automatic Schema Capture

JSON request and response body schemas are automatically captured when using .json() and .as_json() methods:

use clawspec_core::ApiClient;

#[derive(Serialize, Deserialize, ToSchema)]
struct CreateUser { name: String, email: String }

#[derive(Deserialize, ToSchema)]
struct User { id: u32, name: String, email: String }

// Schemas are captured automatically - no explicit registration needed
let user: User = client
    .post("/users")?
    .json(&CreateUser { name: "Alice".to_string(), email: "alice@example.com".to_string() })?
    .await?
    .as_json()
    .await?;

§Manual Schema Registration

For nested schemas or when you need to ensure all dependencies are included, use the register_schemas! macro:

use clawspec_core::{ApiClient, register_schemas};

#[derive(Serialize, Deserialize, ToSchema)]
struct Address { street: String, city: String }

#[derive(Serialize, Deserialize, ToSchema)]
struct CreateUser { name: String, email: String, address: Address }

#[derive(Deserialize, ToSchema)]
struct ErrorResponse { code: String, message: String }

// Register nested schemas and error types for complete documentation
register_schemas!(client, CreateUser, Address, ErrorResponse).await;

§⚠️ Nested Schema Limitation

Current Limitation: While main JSON body schemas are captured automatically, nested schemas may not be fully resolved. If you encounter missing nested schemas in your OpenAPI specification, use the register_schemas! macro to explicitly register them:

use clawspec_core::{ApiClient, register_schemas};

#[derive(Serialize, Deserialize, ToSchema)]
struct Position { lat: f64, lng: f64 }

#[derive(Serialize, Deserialize, ToSchema)]
struct Location { name: String, position: Position }  // Position is nested

// Register both main and nested schemas to ensure complete OpenAPI generation
register_schemas!(client, Location, Position).await;

Workaround: Always register nested schemas explicitly when you need complete OpenAPI documentation with all referenced types properly defined.

§Error Handling

The library provides two main error types:

§See Also

§Re-exports

All commonly used types are re-exported from the crate root for convenience.

Modules§

_tutorial
Tutorial: Getting Started with Clawspec
test_client
Generic test client framework for async server testing.

Macros§

expected_status_codes
Creates an ExpectedStatusCodes instance with the specified status codes and ranges.
register_schemas
Registers multiple schema types with the ApiClient for OpenAPI documentation.

Structs§

ApiCall
Builder for configuring HTTP API calls with comprehensive parameter and validation support.
ApiClient
ApiClientBuilder
Builder for creating ApiClient instances with comprehensive configuration options.
CallBody
Represents the body of an HTTP request with its content type and schema information.
CallCookies
Represents HTTP cookies for an API call.
CallHeaders
Represents HTTP headers for an API call.
CallPath
A parameterized HTTP path with type-safe parameter substitution.
CallQuery
A collection of query parameters for HTTP requests with OpenAPI 3.1 support.
CallResult
Represents the result of an API call with response processing capabilities.
ExpectedStatusCodes
Expected status codes for HTTP requests.
Info
OpenAPI types re-exported from utoipa for convenience. OpenAPI Info object represents metadata of the API.
InfoBuilder
OpenAPI types re-exported from utoipa for convenience. Builder for Info with chainable configuration methods to create a new Info.
OAuth2Configoauth2
OAuth2 authentication configuration.
OAuth2ConfigBuilderoauth2
Builder for OAuth2 configuration.
OAuth2Flow
OAuth2 flow configuration (for flows with token URL).
OAuth2Flows
OAuth2 flow configurations.
OAuth2ImplicitFlow
OAuth2 implicit flow configuration.
OAuth2Tokenoauth2
An OAuth2 access token with expiration tracking.
OpenApi
OpenAPI types re-exported from utoipa for convenience. Root object of the OpenAPI document.
ParamValue
A parameter value with its serialization style
Paths
OpenAPI types re-exported from utoipa for convenience. Implements OpenAPI Paths Object.
RawResult
Represents the raw response data from an HTTP request.
RedactOptionsredaction
Options for configuring redaction behavior.
RedactedResultredaction
Result of a redacted JSON response containing both the real and redacted values.
RedactionBuilderredaction
Builder for applying redactions to JSON responses.
SecureString
Secure wrapper for sensitive string data that automatically zeroes memory on drop.
SecurityRequirement
Security requirement specifying which scheme and scopes are needed.
Server
OpenAPI types re-exported from utoipa for convenience. Represents target server object. It can be used to alter server connection for path operations.
ServerBuilder
OpenAPI types re-exported from utoipa for convenience. Builder for Server with chainable configuration methods to create a new Server.
StatusCode
HTTP status codes re-exported from the http crate. An HTTP status code (status-code in RFC 9110 et al.).
ValueRedactionBuilderredaction
Builder for redacting arbitrary JSON values.

Enums§

ApiClientError
Errors that can occur when using the ApiClient.
ApiKeyLocation
Location where an API key is passed.
Authentication
Authentication configuration for API requests.
AuthenticationError
Errors that can occur during authentication processing.
OAuth2Erroroauth2
Errors that can occur during OAuth2 authentication.
ParamStyle
Parameter styles supported by OpenAPI 3.1 specification.
RawBody
Represents the body content of a raw HTTP response.
SecurityScheme
OpenAPI security scheme configuration.

Traits§

ParameterValue
A trait alias for types that can be used as parameter values.
Redactorredaction
Trait for types that can be used to redact values.
ToSchema
The ToSchema derive macro for generating OpenAPI schemas. Types used in JSON request/response bodies should derive this trait. Trait for implementing OpenAPI Schema object.

Functions§

redact_valueredaction
Create a redaction builder for an arbitrary JSON value.

Derive Macros§

ToSchema
The ToSchema derive macro for generating OpenAPI schemas. Types used in JSON request/response bodies should derive this trait. Generate reusable OpenAPI schema to be used together with OpenApi.