stonehm

Crates.iostonehm
lib.rsstonehm
version0.1.4
created_at2025-09-29 21:01:33.432638+00
updated_at2025-10-28 19:50:59.097477+00
descriptionAutomatic OpenAPI 3.0 generation for Axum applications
homepage
repositoryhttps://github.com/melito/stonehm
max_upload_size
id1860195
size105,270
Mel Gray (melito)

documentation

README

stonehm - Documentation-Driven OpenAPI Generation for Axum

stonehm automatically generates comprehensive OpenAPI 3.0 specifications for Axum web applications by analyzing handler functions and their documentation. The core principle is "documentation is the spec" - write clear, natural documentation and get complete OpenAPI specs automatically.

Key Features

  • Generate OpenAPI 3.0 specs from rustdoc comments
  • Automatic error handling from Result<T, E> return types
  • Type-safe schema generation via derive macros
  • Compile-time processing with zero runtime overhead
  • Drop-in replacement for axum::Router

Quick Start

Installation

Add to your Cargo.toml:

[dependencies]
stonehm = "0.1"
stonehm-macros = "0.1"
axum = "0.7"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1.0", features = ["derive"] }

30-Second Example

use axum::{Json, extract::Path};
use serde::{Serialize, Deserialize};
use stonehm::{api_router, api_handler};
use stonehm_macros::{StonehmSchema, api_error};

// Define your data types
#[derive(Serialize, StonehmSchema)]
struct User {
    id: u32,
    name: String,
    email: String,
}

#[api_error]
enum ApiError {
    /// 404: User not found
    UserNotFound { id: u32 },
    
    /// 500: Internal server error
    DatabaseError,
}

/// Get user by ID
///
/// Retrieves a user's information using their unique identifier.
/// Returns detailed user data including name and email.
#[api_handler]
async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, ApiError> {
    Ok(Json(User {
        id,
        name: format!("User {}", id),
        email: format!("user{}@example.com", id),
    }))
}

#[tokio::main]
async fn main() {
    let app = api_router!("User API", "1.0.0")
        .get("/users/:id", get_user)
        .with_openapi_routes()  // Adds /openapi.json and /openapi.yaml
        .into_router();

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    println!("Server running on http://127.0.0.1:3000");
    println!("OpenAPI spec: http://127.0.0.1:3000/openapi.json");
    
    axum::serve(listener, app).await.unwrap();
}

That's it! You now have a fully documented API with automatic OpenAPI generation.

Automatic OpenAPI generation includes:

  • Path parameter documentation
  • 200 response with User schema
  • 400 Bad Request with ApiError schema
  • 500 Internal Server Error with ApiError schema

Documentation Approaches

stonehm supports three documentation approaches to fit different needs:

1. Automatic Documentation (Recommended)

Let stonehm infer everything from your code structure:

/// Get user profile
///
/// Retrieves the current user's profile information.
#[api_handler]
async fn get_profile() -> Result<Json<User>, ApiError> {
    Ok(Json(User::default()))
}

Automatically generates:

  • 200 response with User schema
  • 400 Bad Request with ApiError schema
  • 500 Internal Server Error with ApiError schema

### 2. Structured Documentation

Add detailed parameter and response documentation:

```rust
/// Update user profile
///
/// Updates the user's profile information. Only provided fields
/// will be updated, others remain unchanged.
///
/// # Parameters
/// - id (path): The user's unique identifier
/// - version (query): API version to use
/// - authorization (header): Bearer token for authentication
///
/// # Request Body
/// Content-Type: application/json
/// User update data with optional fields for name, email, and preferences.
///
/// # Responses
/// - 200: User successfully updated
/// - 400: Invalid user data provided
/// - 401: Authentication required
/// - 404: User not found
/// - 422: Validation failed
#[api_handler]
async fn update_profile(
    Path(id): Path<u32>,
    Json(request): Json<UpdateUserRequest>
) -> Result<Json<User>, ApiError> {
    Ok(Json(User::default()))
}

3. Elaborate Documentation

For complex APIs requiring detailed error schemas:

/// Delete user account
///
/// Permanently removes a user account and all associated data.
/// This action cannot be undone.
///
/// # Parameters
/// - id (path): The unique user identifier to delete
///
/// # Responses
/// - 204: User successfully deleted
/// - 404:
///   description: User not found
///   content:
///     application/json:
///       schema: NotFoundError
/// - 403:
///   description: Insufficient permissions to delete user
///   content:
///     application/json:
///       schema: PermissionError
/// - 409:
///   description: Cannot delete user with active subscriptions
///   content:
///     application/json:
///       schema: ConflictError
#[api_handler]
async fn delete_user(Path(id): Path<u32>) -> Result<(), ApiError> {
    Ok(())
}

Schema Generation

stonehm uses the StonehmSchema derive macro for automatic schema generation:

use serde::{Serialize, Deserialize};
use stonehm_macros::StonehmSchema;

#[derive(Serialize, Deserialize, StonehmSchema)]
struct CreateUserRequest {
    name: String,
    email: String,
    age: Option<u32>,
    preferences: UserPreferences,
}

#[derive(Serialize, StonehmSchema)]
struct UserResponse {
    id: u32,
    name: String,
    email: String,
    created_at: String,
    is_active: bool,
}

#[api_error]
enum ApiError {
    /// 400: Invalid input provided
    InvalidInput { field: String, message: String },
    
    /// 404: User not found
    UserNotFound { id: u32 },
    
    /// 409: Email already exists
    EmailAlreadyExists { email: String },
    
    /// 500: Internal server error
    DatabaseError,
    
    /// 422: Validation failed
    ValidationFailed,
}

Supported types: All primitive types, Option<T>, Vec<T>, nested structs, and enums.

Router Setup

Basic Setup

use stonehm::api_router;

#[tokio::main]
async fn main() {
    let app = api_router!("My API", "1.0.0")
        .get("/users/:id", get_user)
        .post("/users", create_user)
        .put("/users/:id", update_user)
        .delete("/users/:id", delete_user)
        .with_openapi_routes()  // Adds /openapi.json and /openapi.yaml
        .into_router();

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Custom OpenAPI Endpoints

// Default endpoints
.with_openapi_routes()  // Creates /openapi.json and /openapi.yaml

// Custom prefix
.with_openapi_routes_prefix("/api/docs")  // Creates /api/docs.json and /api/docs.yaml

// Custom paths
.with_openapi_routes_prefix("/v1/spec")   // Creates /v1/spec.json and /v1/spec.yaml

Documentation Format Reference

Summary and Description

/// Brief one-line summary
///
/// Detailed description that can span multiple paragraphs.
/// This becomes the OpenAPI description field.

Parameters Section

/// # Parameters  
/// - id (path): The unique user identifier
/// - page (query): Page number for pagination
/// - limit (query): Maximum results per page  
/// - authorization (header): Bearer token for authentication

Request Body Section

/// # Request Body
/// Content-Type: application/json
/// Detailed description of the expected request body structure
/// and any validation requirements.

Response Documentation

Simple format (covers most use cases):

/// # Responses
/// - 200: User successfully created
/// - 400: Invalid user data provided
/// - 409: Email address already exists

Elaborate format (for detailed error documentation):

/// # Responses
/// - 201: User successfully created
/// - 400:
///   description: Validation failed
///   content:
///     application/json:
///       schema: ValidationError
/// - 409:
///   description: Email already exists
///   content:
///     application/json:
///       schema: ConflictError

Best Practices

1. Use Result Types for Error Handling

Return Result<Json<T>, E> to get automatic error responses:

/// Recommended - Automatic error handling
#[api_handler]
async fn get_user() -> Result<Json<User>, ApiError> {
    Ok(Json(User { id: 1, name: "John".to_string(), email: "john@example.com".to_string() }))
}

/// Manual - Requires explicit response documentation
#[api_handler]  
async fn get_user_manual() -> Json<User> {
    Json(User { id: 1, name: "John".to_string(), email: "john@example.com".to_string() })
}

Generated OpenAPI for automatic error handling:

responses:
  '200':
    description: Success
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/User'
  '400':
    description: Bad Request
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/ApiError'
  '500':
    description: Internal Server Error
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/ApiError'

Manual documentation requires explicit responses:

responses:
  '200':
    description: Success
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/User'

2. Use api_error Macro for Error Types

use stonehm_macros::api_error;

#[api_error]
enum ApiError {
    /// 404: User not found
    UserNotFound { id: u32 },
    
    /// 400: Validation failed
    ValidationError { field: String, message: String },
    
    /// 500: Internal server error
    DatabaseError,
}

The api_error macro automatically generates IntoResponse, Serialize, and StonehmSchema implementations, eliminating all boilerplate.

3. Keep Documentation Natural

Focus on business logic, not OpenAPI details:

/// Good - describes what the endpoint does
/// Creates a new user account with email verification

/// Avoid - implementation details
/// Returns HTTP 201 with application/json content-type

4. Choose the Right Documentation Level

/// Simple for basic APIs
/// # Responses
/// - 200: Success
/// - 400: Bad request

/// Elaborate for complex error handling
/// # Responses  
/// - 400:
///   description: Validation failed
///   content:
///     application/json:
///       schema: ValidationError

Automatic vs Manual Response Documentation

Return Type Automatic Behavior When to Use Manual
Json<T> 200 response with T schema Simple endpoints
Result<Json<T>, E> 200 with T schema
400, 500 with E schema
Most endpoints (recommended)
() or StatusCode 200 empty response DELETE operations
Custom types Depends on implementation Advanced use cases

Common Troubleshooting

Q: My error responses aren't appearing
A: Ensure your function returns Result<Json<T>, E> and E implements IntoResponse.

Q: Schemas aren't in the OpenAPI spec
A: Add #[derive(StonehmSchema)] to your types and use them in function signatures.

Q: Path parameters not documented
A: Add them to the # Parameters section with (path) type specification.

Q: Custom response schemas not working
A: Use the elaborate response format with explicit schema references.

API Reference

Macros

Macro Purpose Example
api_router!(title, version) Create documented router api_router!("My API", "1.0.0")
#[api_handler] Mark handler for documentation #[api_handler] async fn get_user() {}
#[derive(StonehmSchema)] Generate JSON schema #[derive(Serialize, StonehmSchema)] struct User {}

Router Methods

let app = api_router!("API", "1.0.0")
    .get("/users", list_users)           // GET route
    .post("/users", create_user)         // POST route  
    .put("/users/:id", update_user)      // PUT route
    .delete("/users/:id", delete_user)   // DELETE route
    .patch("/users/:id", patch_user)     // PATCH route
    .with_openapi_routes()               // Add OpenAPI endpoints
    .into_router();                      // Convert to axum::Router

OpenAPI Endpoints

Method Creates Description
.with_openapi_routes() /openapi.json
/openapi.yaml
Default OpenAPI endpoints
.with_openapi_routes_prefix("/api") /api.json
/api.yaml
Custom prefix

Response Type Mapping

Rust Type OpenAPI Response Automatic Errors
Json<T> 200 with T schema None
Result<Json<T>, E> 200 with T schema 400, 500 with E schema
() 204 No Content None
StatusCode Custom status None

Examples

Full REST API Example

use axum::{Json, extract::{Path, Query}};
use serde::{Serialize, Deserialize};
use stonehm::{api_router, api_handler};
use stonehm_macros::StonehmSchema;

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

#[derive(Deserialize, StonehmSchema)]
struct CreateUserRequest {
    name: String,
    email: String,
}

#[derive(Deserialize)]
struct UserQuery {
    page: Option<u32>,
    limit: Option<u32>,
}

#[api_error]
enum ApiError {
    /// 404: User not found
    UserNotFound { id: u32 },
    
    /// 400: Validation failed
    ValidationError { field: String, message: String },
    
    /// 500: Internal server error
    DatabaseError,
}

/// List users with pagination
///
/// Retrieves a paginated list of users from the database.
///
/// # Parameters
/// - page (query): Page number (default: 1)
/// - limit (query): Users per page (default: 10, max: 100)
#[api_handler]
async fn list_users(Query(query): Query<UserQuery>) -> Result<Json<Vec<User>>, ApiError> {
    Ok(Json(vec![]))
}

/// Get user by ID
///
/// Retrieves detailed user information by ID.
#[api_handler]
async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, ApiError> {
    Ok(Json(User {
        id,
        name: "John Doe".to_string(),
        email: "john@example.com".to_string(),
        created_at: "2024-01-01T00:00:00Z".to_string(),
    }))
}

/// Create new user
///
/// Creates a new user account with the provided information.
///
/// # Request Body
/// Content-Type: application/json
/// User creation data with required name and email fields.
#[api_handler]
async fn create_user(Json(req): Json<CreateUserRequest>) -> Result<Json<User>, ApiError> {
    Ok(Json(User {
        id: 42,
        name: req.name,
        email: req.email,
        created_at: "2024-01-01T00:00:00Z".to_string(),
    }))
}

#[tokio::main]
async fn main() {
    let app = api_router!("User Management API", "1.0.0")
        .get("/users", list_users)
        .get("/users/:id", get_user)
        .post("/users", create_user)
        .with_openapi_routes()
        .into_router();

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    println!("Server running on http://127.0.0.1:3000");
    println!("OpenAPI spec: http://127.0.0.1:3000/openapi.json");
    axum::serve(listener, app).await.unwrap();
}

Development

Running Examples

# Clone the repository
git clone https://github.com/melito/stonehm.git
cd stonehm

# Run the example server
cargo run -p hello_world

# Test OpenAPI generation
cargo run -p hello_world -- --test-schema

# Use default endpoints (/openapi.json, /openapi.yaml)
cargo run -p hello_world -- --default

Testing Schema Generation

# Generate and view the OpenAPI spec
cargo run -p hello_world -- --test-schema | jq '.'

# Check specific endpoints
cargo run -p hello_world -- --test-schema | jq '.paths."/users".post'

# View all schemas
cargo run -p hello_world -- --test-schema | jq '.components.schemas'

Contributing

We welcome contributions! Please feel free to submit issues and pull requests.

Development Setup

# Run tests
cargo test

# Check formatting
cargo fmt --check

# Run clippy
cargo clippy -- -D warnings

# Test all examples
cargo test --workspace

License

This project is licensed under the MIT License - see the LICENSE file for details.


Documentation | Crates.io | Repository

Commit count: 0

cargo fmt