< 返回版块

sunli829 发表于 2021-09-03 12:08

尽管有不少朋友已经知道我这几天在做什么,但当Poem-openapi的第一版准时完成,并且完全按照刚开始的想法正常工作时,我还是按捺不住内心的激动希望跟大家分享。

注意:Poem-openapi只支持Poem,所以你如果希望使用它,Poem是必要的依赖,而且我不会考虑支持其它的web框架。😎

据我所知这是Rust语言里第一个用过程宏来实现OpenAPI规范的库,它的工作方式和Async-graphql非常的像,以类型安全的代码来编写符合OpenAPI规范的API并自动生成文档。过程宏的使用完全IDE友好,你绝对不会直接用到过程宏生成的任何代码,避免了IDE满屏幕的红线,或者没法自动完成(我很看重这个,脱离IDE的自动完成我不会写代码)。😂

下面我以一个小例子来介绍它是如何使用的:

这是一个简单的用户管理API实现(别管它有没有什么实际价值,也别告诉我oai这个名字很奇怪,这是官方起的简称,不怪我),我们只看它如何使用。😁

每一个接口都需要定义Request和Response类型,除非它不接收或者返回任何内容。

create_user接口创建一个用户,由于它的请求对象类型是Json,所以它只支持content-typeapplication/json的请求。返回的CreateUserResponse定义了不同状态码对应的响应类型。

所有API宏描述的操作都会自动生成OpenAPI 3.0规范的文档,你可以clone仓库 https://github.com/poem-web/poem-openapi ,然后执行cargo run --example users,浏览器打开http://localhost:3000,就能看到一个非常奢华的Swagger UI(尽管我觉得它离GraphQL Playground的易用度还差得远)。😎

use std::collections::HashMap;

use poem_openapi::{payload::Json, types::Password, OpenAPI, Response, Schema, API};
use tokio::sync::Mutex;

/// Create user schema
#[derive(Debug, Schema, Clone, Eq, PartialEq)]
struct User {
    /// Id
    id: String,
    /// Name
    name: String,
    /// Password
    password: Password,
}

/// Update user schema
#[derive(Debug, Schema, Clone, Eq, PartialEq)]
struct UpdateUser {
    /// Name
    name: Option<String>,
    /// Password
    password: Option<Password>,
}

#[derive(Response)]
enum CreateUserResponse {
    /// Returns when the user is successfully created.
    #[oai(status = 200)]
    Ok,
    /// Returns when the user already exists.
    #[oai(status = 409)]
    UserAlreadyExists,
}

#[derive(Response)]
enum FindUserResponse {
    /// Return the specified user.
    #[oai(status = 200)]
    Ok(Json<User>),
    /// Return when the specified user is not found.
    #[oai(status = 404)]
    NotFound,
}

#[derive(Response)]
enum DeleteUserResponse {
    /// Returns when the user is successfully deleted.
    #[oai(status = 200)]
    Ok,
    /// Return when the specified user is not found.
    #[oai(status = 404)]
    NotFound,
}

#[derive(Response)]
enum UpdateUserResponse {
    /// Returns when the user is successfully updated.
    #[oai(status = 200)]
    Ok,
    /// Return when the specified user is not found.
    #[oai(status = 404)]
    NotFound,
}

#[derive(Default)]
struct Api {
    users: Mutex<HashMap<String, User>>,
}

#[API]
impl Api {
    /// Create a new user
    #[oai(path = "/users", method = "post", tag = "user")]
    async fn create_user(&self, user: Json<User>) -> CreateUserResponse {
        let mut users = self.users.lock().await;
        if users.contains_key(&user.0.id) {
            return CreateUserResponse::UserAlreadyExists;
        }
        users.insert(user.0.id.clone(), user.0);
        CreateUserResponse::Ok
    }

    /// Find user by id
    #[oai(path = "/users/:user_id", method = "get", tag = "user")]
    async fn find_user(
        &self,
        #[oai(name = "user_id", in = "path")] user_id: String,
    ) -> FindUserResponse {
        let users = self.users.lock().await;
        match users.get(&user_id) {
            Some(user) => FindUserResponse::Ok(Json(user.clone())),
            None => FindUserResponse::NotFound,
        }
    }

    /// Delete user by id
    #[oai(path = "/users/:user_id", method = "delete", tag = "user")]
    async fn delete_user(
        &self,
        #[oai(name = "user_id", in = "path")] user_id: String,
    ) -> DeleteUserResponse {
        let mut users = self.users.lock().await;
        match users.remove(&user_id) {
            Some(_) => DeleteUserResponse::Ok,
            None => DeleteUserResponse::NotFound,
        }
    }

    /// Update user by id
    #[oai(path = "/users/:user_id", method = "put", tag = "user")]
    async fn put_user(
        &self,
        #[oai(name = "user_id", in = "path")] user_id: String,
        update: Json<UpdateUser>,
    ) -> UpdateUserResponse {
        let mut users = self.users.lock().await;
        match users.get_mut(&user_id) {
            Some(user) => {
                if let Some(name) = update.0.name {
                    user.name = name;
                }
                if let Some(password) = update.0.password {
                    user.password = password;
                }
                UpdateUserResponse::Ok
            }
            None => UpdateUserResponse::NotFound,
        }
    }
}

#[tokio::main]
async fn main() {
    poem::Server::bind("127.0.0.1:3000")
        .await
        .unwrap()
        .run(
            OpenAPI::new(Api::default())
                .title("poem-openapi")
                .version("0.1.0")
                .server_with_description("http://localhost:3000", "localhost")
                .tag_with_description("user", "Operations about user")
                .ui_path("/"),
        )
        .await
        .unwrap();
}

要完全支持Open API规范中定义的特性还有不少功能要做,比如JsonSchema的所有校验器,认证,权限等等,如果你觉得这个库有用,并且希望能够为它贡献自己的力量,我非常欢迎!😁


Ext Link: https://github.com/poem-web/poem-openapi

评论区

写评论
作者 sunli829 2021-09-03 16:42

你这个问题提得相当得好! 没有用poem原有的Path类型原因有两点。

  1. poem基于serde的Path远比这个强大,而openapi规范无法体现
  2. Poem是一个web的基础库,而openapi生成的接口会包含一些反射信息

BTW: oai的handler确实可以复用poem的extractor,就像下面的例子

#[oai(path = "/users/:user_id", method = "get", tag = "user")]
async fn find_user(
    &self,
    #[oai(name = "user_id", in = "path")] user_id: String,
    #[oai(extractor)] poem::web::Query<String>, // 复用poem的extactor,但这些信息无法体现在自动生成的文档中
) -> FindUserResponse {
   ...
}

欢迎继续提问🙂

--
👇
songzhi: 既然是用于poem, 是不是可以把逻辑放到#[handler]那里, 或者至少可以部分重用.

poem的示例中handler写的是这样:fn hello(Path(name): Path<String>) -> String, 而poem-openapi却要这样写:

#[oai(path = "/users/:user_id", method = "get", tag = "user")]
async fn find_user(
    &self,
    #[oai(name = "user_id", in = "path")] user_id: String,
) -> FindUserResponse {
   ...
}

感觉风格不太兼容.

songzhi 2021-09-03 15:31

既然是用于poem, 是不是可以把逻辑放到#[handler]那里, 或者至少可以部分重用.

poem的示例中handler写的是这样:fn hello(Path(name): Path<String>) -> String, 而poem-openapi却要这样写:

#[oai(path = "/users/:user_id", method = "get", tag = "user")]
async fn find_user(
    &self,
    #[oai(name = "user_id", in = "path")] user_id: String,
) -> FindUserResponse {
   ...
}

感觉风格不太兼容.

1 共 2 条评论, 1 页