mandolin 0.4.5

Input openapi.json/yaml, output server source code in rust.
Documentation

mandolin

GitHub License Crates.io

Mandolin is a tool to generate server and client code from OpenAPI specifications (JSON, and optionally YAML). It currently supports:

  • Rust server: axum
  • Rust client: reqwest (opt-in via mandolin_client feature)
  • TypeScript: Hono

Mandolin adopts a "Logic in Templates" design philosophy, where Rust handles data preparation and $ref resolution, while templates handle the code assembly.

Features

Feature Default Description
yaml off Enable YAML input support via serde_yaml. When disabled, all input (regardless of file extension) is parsed as JSON.
mandolin_client off Enable ApiClient in the generated code. Uses reqwest to call the API as an HTTP client.

Enable YAML support:

# Cargo.toml
[dependencies]
mandolin = { version = "...", features = ["yaml"] }

Or when installing the CLI:

$ cargo install mandolin --features yaml

Getting started

Install mandolin from source:

$ cargo install --path .

Render axum server code using builtin "RUST_AXUM" template:

$ mandolin -i ./openapi/openapi_plant.yaml -t RUST_AXUM -o ./examples/example_server.rs

Using pipes:

$ cat openapi.json | mandolin -i - -t TYPESCRIPT_HONO > ./examples/server.ts

You can also use mandolin as library

Mandolin exposes a simple API mandolin::environment that returns a configured Minijinja environment.

use mandolin;
use openapiv3::OpenAPI;

fn main() {
    // 1. Read OpenAPI file (use openapi_loader to handle JSON / YAML transparently)
    let f = std::fs::File::open("./openapi/openapi_petstore.json").unwrap();
    let api: OpenAPI = mandolin::openapi_loader::load(f, "openapi_petstore.json").unwrap();

    // 2. Create environment
    let env = mandolin::environment(api).unwrap();

    // 3. Render
    let output = env.get_template("RUST_AXUM").unwrap().render(0).unwrap();
    
    std::fs::write("examples/generated_server.rs", output).unwrap();
}

Note: To load YAML files, enable the yaml feature. Without it, openapi_loader::load falls back to JSON parsing regardless of the file extension.

Example of generated code

The generated code defines two traits: ApiInterface (business logic) and ApiInterfaceAxum (axum-specific behavior). You must implement both to use axum_router.

// This is generated by mandolin

use axum;
use serde;
use std::future::Future;

pub trait ApiInterface {
    // get /device/{key}
    fn device_get(&self, key: String) -> impl Future<Output = DeviceGetResponse> + Send {
        async { DeviceGetResponse::Status404 }
    }
    
    // post /device
    fn device_create(&self, req: DeviceCreateRequest) -> impl Future<Output = DeviceCreateResponse> + Send {
        async { DeviceCreateResponse::Status201(Default::default()) }
    }
}
// ... Request/Response structs and Router definition follows ...

Running the generated server with your implementation

You can import the generated module and implement the trait to build your server.

mod generated; // The file generated by mandolin

use generated::*;
use axum::{Router, serve};
use tokio::net::TcpListener;

struct MyServer {
    db_url: String,
}

// Implement the business logic
impl ApiInterface for MyServer {
    async fn device_get(&self, req: DeviceGetRequest) -> DeviceGetResponse {
        // Your implementation here...
        DeviceGetResponse::Status200(Device {
            key: req.key,
            name: "example-device".to_string(),
            ..Default::default()
        })
    }
}

// Required to use axum_router. Override methods for axum-specific behavior.
impl ApiInterfaceAxum for MyServer {
    // async fn authorize(&self, _req: http::Request<()>) -> Result<AuthContext, String> { Ok(Default::default()) }

    // GET /device/{key}
    // async fn device_get(&self, _raw: http::Request<()>, req: DeviceGetRequest) -> axum::response::Response { ... }

    // POST /device
    // async fn device_create(&self, _raw: http::Request<()>, req: DeviceCreateRequest) -> axum::response::Response { ... }
}

#[tokio::main]
async fn main() {
    let api = MyServer { db_url: "postgres://...".to_string() };
    
    // 'axum_router' is generated by mandolin
    let app = axum_router(api); 
    
    let listener = TcpListener::bind("0.0.0.0:8080").await.unwrap();
    serve(listener, app).await.unwrap();
}

Using the generated client

Enable the mandolin_client feature in your generated crate's Cargo.toml:

[features]
mandolin_client = ["dep:reqwest"]

[dependencies]
reqwest = { version = "*", features = ["json"], optional = true }

The generated code includes ApiClient, which implements ApiInterface using reqwest. You can use it anywhere a server implementation would be used.

mod generated; // The file generated by mandolin

use generated::*;

#[tokio::main]
async fn main() {
    let client = ApiClient::new("http://localhost:8080/api");

    // Call the API — same interface as the server implementation
    let response = client.device_get(DeviceGetRequest {
        key: "my-device".to_string(),
    }).await;

    match response {
        DeviceGetResponse::Status200(device) => println!("{:?}", device),
        DeviceGetResponse::Status404       => println!("not found"),
        DeviceGetResponse::Error(msg)      => println!("error: {msg}"),
        _ => {}
    }
}

DeviceGetResponse::Error(String) is returned on network errors, unexpected status codes, or deserialization failures.

Custom Templates

You can easily use your own templates. Dependencies are minimized, and helpers like include_ref are no longer needed because $ref is pre-resolved.

use mandolin;
use openapiv3::OpenAPI;
use std::fs;

fn main() {
    let f = fs::File::open("./openapi/openapi.json").unwrap();
    let api: OpenAPI = mandolin::openapi_loader::load(f, "openapi.json").unwrap();
    
    let mut env = mandolin::environment(api).unwrap();
    
    // Add your custom template
    let content = fs::read_to_string("./my_templates/custom_rust.template").unwrap();
    env.add_template("CUSTOM_RUST", &content).unwrap();

    let output = env.get_template("CUSTOM_RUST").unwrap().render(0).unwrap();
    fs::write("server.rs", output).unwrap();
}

Version History

  • 0.4.5

    • Removed blanket impl<T: ApiInterface + Sync> ApiInterfaceAxum for T. Users must now explicitly implement ApiInterfaceAxum (empty impl is fine) to use axum_router. This allows overriding authorize and per-operation methods.
  • 0.4.2-alpha.1

    • Added ApiClient to generated code: implements ApiInterface using reqwest, enabled via mandolin_client feature.
    • Added Error(String) variant to all generated response enums for network/deserialization errors.
    • ApiInterface is now fully framework-agnostic (no axum dependency).
    • Axum-specific logic moved to ApiInterfaceAxum (server-only).
    • authorize moved from ApiInterface to ApiInterfaceAxum.
    • IntoResponse implemented per response enum (replaces inline match in router).
  • 0.4.0-alpha.6

    • Added yaml optional feature (disabled by default). YAML input support via serde_yaml is now opt-in.
    • Added openapi_loader module: mandolin::openapi_load(reader) and mandolin::openapi_parse_str(s) as top-level helpers.
    • When the yaml feature is enabled, YAML parsing is attempted first and falls back to JSON on failure.
  • 0.4.0-alpha.1

    • Major Re-architecture: "Logic in Templates".
    • Moved logic from Rust to templates.
    • $ref is now pre-resolved in Rust.
    • Templates are consolidated into single files (no more include hell).
    • TypeScript (Hono) support improved.
  • 0.2.5

    • Improve rust_axum.template to correctly set Content-Type header
  • 0.2.4

    • Internal bug fixes and improvements to response handling
  • 0.2.3 add binary target

  • 0.2.2 Fix bugs about no content response

  • 0.2.1 Add impl AsRef<axum::http::Requestaxum::body::Body> for Requests

  • 0.2.0

    • support parse multipart/form-data
    • support catch-all path arguments
  • 0.1.13 support date schema

  • 0.1.12 add target "TYPESCRIPT_HONO"

  • 0.1.0 publish

My favorite mandolin music