mandolin

Crates.iomandolin
lib.rsmandolin
version0.2.5
created_at2024-12-11 11:44:42.444228+00
updated_at2026-01-18 18:01:46.733587+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
size92,846
Satoshi Misumi (lzpel)

documentation

README

mandolin: Strongly Typed Rust Code Generator from OpenAPI

GitHub License Crates.io

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

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

What is this.

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

Getting started

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

You can also use mandolin as library

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();
}

Example of generated code







// 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();
}

Running the generated server with your implementation

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.

img


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();
}

version

  • 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
    • update README.md
    • fix many bugs.
    • support parse multipart/form-data
    • support catch-all path arguments like /files/{file_path} in axum
  • 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: 137

cargo fmt