# mandolin
<a href="https://crates.io/crates/mandolin"><img alt="crates.io" src="https://img.shields.io/crates/v/mandolin.svg?style=for-the-badge&logo=rust" height="20"/></a>
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
- axum(Rust) https://github.com/tokio-rs/axum
- hono(Typescript) https://github.com/honojs/hono
## Getting started
Render axum server code using builtin "RUST_AXUM" template
```rust:examples/example_builtin_axum.rs
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`
```rust
#![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`.
```rust
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
```rust:examples/example_builtin_hono.rs
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`
```typescript
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.
```rust:examples/example_custom.rs
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
- 月に舞う/武藤理恵 https://youtu.be/OVKkRj0di2I
- Suite Spagnola/C.Mandonico https://youtu.be/fCkcP_cuneU