Crates.io | mandolin |
lib.rs | mandolin |
version | 0.1.14-alpha.2 |
created_at | 2024-12-11 11:44:42.444228+00 |
updated_at | 2025-08-26 10:41:05.686255+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 | 79,249 |
Input openapi.json/yaml, output one server code in rust or typescript.
Online demo with wasm: https://lzpel.github.io/mandolin/
Generate one server code from openapi specification and jinja2 templates.
Currently, mandolin provide 2 builtin templates for following frameworks
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();
}
mandolin-cli
.pub fn new(api: OpenAPI) -> Result<Self, serde_yaml::Error>
into pub fn new(api: OpenAPI) -> Self