#文档 #OpenAPI #REST API #Web #编译时 #hypers

hypers_openapi

为 hypers 生成的 OpenAPI 文档

18 个版本 (6 个破坏性更新)

0.13.0 2024 年 8 月 9 日
0.12.3 2024 年 7 月 12 日
0.11.1 2024 年 7 月 20 日
0.0.0 2023 年 12 月 3 日
0.0.0-alpha2023 年 11 月 25 日

#725Web 编程

Download history 126/week @ 2024-04-22 20/week @ 2024-04-29 132/week @ 2024-05-06 8/week @ 2024-05-13 164/week @ 2024-05-20 217/week @ 2024-05-27 187/week @ 2024-06-03 308/week @ 2024-06-10 12/week @ 2024-06-17 131/week @ 2024-06-24 452/week @ 2024-07-01 200/week @ 2024-07-08 75/week @ 2024-07-15 17/week @ 2024-07-22 135/week @ 2024-07-29 100/week @ 2024-08-05

每月 339 次下载
用于 hypers

Apache-2.0

4MB
7K SLoC

示例

hypers_rbatis_admin

⚡️ 快速开始

Cargo.toml

[dependencies]
hypers = { version = "0.13", features = ["full","openapi","debug"] }
tokio = { version = "=1.38.0", features = ["full"] }
serde = { version = "=1.0.205", features = ["derive"] }

Rust 代码

use hypers::{hyper::StatusCode, once_cell::sync::Lazy, prelude::*};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;

static STORE: Lazy<Db> = Lazy::new(new_store);
pub type Db = Mutex<Vec<Todo>>;

pub fn new_store() -> Db {
    Mutex::new(Vec::new())
}

#[derive(Serialize, Deserialize, Clone, Debug, ToSchema)]
pub struct Todo {
    #[hypers(schema(example = 1))]
    pub id: u64,
    #[hypers(schema(example = "Buy coffee"))]
    pub text: String,
    pub completed: bool,
}

#[derive(Clone)]
struct Api;
#[openapi(name = "/api", tag = "api1 todos")]
impl Api {
    /// List todos.
    #[get(
        "/list_todos",
        parameter(
            ("offset", description = "Offset is an query paramter."),
            ("limit", description = "Offset is an query paramter."),
        )
    )]
    async fn list_todos(offset: Query<usize>, limit: Query<usize>) -> Json<Vec<Todo>> {
        let todos = STORE.lock().await;
        let todos: Vec<Todo> = todos
            .clone()
            .into_iter()
            .skip(offset.0)
            .take(limit.0)
            .collect();
        Json(todos)
    }
    /// Create new todo.
    #[post("/create_todo", status(201, 409))]
    async fn create_todo(req: Json<Todo>) -> Result<StatusCode, StatusError> {
        let mut vec = STORE.lock().await;
        for todo in vec.iter() {
            if todo.id == req.id {
                return Err(StatusError::bad_request().detail("todo already exists"));
            }
        }
        vec.push(req.0);
        Ok(StatusCode::CREATED)
    }
}

#[derive(Clone)]
struct Base;
#[openapi(tag = "api2 todos")]
impl Base {
    /// Update existing todo.
    #[patch("/update_todo/:id", status(200, 404))]
    async fn update_todo(id: Path<u64>, updated: Json<Todo>) -> Result<StatusCode, StatusError> {
        let mut vec = STORE.lock().await;
        for todo in vec.iter_mut() {
            if todo.id == *id {
                *todo = (*updated).clone();
                return Ok(StatusCode::OK);
            }
        }
        Err(StatusError::not_found())
    }
    #[delete("/:id", status(200, 401, 404))]
    async fn delete_todo(id: Path<u64>) -> Result<StatusCode, StatusError> {
        let mut vec = STORE.lock().await;
        let len = vec.len();
        vec.retain(|todo| todo.id != *id);
        let deleted = vec.len() != len;
        if deleted {
            Ok(StatusCode::NO_CONTENT)
        } else {
            Err(StatusError::not_found())
        }
    }
    #[post("/upload")]
    async fn upload(file: FilePart) -> Response {
        let mut res = Response::default();
        let dest = format!("temp/{}", file.name.clone().unwrap_or("file".to_owned()));
        println!("{dest}");
        let info = if let Err(e) = std::fs::copy(file.path.clone(), std::path::Path::new(&dest)) {
            res.status(StatusCode::INTERNAL_SERVER_ERROR);
            format!("file not found in request: {e}")
        } else {
            format!("File uploaded to {dest}")
        };
        res.render(Text::Plain(info))
    }
    #[post("/uploads")]
    async fn uploads(files: FileParts) -> Response {
        let mut msgs = Vec::with_capacity(files.len());
        let mut res = Response::default();
        for file in files.0 {
            let dest = format!("temp/{}", file.name.clone().unwrap_or("file".to_owned()));
            if let Err(e) = std::fs::copy(file.path.clone(), std::path::Path::new(&dest)) {
                res.status(StatusCode::INTERNAL_SERVER_ERROR)
                    .body(format!("file not found in request: {e}"));
                return res;
            } else {
                msgs.push(dest);
            }
        }
        res.body(format!("Files uploaded:\n\n{}", msgs.join("\n")));
        return res;
    }
}

pub async fn upload(_: Request) -> impl Responder {
    Text::Html(
        r#"<!DOCTYPE html>
            <html>
                <head>
                    <title>Upload file</title>
                </head>
                <body>
                    <h1>Upload file</h1>
                    <form action="/upload" method="post" enctype="multipart/form-data">
                        <input type="file" name="file" />
                        <input type="submit" value="upload" />
                    </form>
                </body>
            </html>
        "#,
    )
}

pub async fn uploads(_: Request) -> Response {
    let res = Response::default();
    res.render(Text::Html(
        r#"<!DOCTYPE html>
            <html>
                <head>
                    <title>Upload files</title>
                </head>
                <body>
                    <h1>Upload files</h1>
                    <form action="/uploads" method="post" enctype="multipart/form-data">
                        <input type="file" name="files" multiple/>
                        <input type="submit" value="upload" />
                    </form>
                </body>
            </html>
        "#,
    ))
}

pub async fn index(_: Request) -> impl Responder {
    Text::Html(
        r#"<!DOCTYPE html>
            <html>
                <head>
                    <title>Oapi todos</title>
                </head>
                <body>
                    <ul>
                    <li><a href="swagger_ui/" target="_blank">swagger_ui</a></li>
                    <li><a href="scalar" target="_blank">scalar</a></li>
                    <li><a href="rapidoc" target="_blank">rapidoc</a></li>
                    <li><a href="redoc" target="_blank">redoc</a></li>
                    </ul>
                </body>
            </html>
        "#,
    )
}
const USER_NAME: &str = "admin";
const PASS_WORD: &str = "123456";
const LOGIN_HTML: &str = r#"<!DOCTYPE html>
<html>
    <head>
        <title>swagger-ui login</title>
    </head>
    <style>
        html,body{
            margin:0;
            padding:0;
            width:100%;
            height:100%;
        }
        .container{
            display:flex;
            align-item:center;
            justify-content:center;
        }
        .form{
            display:flex;
            align-item:center;
            justify-content:center;
            flex-direction:column;
        }
        .mt-20{
            margin-top: 20px;
        }

    </style>
    <body class="container">
        <form class="form" action="/swaggerLogin" method="post">
            <h1>swagger-ui</h1>
            <input type="text" name="username" placeholder="用户名" />
            <input class="mt-20" type="password" name="password" placeholder="密码" />
            <button class="mt-20" type="submit" id="submit">登录</button>
        </form>
    </body>
</html>
"#;

#[hook]
pub async fn auth_token(req: Request, next: Next<'_>) -> impl Responder {
    let res = Response::default();
    if let Some(session) = req.session() {
        let username = session.get::<String>("username");
        let password = session.get::<String>("password");
        println!("username = {:?}", username);
        println!("password = {:?}", password);
        return next.next(req).await;
    } else {
        return res.render(Text::Html(LOGIN_HTML));
    }
}

pub async fn swagger_login(mut req: Request) -> impl Responder {
    let username = req.form::<String>("username").await;
    let password = req.form::<String>("password").await;
    let mut res = Response::default();
    if let (Some(name), Some(pass)) = (username, password) {
        if name.eq(USER_NAME) && pass.eq(PASS_WORD) {
            let mut session = Session::new();
            let _ = session.insert("username", name);
            let _ = session.insert("password", pass);
            res.set_session(session);
            res.redirect(StatusCode::SEE_OTHER, "/swagger_ui/");
            return res;
        }
    }
    res.render(Text::Html(LOGIN_HTML))
}

#[tokio::main]
async fn main() -> Result<()> {
    tracing_subscriber::fmt().init();
    std::fs::create_dir_all("temp").unwrap();
    let mut root = Router::default();
    let session_hook = SessionHook::new(
        CookieStore::new(),
        b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
    )?;
    root.hook(session_hook, None, None);
    root.hook(auth_token, vec!["/swagger_ui/"], vec!["/swaggerLogin"]);

    root.get("/", index); // http://127.0.0.1:7878/
    root.get("/upload", upload);
    root.get("/uploads", uploads);
    root.post("/swaggerLogin", swagger_login);

    let mut openapi = OpenApi::new("todos api", "0.0.1");
    openapi.push(Api);
    openapi.push(Base);

    let swagger = SwaggerUi::new("/api-doc/openapi.json");
    root.get("/swagger_ui/*", swagger); // http://127.0.0.1:7878/swagger_ui/

    let rapidoc = RapiDoc::new("/api-doc/openapi.json");
    root.get("/rapidoc", rapidoc); // http://127.0.0.1:7878/rapidoc

    let redoc = ReDoc::new("/api-doc/openapi.json");
    root.get("/redoc", redoc); // http://127.0.0.1:7878/redoc

    let scalar: Scalar = Scalar::new("/api-doc/openapi.json");
    root.get("/scalar", scalar); // http://127.0.0.1:7878/scalar

    let root = openapi.openapi("/api-doc/openapi.json", root);
    println!("root = {:#?}", root);
    let listener = hypers::TcpListener::bind("127.0.0.1:7878").await?;
    hypers::listen(root, listener).await
}

依赖项

~19–32MB
~565K SLoC