Author: Sun Li
directory
- Introduction to the
- Quick start
- The base type
- Object type
- Define the API
- Custom request
- Custom response
- File upload
- Parameter calibration
- certification
- conclusion
- Useful links
Hi, I’m Lao Tiao, a coder who loves the Rust language. Last month, I decided to develop a new Web frame Poem. When the whole frame is basically formed, I think I should add some useful functions that other frames don’t have, so I develop Poem-openapi.
Introduction to the
The OpenAPI specification defines a standard and language-neutral interface for RESTful apis that allows humans and computers to discover and understand the capabilities of services without accessing source code, documentation, or network traffic checks. Callers can easily understand and interact with remote services, and provide useful tools such as Swagger UI (browsing test testing APIS in web pages), Swagger CodeGen (generating client SDKS in multiple languages).
Poems – OpenAPI is the openAPI server framework based on POEMS.
In general, if you want your API to support the specification, you need to create an interface definition file first, and then write the corresponding code according to the interface definition. Or after creating the interface definition file, use Swagger CodeGen to generate the server-side code framework. However, POEMS -OpenAPI is different from these two methods. It allows you to write Rust business code and use process macros to automatically generate openAPI interface and interface definition file (which is equivalent to interface documentation), much like my other open source library Async-GraphQL. OpenAPI and GraphQL are complementary and suitable for different scenarios.
Some of us may find macros scary, as they can make code difficult to understand, but I think that process macros can greatly improve our development efficiency if implemented in the right way. Therefore, the implementation of poems -openapi process macros follows the following principles:
- You will never directly use anything generated by procedure macros. (Because the IDE doesn’t recognize the code generated by procedure macros, they can have annoying red underscores if used directly, and autocomplete doesn’t work, turning the IDE into a text editor.)
- If your code does not compile, then your interface does not comply with the OpenAPI specification. (Try to expose all problems at compile time.)
- Don’t invent your own DSL.If my code can’t be
Rustfmt
Formatting, which annoys me a lot) - No extra overhead.(You can do it by hand
OpenAPI
Standard interface, but usually without any improvement in execution efficiency)
Quick start
In this example, we define an API with path /hello that takes a URL parameter named name and returns a string as the response. The name argument is of type Option
, which means it is an optional argument.
After running the following code, open http://localhost:3000 in a browser to see the Swagger UI, which you can use to browse through the API definitions and test them.
use poem::{listener::TcpListener, route};
use poem_openapi::{payload::PlainText, OpenApi, OpenApiService};
struct Api;
#[OpenApi]
impl Api {
#[oai(path = "/hello", method = "get")]
async fn index(&self.#[oai(name = "name", in = "query")] name: Option<String>, // in="query" indicates that this parameter is from the Url
) -> PlainText<String> { // PlainText is the response Type, which indicates that the API's response Type is a string, and the content-type is' text/plain '
match name {
Some(name) => PlainText(format!("hello, {}!", name)),
None => PlainText("hello!".to_string()),
}
}
}
#[tokio::main]
async fn main() - >Result<(), std::io::Error> {
// Create a TCP listener
let listener = TcpListener::bind("127.0.0.1:3000");
// Create an API service
let api_service = OpenApiService::new(Api)
.title("Hello World")
.server("http://localhost:3000/api");
// Open Swagger UI
let ui = api_service.swagger_ui("http://localhost:3000");
// Start the server and specify that the API root path is/API and the Swagger UI path is /
poem::Server::new(listener)
.await?
.run(route().nest("/api", api_service).nest("/", ui))
.await
}
Copy the code
This is an example of poem-open API, so you can also verify this by simply executing the following command:
git clone https://github.com/poem-web/poem
cargo run --bin example-openapi-hello-world
Copy the code
The base type
The base type can be the parameters of the request, the content of the request, or the content of the request response. Poems defines a Type trait. The types that implement this trait are base types. They provide some information about the Type at runtime to generate the interface definition file.
Poems implements Typetraits for most common types. You can use them directly and also customize new types, but you need to know some Json Schema (this is not difficult, I only know some simple usage of Json Schema before writing this library. Did not have a thorough understanding).
The following table shows the Rust data types that correspond to the data types in Json Schema (just a few) :
Json Schema | Rust |
---|---|
{type: "integer", format: "int32"} |
i32 |
{type: "integer", format: "float32"} |
f32 |
{type: "string" } |
String, &str |
{type: "string", format: "binary" } |
Binary |
{type: "string", format: "bytes" } |
Base64 |
{type: "array" } |
Vec |
Object type
To define an Object with the procedure macro Object, the members of the Object must be types that implement the Type trait (unless you annotate it with #[oai(skip)], then ignore this field and use the default values for serialization and deserialization instead).
The following code defines an object type that contains four fields, one of which is an enumerated type.
The object type is also one of the basic types, which is also implementedType trait
, so it can also be a member of another object.
use poem_api::{Object, Enum};
#[derive(Enum)]
enum PetStatus {
Available,
Pending,
Sold,
}
#[derive(Object)]
struct Pet {
id: u64,
name: String,
photo_urls: Vec<String>,
status: PetStatus,
}
Copy the code
Define the API
The following defines a set of apis to add, delete, change and check the pet table.
Add_pet and update_pet are used to add and update Pet objects. This is the basic Type we defined earlier. The basic Type cannot be used as request Content directly, but you need to wrap it with a Payload Type so that you can determine the Content’s content-type. In the following example, we wrap it with payload::Json, indicating that the content-type of the two API requests is Application/Json.
Find_pet_by_id and find_pets_by_status are used to find Pet objects. Their response is also a Pet object, which also needs to be wrapped as Payload.
We can use #[oai(name = “…”, in = “…”)] to specify the source of the function value. The value of in can be query, path, header, or cookie. The id parameter of delete_pet is extracted from the path, and the find_pet_by_id and find_pets_by_status parameters are obtained from the Query. If the parameter type is not Option
, it indicates that the parameter is not an optional parameter and a 400 Bad Request error will be returned if extraction fails.
You can define multiple function parameters, but you can only have one Payload type as the content of the request, or multiple primitive types as the parameters of the request.
use poem_api::{
OpenApi,
poem_api::payload::Json,
};
use poem::Result;
struct Api;
#[OpenApi]
impl Api {
// add a new Pet
#[oai(path = "/pet", method = "post")]
async fn add_pet(&self, pet: Json<Pet>) -> Result<()> {
todo!()
}
// update existing Pet
#[oai(path = "/pet", method = "put")]
async fn update_pet(&self, pet: Json<Pet>) -> Result<()> {
todo!()
}
// delete a Pet
#[oai(path = "/pet/:pet_id", method = "delete")]
async fn delete_pet(&self.#[oai(name = "pet_id", in = "path")] id: u64) - >Result<()> {
todo!()
}
// query Pet by ID
#[oai(path = "/pet/:pet_id", method = "delete")]
async fn find_pet_by_id(&self.#[oai(name = "status", in = "query")] id: u64) - >Result<Json<Pet>> {
todo!()
}
// query Pet by state
#[oai(path = "/pet/findByStatus", method = "delete")]
async fn find_pets_by_status(&self.#[oai(name = "status", in = "query")] status: Status) -> Result<Json<Vec<Pet>>> { todo! ()}}Copy the code
Custom request
The OpenAPI specification allows the same interface to handle requests of different content-types. For example, an interface can accept both application/ JSON and Text /plain Payload. You can handle requests of different content-types.
In POem-Open API, to support this type of request, you need to customize a request object that implements the Payload trait with the ApiRequest macro.
The create_POST function accepts the CreatePostRequest request and returns the ID when it is successfully created.
use poem_open::{
ApiRequest, Object,
payload::{PlainText, Json},
};
use poem::Result;
#[derive(Object)]
struct Post {
title: String,
content: String,}#[derive(ApiRequest)]
enum CreatePostRequest {
/// create from JSON
Json(Json<Blog>),
/// create from text
Text(PlainText<String>),}struct Api;
#[OpenApi]
impl Api {
#[oai(path = "/hello", method = "post")]
async fn create_post(&self,
req: CreatePostRequest,
) -> Result<Json<u64> > {// Handle them according to the content-type
matchreq { CreatePostRequest::Json(Json(blog)) => { todo! (a); } CreatePostRequest::Text(content) => { todo! (a); }}}}Copy the code
Custom response
In the previous example, all our request handlers return a Result poem::Error, which contains the cause of the Error and a status code. However, the OpenAPI specification allows for a more detailed description of the response to the request, such as what status codes might be returned by the interface, the reason for the status codes and the content of the response.
Next we change the return value of create_POST to CreateBlogResponse.
Ok, Forbidden, and InternalError describe the response type for a particular status code.
use poem_openapi::ApiResponse;
use poem::http::StatusCode;
#[derive(ApiResponse)]
enum CreateBlogResponse {
// The creation is complete
#[oai(status = 200)]
Ok(Json<u64>),
/// No permissions
#[oai(status = 403)]
Forbidden,
/// Internal error
#[oai(status = 500)]
InternalError,
}
struct Api;
#[OpenApi]
impl Api {
#[oai(path = "/hello", method = "get")]
async fn create_post(&self,
req: CreatePostRequest,
) -> CreateBlogResponse {
matchreq { CreatePostRequest::Json(Json(blog)) => { todo! (a); } CreatePostRequest::Text(content) => { todo! (a); }}}}Copy the code
By default, 400 Bad Request errors are returned when the Request fails to resolve, but sometimes we want to return a custom error. We can set an error handler using the bad_request_handler attribute. Equesterror this function is used to convert ParseRequestError to the response type specified.
use poem_openapi::{
ApiResponse, Object, ParseRequestError, payload::Json,
};
#[derive(Object)]
struct ErrorMessage {
code: i32,
reason: String,}#[derive(ApiResponse)]
#[oai(bad_request_handler = "bad_request_handler")]
enum CreateBlogResponse {
// The creation is complete
#[oai(status = 200)]
Ok(Json<u64>),
/// No permissions
#[oai(status = 403)]
Forbidden,
/// Internal error
#[oai(status = 500)]
InternalError,
/// The request is invalid
#[oai(status = 400)]
BadRequest(Json<ErrorMessage>),
}
fn bad_request_handler(err: ParseRequestError) -> CreateBlogResponse {
// When the parse request fails, a custom error content is returned, which is a JSON
CreateBlogResponse::BadRequest(ErrorMessage {
code: -1,
reason: err.to_string(),
})
}
Copy the code
File upload
Multipart is commonly used for file uploads and can define a form to contain one or more files along with additional fields. The following example provides an interface to create a Pet object, which uploads some image files while creating the Pet object.
use poem_openapi::{Multipart, OpenApi}
use poem::Result;
#[derive(Debug, Multipart)]
struct CreatePetPayload {
name: String,
status: PetStatus,
protos: Vec<Upload>, // Multiple photo files
}
struct Api;
#[OpenApi]
impl Api {
#[oai(path = "/pet", method = "post")]
async fn create_pet(&self, payload: CreatePetPayload) -> Result<Json<u64>> { todo! ()}}Copy the code
See the examples for the complete code.
Parameter calibration
OpenAPI references Json Schema validation specifications, and POEMS – OpenAPI also supports them. You can apply validators to the parameters of the request, the members of the object and the fields of the Multipart. The verifier is type safe. If the data type to be verified does not match what the verifier requires, it will fail to compile. For example, maximum can only be used for numeric types, and max_items can only be used for array types. Refer to the documentation for more validators.
use poem_openapi::{Object, OpenApi, Multipart};
#[derive(Object)]
struct Pet {
id: u64.The name cannot exceed 32 characters
#[oai(max_length = "32")]
name: String.// The array length cannot exceed 3
#[oai(max_items = "3")]
photo_urls: Vec<String>,
status: PetStatus,
}
Copy the code
certification
The OpenApi specification defines five authentication modes, namely APIKey, BASIC, bearer, OAuth2 and openIdConnect, which describe the authentication parameters required by the specified API interface.
Note: the main use of API authentication information is to enableSwagger UI
The authentication process was performed correctly when testing the API.
The following example is a Github login that provides an interface to get all the common repository information.
use poem_openapi::{
SecurityScheme, SecurityScope, OpenApi,
auth::Bearer,
};
#[derive(OAuthScopes)]
enum GithubScope {
/// access to public repositories.
#[oai(rename = "public_repo")]
PublicRepo,
/// access to read a user's profile data.
#[oai(rename = "read:user")]
ReadUser,
}
/// Github authorization
#[derive(SecurityScheme)]
#[oai(
type = "oauth2",
flows(authorization_code(
authorization_url = "https://github.com/login/oauth/authorize",
token_url = "https://github.com/login/oauth/token",
scopes = "GithubScope")))]
struct GithubAuthorization(Bearer);
struct Api;
#[OpenApi]
impl Api {
#[oai(path = "/repo", method = "get")]
async fn repo_list(&self.#[oai(auth("GithubScope::PublicRepo"))) auth: GithubAuthorization,
) -> Result<PlainText<String> > {// Use the GithubAuthorization token to obtain the required data from Githubtodo! ()}}Copy the code
See the examples for the complete code.
conclusion
By the time you get to this point, you will have mastered most of the uses of Poem-openapi. It is much more convenient to use it as an API than a generic Web framework called Poem. Besides, it is not a separate framework of Poem, so you can easily reuse existing components such as extractors and middleware.
Useful links
- The Poem warehouse
- Poem document
- Poem – openapi document
- Swagger website
- OpenAPI specification