mandolin

Crates.iomandolin
lib.rsmandolin
version0.1.14-alpha.2
created_at2024-12-11 11:44:42.444228+00
updated_at2025-08-26 10:41:05.686255+00
descriptionInput openapi.json/yaml, output server source code in rust.
homepagehttps://lzpel.github.io/mandolin/
repositoryhttps://github.com/lzpel/mandolin
max_upload_size
id1480001
size79,249
lzpel (lzpel)

documentation

README

mandolin

crates.io

Input openapi.json/yaml, output one server code in rust or typescript.

Online demo with wasm: https://lzpel.github.io/mandolin/

What is this.

Generate one server code from openapi specification and jinja2 templates.

Currently, mandolin provide 2 builtin templates for following frameworks

Getting started

Render axum server code using builtin "RUST_AXUM" template

use mandolin;
use serde_yaml;
use std::fs;
fn main() {
	// read openapi.yaml
	let input_string=fs::read_to_string("./openapi/openapi_petstore.yaml").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();
	fs::write("./out/server_builtin_axum.rs", output).unwrap();
}

output typescript code ./out/server_builtin_axum.rs

#![allow(non_camel_case_types)]
#![allow(unused_variables)]

// This is generated by mandolin https://github.com/lzpel/mandolin from OpenApi specification

/* Cargo.toml for build this server

[dependencies]
serde= "*"
serde_json= "*"
axum="*"
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{
	// put /pet
	fn updatepet(request: UpdatepetRequest) -> impl Future<Output = UpdatepetResponse> + Send{async{Default::default()}}
	// post /pet
	fn addpet(request: AddpetRequest) -> impl Future<Output = AddpetResponse> + Send{async{Default::default()}}
	// get /pet/findByStatus
	fn findpetsbystatus(request: FindpetsbystatusRequest) -> impl Future<Output = FindpetsbystatusResponse> + Send{async{Default::default()}}
	// get /pet/findByTags
...
use axum;
pub fn axum_router_operations<S: ApiInterface + Sync + Send + 'static>(instance :std::sync::Arc<S>)->axum::Router{
	let router = axum::Router::new();
	let i = instance.clone();
	let router = router.route("/pet", axum::routing::put(|
			path: axum::extract::Path<HashMap<String,String>>,
			query: axum::extract::Query<HashMap<String,String>>,
			header: axum::http::HeaderMap,
			body: axum::body::Bytes,
		| async move{
			let ret=S::updatepet(i.as_ref(), UpdatepetRequest{
				body:match serde_json::from_slice(body.to_vec().as_slice()){Ok(v)=>v,Err(v) => { return (axum::http::StatusCode::INTERNAL_SERVER_ERROR,[(axum::http::header::CONTENT_TYPE, "text/plain")], format!("{:?}", v).as_bytes().to_vec())}},
			}).await;
			match ret{
				UpdatepetResponse::Status200(v)=> (axum::http::StatusCode::from_u16(200).unwrap(),[(axum::http::header::CONTENT_TYPE, "application/json")],serde_json::to_vec_pretty(&v).expect("error serialize response json")),
				UpdatepetResponse::Status400=> (axum::http::StatusCode::from_u16(400).unwrap(),[(axum::http::header::CONTENT_TYPE, "text/plain")], "Invalid ID supplied".as_bytes().to_vec()),
				UpdatepetResponse::Status404=> (axum::http::StatusCode::from_u16(404).unwrap(),[(axum::http::header::CONTENT_TYPE, "text/plain")], "Pet not found".as_bytes().to_vec()),
				UpdatepetResponse::Status422=> (axum::http::StatusCode::from_u16(422).unwrap(),[(axum::http::header::CONTENT_TYPE, "text/plain")], "Validation exception".as_bytes().to_vec()),
			}
		}));
	let i = instance.clone();
	let router = router.route("/pet", axum::routing::post(|
			path: axum::extract::Path<HashMap<String,String>>,
...
pub struct TestServer{}
impl SwaggerPetstoreOpenapi30 for TestServer{}
#[allow(dead_code)]
#[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 app = axum_router::<TestServer>().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.

use generated::ApiInterface;
pub struct YourServer{}
impl ApiInterface for YourServer{
	// get /hello
	async fn paths_hello_get(&self, _req: PathsHelloGetRequest) -> PathsHelloGetResponse{
		PathsHelloGetResponse::Status200(PathsHelloGetResponses200ContentApplicationJsonSchema{
			message: Some("Hello Mandolin".to_string()),
		})
	}
}
#[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 = YourServer{};
	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();
}

Render honojs server code using builtin "TYPESCRIPT_HONO" template

use mandolin;
use serde_yaml;
use std::fs;
fn main() {
	// read openapi.yaml
	let input_string=fs::read_to_string("./openapi/openapi_petstore.yaml").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("TYPESCRIPT_HONO").unwrap().render(0).unwrap();
	fs::write("./out/server_builtin_hono.ts", output).unwrap();
}

output typescript code out/server_builtin_hono.ts

import { Hono } from 'hono'
import { serve } from '@hono/node-server'



// This is generated by mandolin https://github.com/lzpel/mandolin from OpenApi specification
// Request of updatepet
type UpdatepetRequest = {
	body:Pet,
}
// Response of updatepet
type UpdatepetResponse =
	| { code: 200; body:Pet}
	| { code: 400;}
	| { code: 404;}
	| { code: 422;}
// Request of addpet
type AddpetRequest = {
	body:Pet,
}
...
	app.delete('/user/:username', async (c) => {
		if (implement.deleteuser===undefined)return c.text("not yet implemented", 500)
		const request: Partial<DeleteuserRequest> = {}
		{
			let username = c.req.param("username")
			if(username===undefined)return c.text("required parameter 'username' is not in 'path'", 400)
			request.username = username;
		}
		const response = await implement.deleteuser(request as DeleteuserRequest)
		switch (response.code){
			case 400:
				return c.text("Invalid username supplied")
			case 404:
				return c.text("User not found")
		}
	})
}
class TestServer implements SwaggerPetstoreOpenapi30{}
export function main(){
	const app = new Hono()
	addHonoOperations(app,new TestServer());
	serve({
		fetch: app.fetch,
		port: 3000
	}, (info) => {
		console.log(`Server is running on http://localhost:${info.port}`)
	})
}

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.as_str()).unwrap();

	let content = fs::read_to_string("./templates/rust_schema.template").unwrap();
	env.add_template("RUST_SCHEMA", content.as_str()).unwrap();

	let content = fs::read_to_string("./templates/rust_operation.template").unwrap();
	env.add_template("RUST_OPERATION", content.as_str())
		.unwrap();

	// write the rendered output
	let output = env.get_template("RUST_AXUM").unwrap().render(0).unwrap();
	fs::write("./out/server_builtin.rs", output).unwrap();
}

version

  • 0.1.13
    • support date schema {type: "string", format: "date-time" or "date"}
    • add &self argument in rust interface()
  • 0.1.12 add target "TYPESCRIPT_HONO" https://github.com/honojs/hono
  • 0.1.11 update to flatten nested schema. prepare cli-command mandolin-cli.
  • 0.1.7 hotfix
  • 0.1.6 independent from regex, tera
  • 0.1.5 fix ref filter
  • 0.1.4 replace minijinja from tera
  • 0.1.3
    • simplify mandolin::Mandolin::new pub fn new(api: OpenAPI) -> Result<Self, serde_yaml::Error> into pub fn new(api: OpenAPI) -> Self
    • remove mandolin::Mandolin::template_from_path
    • move serde_yaml(deprecated) in dependency into in dev-dependency
    • update README.md
    • add examples
    • rename mandolin::builtin into mandolin::templates
    • exclude frontend from crate
  • 0.1.0 publish

my favorite mandolin music

Commit count: 117

cargo fmt