Struct ApiClient

Source
pub struct ApiClient { /* private fields */ }
Expand description

A type-safe HTTP client for API testing and OpenAPI documentation generation.

ApiClient is the core component of clawspec that enables you to make HTTP requests while automatically capturing request/response schemas for OpenAPI specification generation. It provides a fluent API for building requests with comprehensive parameter support, status code validation, and automatic schema collection.

§Key Features

  • Test-Driven Documentation: Automatically generates OpenAPI specifications from test execution
  • Type Safety: Compile-time guarantees for API parameters and response types
  • Flexible Status Code Validation: Support for ranges, specific codes, and custom patterns
  • Comprehensive Parameter Support: Path, query, and header parameters with multiple styles
  • Request Body Formats: JSON, form-encoded, multipart, and raw binary data
  • Schema Collection: Automatic detection and collection of request/response schemas
  • OpenAPI Metadata: Configurable API info, servers, and operation documentation

§Basic Usage

use clawspec_core::ApiClient;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create an API client
    let mut client = ApiClient::builder()
        .with_host("api.example.com")
        .with_base_path("/v1")?
        .build()?;

    // Make a request and capture the schema
    let user: User = client
        .get("/users/123")?
         
        .await?
        .as_json()
        .await?;

    println!("User: {:?}", user);

    // Generate OpenAPI specification from collected data
    let openapi_spec = client.collected_openapi().await;
    let yaml = serde_yaml::to_string(&openapi_spec)?;
    println!("{yaml}");

    Ok(())
}

§Builder Pattern

The client is created using a builder pattern that allows you to configure various aspects:

use clawspec_core::ApiClient;
use http::uri::Scheme;
use utoipa::openapi::{InfoBuilder, ServerBuilder};

let client = ApiClient::builder()
    .with_scheme(Scheme::HTTPS)
    .with_host("api.github.com")
    .with_port(443)
    .with_base_path("/api/v3")?
    .with_info(
        InfoBuilder::new()
            .title("GitHub API Client")
            .version("1.0.0")
            .description(Some("Auto-generated from tests"))
            .build()
    )
    .add_server(
        ServerBuilder::new()
            .url("https://api.github.com/api/v3")
            .description(Some("GitHub API v3"))
            .build()
    )
    .build()?;

§Making Requests

The client supports all standard HTTP methods with a fluent API:

use clawspec_core::{ApiClient, expected_status_codes, CallQuery, CallHeaders, ParamValue};
use serde::{Serialize, Deserialize};
use utoipa::ToSchema;

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

let mut client = ApiClient::builder().build()?;
let user_data = UserData { name: "John".to_string() };

// GET request with query parameters and headers
let users = client
    .get("/users")?
    .with_query(
        CallQuery::new()
            .add_param("page", ParamValue::new(1))
            .add_param("per_page", ParamValue::new(50))
    )
    .with_header("Authorization", "Bearer token123")
    .with_expected_status_codes(expected_status_codes!(200, 404))
     
    .await?
    .as_json::<Vec<UserData>>()
    .await?;

// POST request with JSON body
let new_user = client
    .post("/users")?
    .json(&user_data)?
    .with_expected_status_codes(expected_status_codes!(201, 409))
     
    .await?
    .as_json::<UserData>()
    .await?;

§Schema Registration

For types that aren’t automatically detected, you can manually register them:

use clawspec_core::{ApiClient, register_schemas};

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
struct ErrorType { message: String }

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]  
struct NestedType { value: i32 }

let mut client = ApiClient::builder().build()?;

// Register multiple schemas at once
register_schemas!(client, ErrorType, NestedType);

// Or register individually
client.register_schema::<ErrorType>().await;

§OpenAPI Generation

The client automatically collects information during test execution and can generate comprehensive OpenAPI specifications:

let mut client = ApiClient::builder().build()?;
let user_data = UserData { name: "John".to_string() };

// Make some API calls...
client.get("/users")?.await?.as_json::<Vec<UserData>>().await?;
client.post("/users")?.json(&user_data)?.await?.as_json::<UserData>().await?;

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

// Convert to YAML or JSON
let yaml = serde_yaml::to_string(&openapi)?;
let json = serde_json::to_string_pretty(&openapi)?;

§Error Handling

The client provides comprehensive error handling for various scenarios:

use clawspec_core::{ApiClient, ApiClientError};

let mut client = ApiClient::builder().build()?;

match client.get("/users/999")?.await {
    Ok(response) => {
        // Handle successful response
        println!("Success!");
    }
    Err(ApiClientError::UnexpectedStatusCode { status_code, body }) => {
        // Handle HTTP errors
        println!("HTTP {} error: {}", status_code, body);
    }
    Err(ApiClientError::ReqwestError(source)) => {
        // Handle network/request errors
        println!("Request failed: {}", source);
    }
    Err(err) => {
        // Handle other errors
        println!("Other error: {}", err);
    }
}

§Thread Safety

ApiClient is designed to be safe to use across multiple threads. The internal schema collection is protected by async locks, allowing concurrent request execution while maintaining data consistency.

§Performance Considerations

  • Schema collection has minimal runtime overhead
  • Request bodies are streamed when possible
  • Response processing is lazy - schemas are only collected when responses are consumed
  • Internal caching reduces redundant schema processing

Implementations§

Source§

impl ApiClient

Source§

impl ApiClient

Source

pub async fn collected_paths(&mut self) -> Paths

Source

pub async fn collected_openapi(&mut self) -> OpenApi

Generates a complete OpenAPI specification from collected request/response data.

This method aggregates all the information collected during API calls and produces a comprehensive OpenAPI 3.1 specification including paths, components, schemas, operation metadata, and server information.

§Features
  • Automatic Path Collection: All endpoint calls are automatically documented
  • Schema Generation: Request/response schemas are extracted from Rust types
  • Operation Metadata: Includes operation IDs, descriptions, and tags
  • Server Information: Configurable server URLs and descriptions
  • Tag Collection: Automatically computed from all operations
  • Component Schemas: Reusable schema definitions with proper references
§Example
use clawspec_core::ApiClient;
use utoipa::openapi::{InfoBuilder, ServerBuilder};
use serde::{Serialize, Deserialize};
use utoipa::ToSchema;

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

let mut client = ApiClient::builder()
    .with_host("api.example.com")
    .with_info(
        InfoBuilder::new()
            .title("My API")
            .version("1.0.0")
            .build()
    )
    .add_server(
        ServerBuilder::new()
            .url("https://api.example.com")
            .description(Some("Production server"))
            .build()
    )
    .build()?;

let user_data = UserData { name: "John".to_string() };

// Make some API calls to collect data
client.get("/users")?.await?.as_json::<Vec<UserData>>().await?;
client.post("/users")?.json(&user_data)?.await?.as_json::<UserData>().await?;

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

// The generated spec includes:
// - API info (title, version, description)
// - Server definitions
// - All paths with operations
// - Component schemas
// - Computed tags from operations

// Export to different formats
let yaml = serde_yaml::to_string(&openapi)?;
let json = serde_json::to_string_pretty(&openapi)?;
§Generated Content

The generated OpenAPI specification includes:

  • Info: API metadata (title, version, description) if configured
  • Servers: Server URLs and descriptions if configured
  • Paths: All documented endpoints with operations
  • Components: Reusable schema definitions
  • Tags: Automatically computed from operation tags
§Tag Generation

Tags are automatically computed from all operations and include:

  • Explicit tags set on operations
  • Auto-generated tags based on path patterns
  • Deduplicated and sorted alphabetically
§Performance Notes
  • This method acquires read locks on internal collections
  • Schema processing is cached to avoid redundant work
  • Tags are computed on-demand from operation metadata
Source

pub async fn register_schema<T>(&mut self)
where T: ToSchema + 'static,

Manually registers a type in the schema collection.

This method allows you to explicitly add types to the OpenAPI schema collection that might not be automatically detected. This is useful for types that are referenced indirectly, such as nested types.

§Type Parameters
  • T - The type to register, must implement ToSchema and 'static
§Example
use clawspec_core::ApiClient;

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
struct NestedErrorType {
    message: String,
}

let mut client = ApiClient::builder().build()?;

// Register the nested type that might not be automatically detected
client.register_schema::<NestedErrorType>().await;

// Now when you generate the OpenAPI spec, NestedErrorType will be included
let openapi = client.collected_openapi().await;
Source§

impl ApiClient

Source

pub fn call( &self, method: Method, path: CallPath, ) -> Result<ApiCall, ApiClientError>

Source

pub fn get(&self, path: impl Into<CallPath>) -> Result<ApiCall, ApiClientError>

Source

pub fn post(&self, path: impl Into<CallPath>) -> Result<ApiCall, ApiClientError>

Source

pub fn put(&self, path: impl Into<CallPath>) -> Result<ApiCall, ApiClientError>

Source

pub fn delete( &self, path: impl Into<CallPath>, ) -> Result<ApiCall, ApiClientError>

Source

pub fn patch( &self, path: impl Into<CallPath>, ) -> Result<ApiCall, ApiClientError>

Trait Implementations§

Source§

impl Clone for ApiClient

Source§

fn clone(&self) -> ApiClient

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ApiClient

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

impl<T> ErasedDestructor for T
where T: 'static,