| Crates.io | mandolin |
| lib.rs | mandolin |
| version | 0.2.5 |
| created_at | 2024-12-11 11:44:42.444228+00 |
| updated_at | 2026-01-18 18:01:46.733587+00 |
| description | Input openapi.json/yaml, output server source code in rust. |
| homepage | https://lzpel.github.io/mandolin/ |
| repository | https://github.com/lzpel/mandolin |
| max_upload_size | |
| id | 1480001 |
| size | 92,846 |
Input openapi.json/yaml, output one server code in rust.
Online demo with wasm: https://lzpel.github.io/mandolin/
Generate one complete code for server from openapi specification and jinja2 templates.
You can implement actual behavior of api handler with generated types and traits.
Currently, mandolin provide 1 builtin templates for following frameworks
Install mandolin
$ cargo install mandolin
Render axum server code using builtin "RUST_AXUM" template
$ mandolin -i ./openapi/openapi_plant.yaml -t RUST_AXUM -o ./examples/readme_axum_generate_out.rs
Render axum server code using builtin "RUST_AXUM" template
use mandolin;
use serde_yaml;
fn main() {
// read openapi.yaml
let input_openapi_path = std::env::args()
.nth(1)
.unwrap_or_else(|| "./openapi/openapi_plant.yaml".to_string());
let input_string = std::fs::read_to_string(input_openapi_path).unwrap();
let input_api = serde_yaml::from_str(&input_string.as_str()).unwrap();
// make environment
let env = mandolin::environment(input_api).unwrap();
// write the rendered output
let output = env.get_template("RUST_AXUM").unwrap().render(0).unwrap();
std::fs::write("examples/readme_axum_generate_out.rs", output).unwrap();
}
// This is generated by mandolin https://github.com/lzpel/mandolin from OpenApi specification
/* Cargo.toml for build this server
[dependencies]
serde= { version="*", features = ["derive"] }
serde_json= "*"
axum = { version = "*", features = ["multipart"] }
tokio = { version = "*", features = ["rt", "rt-multi-thread", "macros", "signal"] }
# optional
uuid = { version = "*", features = ["serde"] }
chrono = { version = "*", features = ["serde"] }
*/
use std::collections::HashMap;
use serde;
use std::future::Future;
pub trait ApiInterface{
fn authorize(&self, _req: axum::http::Request<axum::body::Body>) -> impl Future<Output = Result<AuthContext, String>> + Send{async { Ok(Default::default()) } }
// delete /clean
fn operation_clean(&self, _req: OperationCleanRequest) -> impl Future<Output = OperationCleanResponse> + Send{async{Default::default()}}
// get /credential/iam
fn credential_get(&self, _req: CredentialGetRequest) -> impl Future<Output = CredentialGetResponse> + Send{async{Default::default()}}
──────────────────────────────────────── 1355 lines omitted ────────────────────────────────────────
.map(|s| s.to_ascii_lowercase())
.filter(|s| !s.is_empty());
return Some(mk_origin(proto, host));
}
// 3) Host fallback
let host = headers
.get(axum::http::header::HOST)
.and_then(|h| h.to_str().ok())
.map(str::trim)
.filter(|s| !s.is_empty())?
.to_string();
Some(format!("{}://{}", guess_scheme(&host), host))
}
#[tokio::main]
async fn main() {
let port:u16 = std::env::var("PORT").unwrap_or("8080".to_string()).parse().expect("PORT should be integer");
print_axum_router(port);
let api = TestServer{};
let app = axum_router(api).layer(axum::extract::DefaultBodyLimit::disable());
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(async { tokio::signal::ctrl_c().await.unwrap() })
.await
.unwrap();
}
You can run the mock server from fn main.
You can add your implementation along with the generated trait ApiInterface like followings.
use crate::out; // generated module
pub struct TestServer {
user: String
}
impl out::ApiInterface for TestServer {
async fn device_get(&self, _req: out::DeviceGetRequest) -> out::DeviceGetResponse {
out::DeviceGetResponse::Status200(out::Device{
key: "0107411222".into(),
key_user: self.user.clone(),
name: "device-kix".into(),
latitude: 34.43417,
longitude: 135.23278,
})
}
}
#[tokio::main]
async fn main() {
let port: u16 = std::env::var("PORT")
.unwrap_or("8080".to_string())
.parse()
.expect("PORT should be integer");
out::print_axum_router(port);
let api = TestServer {
user: "lzpel".into()
};
let app = out::axum_router(api).layer(axum::extract::DefaultBodyLimit::disable());
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}"))
.await
.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(async { tokio::signal::ctrl_c().await.unwrap() })
.await
.unwrap();
}
You can run your server with generated out.rs and your main.rs like above code. You can check your api serves as you impleted.
$ cargo run
http://localhost:8080/api/ui
$ curl http://localhost:8080/api/device/anyid
{
"key": "0107411222",
"key_user": "lzpel",
"name": "device-kix",
"latitude": 34.43417,
"longitude": 135.23278
}
Open the output uri http://localhost:8080/api/ui show following swagger-ui page.

Render axum server source code using your custom jinja2 template.
use mandolin;
use serde_yaml;
use std::fs;
fn main() {
// read openapi.yaml
let input_api = serde_yaml::from_str(
fs::read_to_string("./openapi/openapi.yaml")
.unwrap()
.as_str(),
)
.unwrap();
let mut env = mandolin::environment(input_api).unwrap();
// add your templates
let content = fs::read_to_string("./templates/rust_axum.template").unwrap();
env.add_template("RUST_AXUM", &content).unwrap();
let content = fs::read_to_string("./templates/rust_schema.template").unwrap();
env.add_template("RUST_SCHEMA", &content).unwrap();
let content = fs::read_to_string("./templates/rust_operation.template").unwrap();
env.add_template("RUST_OPERATION", &content).unwrap();
// write the rendered output
let output = env.get_template("RUST_AXUM").unwrap().render(0).unwrap();
fs::write("examples/example_axum_generated_custom.rs", output).unwrap();
}
rust_axum.template to correctly set Content-Type headermandolin-cli.pub fn new(api: OpenAPI) -> Result<Self, serde_yaml::Error> into pub fn new(api: OpenAPI) -> Self