mirror of
https://github.com/poem-web/poem.git
synced 2026-01-24 20:08:49 +00:00
Refactor error handling
This commit is contained in:
38
.github/workflows/book.yml
vendored
38
.github/workflows/book.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: Book
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/**'
|
||||
|
||||
jobs:
|
||||
deploy_en:
|
||||
name: Deploy book on gh-pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install mdBook
|
||||
uses: peaceiris/actions-mdbook@v1
|
||||
- name: Render book
|
||||
run: |
|
||||
mdbook build -d gh-pages docs/en
|
||||
mdbook build -d gh-pages docs/zh-CN
|
||||
mkdir docs/gh-pages
|
||||
mv docs/en/gh-pages docs/gh-pages/en
|
||||
mv docs/zh-CN/gh-pages docs/gh-pages/zh-CN
|
||||
mv docs/index.html docs/gh-pages
|
||||
cp -r docs/assets docs/gh-pages/en
|
||||
cp -r docs/assets docs/gh-pages/zh-CN
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3.8.0
|
||||
with:
|
||||
emptyCommits: true
|
||||
keepFiles: false
|
||||
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
|
||||
publish_branch: gh-pages
|
||||
publish_dir: docs/gh-pages
|
||||
cname: poem.rs
|
||||
@@ -41,7 +41,6 @@ The following are cases of community use:
|
||||
|
||||
### Resources
|
||||
|
||||
- [Book](https://poem.rs)
|
||||
- [Examples](https://github.com/poem-web/poem/tree/master/examples)
|
||||
|
||||
## Contributing
|
||||
|
||||
1
docs/.gitignore
vendored
1
docs/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
book
|
||||
@@ -1,9 +0,0 @@
|
||||
[book]
|
||||
authors = ["sunli"]
|
||||
description = "Poem Book"
|
||||
src = "src"
|
||||
language = "en"
|
||||
title = "Poem Book"
|
||||
|
||||
[rust]
|
||||
edition = "2021"
|
||||
@@ -1,28 +0,0 @@
|
||||
# Poem Book
|
||||
|
||||
## Poem
|
||||
|
||||
- [Poem](poem.md)
|
||||
- [Quickstart](poem/quickstart.md)
|
||||
- [Endpoint](poem/endpoint.md)
|
||||
- [Routing](poem/routing.md)
|
||||
- [Extractors](poem/extractors.md)
|
||||
- [Responses](poem/responses.md)
|
||||
- [Handling errors](poem/handling_errors.md)
|
||||
- [Middleware](poem/middleware.md)
|
||||
- [Protocols](poem/protocols.md)
|
||||
- [Websocket](poem/protocols/websocket.md)
|
||||
- [Server-Sent Events (SSE)](poem/protocols/sse.md)
|
||||
- [Listeners](poem/listeners.md)
|
||||
- [OpenAPI](openapi.md)
|
||||
- [Quickstart](openapi/quickstart.md)
|
||||
- [Type System](openapi/type_system.md)
|
||||
- [Basic types](openapi/type_system/basic_types.md)
|
||||
- [Enum](openapi/type_system/enum.md)
|
||||
- [Object](openapi/type_system/object.md)
|
||||
- [API](openapi/api.md)
|
||||
- [Custom Request](openapi/custom_request.md)
|
||||
- [Custom Response](openapi/custom_response.md)
|
||||
- [Upload files](openapi/upload_files.md)
|
||||
- [Validators](openapi/validators.md)
|
||||
- [Authentication](openapi/authentication.md)
|
||||
@@ -1,13 +0,0 @@
|
||||
# OpenAPI
|
||||
|
||||
The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs which allows both humans
|
||||
and computers to discover and understand the capabilities of the service without access to source code, documentation, or
|
||||
through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service
|
||||
with a minimal amount of implementation logic.
|
||||
|
||||
`Poem-openapi` is a [OpenAPI](https://swagger.io/specification/) server-side framework based on `Poem`.
|
||||
|
||||
Generally, if you want your API to support the OAS, you first need to create an [OpenAPI Definitions](https://swagger.io/specification/),
|
||||
and then write the corresponding code according to the definitions, or use `Swagger CodeGen` to generate the boilerplate
|
||||
server code. But `Poem-openapi` is different from these two, it allows you to only write Rust business code and use
|
||||
procedural macros to automatically generate lots of boilerplate code that conform to the OpenAPI specification.
|
||||
@@ -1,77 +0,0 @@
|
||||
# API
|
||||
|
||||
The following defines some API operations to add, delete, modify and query the `pet` table.
|
||||
|
||||
A method represents an API operation. Use the `path` and `method` attributes to specify the path and HTTP method of the operation.
|
||||
|
||||
There can be multiple parameters for each API operation, and the following types can be used:
|
||||
|
||||
- **poem_openapi::param::Query** represents this parameter is parsed from the query string
|
||||
|
||||
- **poem_openapi::param::Header** represents this parameter is parsed from the request headers
|
||||
|
||||
- **poem_openapi::param::Path** represents this parameter is parsed from the URI path
|
||||
|
||||
- **poem_openapi::param::Cookie** represents this parameter is parsed from the cookie
|
||||
|
||||
- **poem_openapi::param::CookiePrivate** represents this parameter is parsed from the private cookie
|
||||
|
||||
- **poem_openapi::param::CookieSigned** represents this parameter is parsed from the signed cookie
|
||||
|
||||
- **poem_openapi::payload::Binary** represents a binary request payload
|
||||
|
||||
- **poem_openapi::payload::Json** represents a request payload encoded with JSON
|
||||
|
||||
- **poem_openapi::payload::PlainText** represents a utf8 string request payload
|
||||
|
||||
- **ApiRequest** parse the request payload generated by `ApiRequest` macro
|
||||
|
||||
- **SecurityScheme** parse the security scheme generated by `SecurityScheme` macro
|
||||
|
||||
- **T: FromRequest** used poem's extractors
|
||||
|
||||
The return value can be any type that implements `ApiResponse`.
|
||||
|
||||
```rust
|
||||
use poem_api::{
|
||||
OpenApi,
|
||||
poem_api::payload::Json,
|
||||
param::{Path, Query},
|
||||
};
|
||||
use poem::Result;
|
||||
|
||||
struct Api;
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
/// Add 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: Path<u64>) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Query pet by id
|
||||
#[oai(path = "/pet", method = "get")]
|
||||
async fn find_pet_by_id(&self, id: Query<u64>) -> Result<Json<Pet>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Query pets by status
|
||||
#[oai(path = "/pet/findByStatus", method = "get")]
|
||||
async fn find_pets_by_status(&self, status: Query<Status>) -> Result<Json<Vec<Pet>>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,82 +0,0 @@
|
||||
# Authentication
|
||||
|
||||
The OpenApi specification defines `apikey`, `basic`, `bearer`, `oauth2` and `openIdConnect` authentication modes, which
|
||||
describe the authentication parameters required for the specified operation.
|
||||
|
||||
The following example is to log in with `Github` and provide an operation to get all public repositories.
|
||||
|
||||
```rust
|
||||
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(scope("GithubScope::PublicRepo"))] auth: GithubAuthorization,
|
||||
) -> Result<PlainText<String>> {
|
||||
// Use the token in GithubAuthorization to obtain all public repositories from Github.
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For the complete example, please refer to [Example](https://github.com/poem-web/poem/tree/master/examples/openapi/auth-github).
|
||||
|
||||
## Check authentication information
|
||||
|
||||
You can use the `checker` attribute to specify a checker function to check the original authentication information and
|
||||
convert it to the return type of this function. This function must return `Option<T>`, and return `None` if check fails.
|
||||
|
||||
```rust
|
||||
struct User {
|
||||
username: String,
|
||||
}
|
||||
|
||||
/// ApiKey authorization
|
||||
#[derive(SecurityScheme)]
|
||||
#[oai(
|
||||
type = "api_key",
|
||||
key_name = "X-API-Key",
|
||||
in = "header",
|
||||
checker = "api_checker"
|
||||
)]
|
||||
struct MyApiKeyAuthorization(User);
|
||||
|
||||
async fn api_checker(req: &Request, api_key: ApiKey) -> Option<User> {
|
||||
let connection = req.data::<DbConnection>().unwrap();
|
||||
|
||||
// check in database
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
For the complete example, please refer to [Example](https://github.com/poem-web/poem/tree/master/examples/openapi/auth-apikey).
|
||||
@@ -1,52 +0,0 @@
|
||||
# Custom Request
|
||||
|
||||
The `OpenAPI` specification allows the same operation to support processing different requests of `Content-Type`,
|
||||
for example, an operation can support `application/json` and `text/plain` types of request content.
|
||||
|
||||
In `Poem-openapi`, to support this type of request, you need to use the `ApiRequest` macro to customize a request object
|
||||
that implements the `Payload` trait.
|
||||
|
||||
In the following example, the `create_post` function accepts the `CreatePostRequest` request, and when the creation is
|
||||
successful, it returns the `id`.
|
||||
|
||||
```rust
|
||||
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 plain text
|
||||
Text(PlainText<String>),
|
||||
}
|
||||
|
||||
struct Api;
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(path = "/hello", method = "post")]
|
||||
async fn create_post(
|
||||
&self,
|
||||
req: CreatePostRequest,
|
||||
) -> Result<Json<u64>> {
|
||||
match req {
|
||||
CreatePostRequest::Json(Json(blog)) => {
|
||||
todo!();
|
||||
}
|
||||
CreatePostRequest::Text(content) => {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,94 +0,0 @@
|
||||
# Custom Response
|
||||
|
||||
In all the previous examples, all operations return `Result`. When an error occurs, a `poem::Error` is returned, which
|
||||
contains the reason and status code of the error. However, the `OpenAPI` specification supports a more detailed definition
|
||||
of the response of the operation, such as which status codes may be returned, and the reason for the status code and the
|
||||
content of the response.
|
||||
|
||||
In the following example, we change the return type of the `create_post` function to `CreateBlogResponse`.
|
||||
|
||||
`Ok`, `Forbidden` and `InternalError` specify the response content of a specific status code.
|
||||
|
||||
```rust
|
||||
use poem_openapi::ApiResponse;
|
||||
use poem::http::StatusCode;
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
enum CreateBlogResponse {
|
||||
/// Created successfully
|
||||
#[oai(status = 200)]
|
||||
Ok(Json<u64>),
|
||||
|
||||
/// Permission denied
|
||||
#[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 {
|
||||
match req {
|
||||
CreatePostRequest::Json(Json(blog)) => {
|
||||
todo!();
|
||||
}
|
||||
CreatePostRequest::Text(content) => {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When the parsing request fails, the default `400 Bad Request` error will be returned, but sometimes we want to return a
|
||||
custom error content, we can use the `bad_request_handler` attribute to set an error handling function, this function is
|
||||
used to convert `ParseRequestError` to specified response type.
|
||||
|
||||
```rust
|
||||
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 {
|
||||
/// Created successfully
|
||||
#[oai(status = 200)]
|
||||
Ok(Json<u64>),
|
||||
|
||||
/// Permission denied
|
||||
#[oai(status = 403)]
|
||||
Forbidden,
|
||||
|
||||
/// Internal error
|
||||
#[oai(status = 500)]
|
||||
InternalError,
|
||||
|
||||
/// Bad request
|
||||
#[oai(status = 400)]
|
||||
BadRequest(Json<ErrorMessage>),
|
||||
}
|
||||
|
||||
fn bad_request_handler(err: ParseRequestError) -> CreateBlogResponse {
|
||||
// When the parsing request fails, a custom error content is returned, which is a JSON
|
||||
CreateBlogResponse::BadRequest(Json(ErrorMessage {
|
||||
code: -1,
|
||||
reason: err.to_string(),
|
||||
}))
|
||||
}
|
||||
```
|
||||
@@ -1,63 +0,0 @@
|
||||
# Quickstart
|
||||
|
||||
In the following example, we define an API with a path of `/hello`, which accepts a URL parameter named `name` and returns
|
||||
a string as the response content. The type of the `name` parameter is `Option<String>`, which means it is an optional parameter.
|
||||
|
||||
Running the following code, open `http://localhost:3000` with a browser to see `Swagger UI`, you can use it to browse API
|
||||
definitions and test them.
|
||||
|
||||
```rust
|
||||
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" means this parameter is parsed from Url
|
||||
) -> PlainText<String> { // PlainText is the response type, which means that the response type of the API 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 API service
|
||||
let api_service = OpenApiService::new(Api, "Demo", "0.1.0")
|
||||
.title("Hello World")
|
||||
.server("http://localhost:3000/api");
|
||||
|
||||
// Enable the Swagger UI
|
||||
let ui = api_service.swagger_ui();
|
||||
|
||||
// Enable the OpenAPI specification
|
||||
let spec = api_service.spec_endpoint();
|
||||
|
||||
// Start the server and specify that the root path of the API is /api, and the path of Swagger UI is /
|
||||
poem::Server::new(listener)
|
||||
.await?
|
||||
.run(
|
||||
Route::new()
|
||||
.at("/openapi.json", spec)
|
||||
.nest("/api", api_service)
|
||||
.nest("/", ui)
|
||||
)
|
||||
.await
|
||||
}
|
||||
```
|
||||
|
||||
This is an example of `poem-openapi`, so you can also directly execute the following command to play:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/poem-web/poem
|
||||
cargo run --bin example-openapi-hello-world
|
||||
```
|
||||
@@ -1,5 +0,0 @@
|
||||
# Type System
|
||||
|
||||
|
||||
Poem-openapi implements conversions from OpenAPI types to Rust types, and it's easy to use.
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# Basic types
|
||||
|
||||
The basic type can be used as a request parameter, request content or response content. `Poem` defines a `Type` trait to
|
||||
represent a basic type, which can provide some information about the type at runtime to generate OpenAPI definitions.
|
||||
|
||||
`Poem` implements `Type` traits for most common types, you can use them directly, and you can also customize new types,
|
||||
but you need to have a certain understanding of [Json Schema](https://json-schema.org/).
|
||||
|
||||
The following table lists the Rust data types corresponding to some OpenAPI data types:
|
||||
|
||||
| Open API | Rust |
|
||||
|-----------------------------------------|-----------------------------------|
|
||||
| `{type: "integer", format: "int32" }` | i32 |
|
||||
| `{type: "integer", format: "float32" }` | f32 |
|
||||
| `{type: "bool" }` | bool |
|
||||
| `{type: "string" }` | String, &str |
|
||||
| `{type: "string", format: "binary" }` | Binary |
|
||||
| `{type: "string", format: "bytes" }` | Base64 |
|
||||
| `{type: "array" }` | Vec<T> |
|
||||
@@ -1,16 +0,0 @@
|
||||
# Enum
|
||||
|
||||
Use the procedural macro `Enum` to define an enumerated type.
|
||||
|
||||
**Poem-openapi will automatically change the name of each item to `SCREAMING_SNAKE_CASE` convention. You can use `rename_all` attribute to rename all items.**
|
||||
|
||||
```rust
|
||||
use poem_api::Enum;
|
||||
|
||||
#[derive(Enum)]
|
||||
enum PetStatus {
|
||||
Available,
|
||||
Pending,
|
||||
Sold,
|
||||
}
|
||||
```
|
||||
@@ -1,29 +0,0 @@
|
||||
# Object
|
||||
|
||||
Use the procedural macro `Object` to define an object. All object members must be types that implement the `Type trait`
|
||||
(unless you mark it with `#[oai(skip)]`, the field will be ignored serialization and use the default value instead).
|
||||
|
||||
Use the following code to define an object type, which contains four fields, one of which is an enumerated type.
|
||||
|
||||
_Object type is also a kind of basic type, it also implements the `Type` trait, so it can also be a member of another object._
|
||||
|
||||
**Poem-openapi will automatically change the name of each member to `camelCase` convention. You can use `rename_all` attribute to rename all items.**
|
||||
|
||||
```rust
|
||||
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,
|
||||
}
|
||||
```
|
||||
@@ -1,29 +0,0 @@
|
||||
# Upload files
|
||||
|
||||
The `Multipart` macro is usually used for file upload. It can define a form to contain one or more files and some
|
||||
additional fields. The following example provides an operation to create a `Pet` object, which can upload some image
|
||||
files at the same time.
|
||||
|
||||
```rust
|
||||
use poem_openapi::{Multipart, OpenApi};
|
||||
use poem::Result;
|
||||
|
||||
#[derive(Debug, Multipart)]
|
||||
struct CreatePetPayload {
|
||||
name: String,
|
||||
status: PetStatus,
|
||||
photos: Vec<Upload>, // some photos
|
||||
}
|
||||
|
||||
struct Api;
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(path = "/pet", method = "post")]
|
||||
async fn create_pet(&self, payload: CreatePetPayload) -> Result<Json<u64>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For the complete example, please refer to [Upload Example](https://github.com/poem-web/poem/tree/master/examples/openapi/upload).
|
||||
@@ -1,27 +0,0 @@
|
||||
# Validators
|
||||
|
||||
The `OpenAPI` specification supports validation based on `Json Schema`, and `Poem-openapi` also supports them. You can
|
||||
apply validators to operation parameters, object members, and `Multipart` fields. The validator can only work on specific
|
||||
data types, otherwise 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.
|
||||
|
||||
For more validators, please refer to [document](https://docs.rs/poem-openapi/*/poem_openapi/attr.OpenApi.html#operation-argument-parameters).
|
||||
|
||||
```rust
|
||||
use poem_openapi::{Object, OpenApi, Multipart};
|
||||
|
||||
#[derive(Object)]
|
||||
struct Pet {
|
||||
id: u64,
|
||||
|
||||
/// The length of the name must be less than 32
|
||||
#[oai(validator(max_length = "32"))]
|
||||
name: String,
|
||||
|
||||
/// Array length must be less than 3 and the url length must be less than 256
|
||||
#[oai(validator(max_items = "3", max_length = "256"))]
|
||||
photo_urls: Vec<String>,
|
||||
|
||||
status: PetStatus,
|
||||
}
|
||||
```
|
||||
@@ -1,4 +0,0 @@
|
||||
# Poem
|
||||
|
||||
`Poem` is a full-featured and easy-to-use web framework with the Rust programming language.
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# Endpoint
|
||||
|
||||
The endpoint can handle HTTP requests. You can implement the `Endpoint` trait to create your own endpoint.
|
||||
`Poem` also provides some convenient functions to easily create a custom endpoint type.
|
||||
|
||||
In the previous chapter, we learned how to use the `handler` macro to convert a function to an endpoint.
|
||||
|
||||
Now let's see how to create your own endpoint by implementing the `Endpoint` trait.
|
||||
|
||||
This is the definition of the `Endpoint` trait, you need to specify the type of `Output` and implement the `call` method.
|
||||
|
||||
```rust
|
||||
/// An HTTP request handler.
|
||||
#[async_trait]
|
||||
pub trait Endpoint: Send + Sync + 'static {
|
||||
/// Represents the response of the endpoint.
|
||||
type Output: IntoResponse;
|
||||
|
||||
/// Get the response to the request.
|
||||
async fn call(&self, req: Request) -> Self::Output;
|
||||
}
|
||||
```
|
||||
|
||||
Now we implement an `Endpoint`, which receives HTTP requests and outputs a string containing the request method and path.
|
||||
|
||||
The `Output` associated type must be a type that implements the `IntoResponse` trait. Poem has been implemented by most
|
||||
commonly used types.
|
||||
|
||||
Since `Endpoint` contains an asynchronous method `call`, we need to decorate it with the `async_trait` macro.
|
||||
|
||||
```rust
|
||||
struct MyEndpoint;
|
||||
|
||||
#[async_trait]
|
||||
impl Endpoint for MyEndpoint {
|
||||
type Output = String;
|
||||
|
||||
async fn call(&self, req: Request) -> Self::Output {
|
||||
format!("method={} path={}", req.method(), req.uri().path());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Create from functions
|
||||
|
||||
You can use `poem::endpoint::make` and `poem::endpoint::make_sync` to create endpoints from asynchronous functions and
|
||||
synchronous functions.
|
||||
|
||||
The following endpoint does the same thing:
|
||||
|
||||
```rust
|
||||
let ep = poem::endpoint::make(|req| async move {
|
||||
format!("method={} path={}", req.method(), req.uri().path())
|
||||
});
|
||||
```
|
||||
|
||||
## EndpointExt
|
||||
|
||||
The `EndpointExt` trait provides some convenience functions for converting the input or output of the endpoint.
|
||||
|
||||
- `EndpointExt::before` is used to convert the request.
|
||||
- `EndpointExt::after` is used to convert the output.
|
||||
- `EndpointExt::map_ok`, `EndpointExt::map_err`, `EndpointExt::and_then` are used to process the output of type `Result<T>`.
|
||||
|
||||
## Using Result type
|
||||
|
||||
`Poem` also implements `IntoResponse` for the `poem::Result<T>` type, so it can also be used as the output type of the
|
||||
endpoint, so you can use `?` in the `call` method.
|
||||
|
||||
```rust
|
||||
struct MyEndpoint;
|
||||
|
||||
#[async_trait]
|
||||
impl Endpoint for MyEndpoint {
|
||||
type Output = poem::Result<String>;
|
||||
|
||||
async fn call(&self, req: Request) -> Self::Output {
|
||||
Ok(req.take_body().into_string().await?)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use the `EndpointExt::map_to_response` method to convert the output of the endpoint to the `Response` type, or
|
||||
use the `EndpointExt::map_to_result` to convert the output to the `poem::Result<Response>` type.
|
||||
|
||||
```rust
|
||||
let ep = MyEndpoint.map_to_response() // impl Endpoint<Output = Response>
|
||||
```
|
||||
@@ -1,225 +0,0 @@
|
||||
# Extractors
|
||||
|
||||
The extractor is used to extract something from the HTTP request.
|
||||
|
||||
`Poem` provides some commonly used extractors for extracting something from HTTP requests.
|
||||
|
||||
You can use one or more extractors as the parameters of the function, up to 16.
|
||||
|
||||
In the following example, the `index` function uses 3 extractors to extract the remote address, HTTP method and URI.
|
||||
|
||||
```rust
|
||||
#[handler]
|
||||
fn index(remote_addr: SocketAddr, method: Method, uri: &Uri) {}
|
||||
```
|
||||
|
||||
# Built-in extractors
|
||||
|
||||
- **Option<T>**
|
||||
|
||||
Extracts `T` from the incoming request, returns `None` if it
|
||||
fails.
|
||||
|
||||
- **&Request**
|
||||
|
||||
Extracts the `Request` from the incoming request.
|
||||
|
||||
- **&RemoteAddr**
|
||||
|
||||
Extracts the remote peer's address [`RemoteAddr`] from request.
|
||||
|
||||
- **&LocalAddr**
|
||||
|
||||
Extracts the local server's address [`LocalAddr`] from request.
|
||||
|
||||
- **Method**
|
||||
|
||||
Extracts the `Method` from the incoming request.
|
||||
|
||||
- **Version**
|
||||
|
||||
Extracts the `Version` from the incoming request.
|
||||
|
||||
- **&Uri**
|
||||
|
||||
Extracts the `Uri` from the incoming request.
|
||||
|
||||
- **&HeaderMap**
|
||||
|
||||
Extracts the `HeaderMap` from the incoming request.
|
||||
|
||||
- **Data<&T>**
|
||||
|
||||
Extracts the `Data` from the incoming request.
|
||||
|
||||
- **TypedHeader<T>**
|
||||
|
||||
Extracts the `TypedHeader` from the incoming request.
|
||||
|
||||
- **Path<T>**
|
||||
|
||||
Extracts the `Path` from the incoming request.
|
||||
|
||||
- **Query<T>**
|
||||
|
||||
Extracts the `Query` from the incoming request.
|
||||
|
||||
- **Form<T>**
|
||||
|
||||
Extracts the `Form` from the incoming request.
|
||||
|
||||
- **Json<T>**
|
||||
|
||||
Extracts the `Json` from the incoming request.
|
||||
|
||||
_This extractor will take over the requested body, so you should avoid
|
||||
using multiple extractors of this type in one handler._
|
||||
|
||||
- **TempFile**
|
||||
|
||||
Extracts the `TempFile` from the incoming request.
|
||||
|
||||
_This extractor will take over the requested body, so you should avoid
|
||||
using multiple extractors of this type in one handler._
|
||||
|
||||
- **Multipart**
|
||||
|
||||
Extracts the `Multipart` from the incoming request.
|
||||
|
||||
_This extractor will take over the requested body, so you should avoid
|
||||
using multiple extractors of this type in one handler._
|
||||
|
||||
- **&CookieJar**
|
||||
|
||||
Extracts the `CookieJar`](cookie::CookieJar) from the incoming request.
|
||||
|
||||
_Requires `CookieJarManager` middleware._
|
||||
|
||||
- **&Session**
|
||||
|
||||
Extracts the [`Session`](crate::session::Session) from the incoming request.
|
||||
|
||||
_Requires `CookieSession` or `RedisSession` middleware._
|
||||
|
||||
- **Body**
|
||||
|
||||
Extracts the `Body` from the incoming request.
|
||||
|
||||
_This extractor will take over the requested body, so you should avoid
|
||||
using multiple extractors of this type in one handler._
|
||||
|
||||
- **String**
|
||||
|
||||
Extracts the body from the incoming request and parse it into utf8 string.
|
||||
|
||||
_This extractor will take over the requested body, so you should avoid
|
||||
using multiple extractors of this type in one handler._
|
||||
|
||||
- **Vec<u8>**
|
||||
|
||||
Extracts the body from the incoming request and collect it into
|
||||
`Vec<u8>`.
|
||||
|
||||
_This extractor will take over the requested body, so you should avoid
|
||||
using multiple extractors of this type in one handler._
|
||||
|
||||
- **Bytes**
|
||||
|
||||
Extracts the body from the incoming request and collect it into
|
||||
`Bytes`.
|
||||
|
||||
_This extractor will take over the requested body, so you should avoid
|
||||
using multiple extractors of this type in one handler._
|
||||
|
||||
- **WebSocket**
|
||||
|
||||
Ready to accept a websocket connection.
|
||||
|
||||
## Handling of extractor errors
|
||||
|
||||
By default, the extractor will return a `400 Bad Request` when an error occurs, but sometimes you may want to change
|
||||
this behavior, so you can handle the error yourself.
|
||||
|
||||
In the following example, when the `Query` extractor fails, it will return a `500 Internal Server Error` response and the reason for the error.
|
||||
|
||||
```rust
|
||||
use poem::web::Query;
|
||||
use poem::error::ParseQueryError;
|
||||
use poem::{IntoResponse, Response};
|
||||
use poem::http::StatusCode;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Params {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[handler]
|
||||
fn index(res: Result<Query<Params>, ParseQueryError>) -> Response {
|
||||
match res {
|
||||
Ok(Query(params)) => params.name.into_response(),
|
||||
Err(err) => Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(err.to_string()),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom extractor
|
||||
|
||||
You can also implement your own extractor.
|
||||
|
||||
The following is an example of a custom token extractor, which extracts the
|
||||
token from the `MyToken` header.
|
||||
|
||||
```rust
|
||||
use poem::{
|
||||
get, handler, http::StatusCode, listener::TcpListener, FromRequest, Request,
|
||||
RequestBody, Response, Route, Server,
|
||||
};
|
||||
|
||||
struct Token(String);
|
||||
|
||||
// Error type for Token extractor
|
||||
#[derive(Debug)]
|
||||
struct MissingToken;
|
||||
|
||||
/// custom-error can also be reused
|
||||
impl IntoResponse for MissingToken {
|
||||
fn into_response(self) -> Response {
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body("missing token")
|
||||
}
|
||||
}
|
||||
|
||||
// Implements a token extractor
|
||||
#[poem::async_trait]
|
||||
impl<'a> FromRequest<'a> for Token {
|
||||
type Error = MissingToken;
|
||||
|
||||
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self, Self::Error> {
|
||||
let token = req
|
||||
.headers()
|
||||
.get("MyToken")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.ok_or(MissingToken)?;
|
||||
Ok(Token(token.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn index(token: Token) {
|
||||
assert_eq!(token.0, "token123");
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
if std::env::var_os("RUST_LOG").is_none() {
|
||||
std::env::set_var("RUST_LOG", "poem=debug");
|
||||
}
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let app = Route::new().at("/", get(index));
|
||||
let listener = TcpListener::bind("127.0.0.1:3000");
|
||||
let server = Server::new(listener).await?;
|
||||
server.run(app).await
|
||||
}
|
||||
```
|
||||
@@ -1,119 +0,0 @@
|
||||
# Handling errors
|
||||
|
||||
In `Poem`, we handle errors based on the response status code. When the status code is in `400-599`, we can think that
|
||||
an error occurred while processing this request.
|
||||
|
||||
We can use `EndpointExt::after` to create a new endpoint type to customize the error response.
|
||||
|
||||
In the following example, the `after` function is used to convert the output of the `index` function and output an error
|
||||
response when an server error occurs.
|
||||
|
||||
**Note that the endpoint type generated by a `handler` macro is always `Endpoint<Output=Response>`, even if it returns
|
||||
a `Result<T>`.**
|
||||
|
||||
```rust
|
||||
use poem::{handler, Result, Error};
|
||||
use poem::http::StatusCode;
|
||||
|
||||
#[handler]
|
||||
async fn index() -> Result<()> {
|
||||
Err(Error::new(StatusCode::BAD_REQUEST))
|
||||
}
|
||||
|
||||
let ep = index.after(|resp| {
|
||||
if resp.status().is_server_error() {
|
||||
Response::builder()
|
||||
.status(resp.status())
|
||||
.body("custom error")
|
||||
} else {
|
||||
resp
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The `EndpointExt::map_to_result` function can help us convert any type of endpoint to `Endpoint<Output = Response>`, so
|
||||
that we only need to check the status code to know whether an error has occurred.
|
||||
|
||||
```rust
|
||||
use poem::endpoint::make;
|
||||
use poem::{Error, EndpointExt};
|
||||
use poem::http::StatusCode;
|
||||
|
||||
let ep = make(|_| Ok::<(), Error>(Error::new(StatusCode::new(Status::BAD_REQUEST))))
|
||||
.map_to_response();
|
||||
|
||||
let ep = ep.after(|resp| {
|
||||
if resp.status().is_server_error() {
|
||||
Response::builder()
|
||||
.status(resp.status())
|
||||
.body("custom error")
|
||||
} else {
|
||||
resp
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## poem::Error
|
||||
|
||||
`poem::Error` is a general error type, which implements `From<T: Display>`, so you can easily use the `?` operator to
|
||||
convert any error type to it. The default status code is `503 Internal Server Error`.
|
||||
|
||||
```rust
|
||||
use poem::Result;
|
||||
|
||||
#[handler]
|
||||
fn index(data: Vec<u8>) -> Result<i32> {
|
||||
let value: i32 = serde_json::from_slice(&data)?;
|
||||
Ok(value)
|
||||
}
|
||||
```
|
||||
|
||||
But sometimes we don't want to always use the `503` status code, `Poem` provides some helper functions to convert the error type.
|
||||
|
||||
```rust
|
||||
use poem::{Result, web::Json, error::BadRequest};
|
||||
|
||||
#[handler]
|
||||
fn index(data: Vec<u8>) -> Result<Json<i32>> {
|
||||
let value: i32 = serde_json::from_slice(&data).map_err(BadRequest)?;
|
||||
Ok(Json(value))
|
||||
}
|
||||
```
|
||||
|
||||
## Custom error type
|
||||
|
||||
Sometimes we can use custom error types to reduce boilerplate code.
|
||||
|
||||
NOTE: `Poem`'s error types usually only needs to implement `IntoResponse`.
|
||||
|
||||
```rust
|
||||
use poem::{
|
||||
Response,
|
||||
error::ReadBodyError,
|
||||
http::StatusCode,
|
||||
};
|
||||
|
||||
enum MyError {
|
||||
InvalidValue,
|
||||
ReadBodyError(ReadBodyError),
|
||||
}
|
||||
|
||||
impl IntoResponse for MyError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
MyError::InvalidValue => Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body("invalid value"),
|
||||
MyError::ReadBodyError(err) => err.into(), // ReadBodyError has implemented `IntoResponse`.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
fn index(data: Result<String, ReadBodyError>) -> Result<(), MyError> {
|
||||
let data = data?;
|
||||
if data.len() > 10 {
|
||||
return Err(MyError::InvalidValue);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,56 +0,0 @@
|
||||
# Listeners
|
||||
|
||||
`Poem` provides some commonly used listeners.
|
||||
|
||||
- TcpListener
|
||||
|
||||
Listens for incoming TCP connections.
|
||||
|
||||
- UnixListener
|
||||
|
||||
Listens for incoming Unix domain socket connections.
|
||||
|
||||
## TLS
|
||||
|
||||
You can call the `Listener::tls` function to wrap a listener and make it support TLS connections.
|
||||
|
||||
```rust
|
||||
let listener = TcpListener::bind("127.0.0.1:3000")
|
||||
.tls(TlsConfig::new().key(KEY).cert(CERT));
|
||||
```
|
||||
|
||||
## TLS reload
|
||||
|
||||
You can use a stream to pass the latest Tls config to `Poem`.
|
||||
|
||||
The following example loads the latest TLS config from file every 1 minute:
|
||||
|
||||
```rust
|
||||
use async_trait::async_trait;
|
||||
|
||||
fn load_tls_config() -> Result<TlsConfig, std::io::Error> {
|
||||
Ok(TlsConfig::new()
|
||||
.cert(std::fs::read("cert.pem")?)
|
||||
.key(std::fs::read("key.pem")?))
|
||||
}
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:3000")
|
||||
.tls(async_stream::stream! {
|
||||
loop {
|
||||
if let Ok(tls_config) = load_tls_config() {
|
||||
yield tls_config;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Combine multiple listeners.
|
||||
|
||||
Call `Listener::combine` to combine two listeners into one, or you can call this function multiple times to combine more listeners.
|
||||
|
||||
```rust
|
||||
let listener = TcpListener::bind("127.0.0.1:3000")
|
||||
.combine(TcpListener::bind("127.0.0.1:3001"))
|
||||
.combine(TcpListener::bind("127.0.0.1:3002"));
|
||||
```
|
||||
@@ -1,114 +0,0 @@
|
||||
# Middleware
|
||||
|
||||
The middleware can do something before or after the request is processed.
|
||||
|
||||
`Poem` provides some commonly used middleware implementations.
|
||||
|
||||
- `AddData`
|
||||
|
||||
Used to attach a status to the request, such as a token for authentication.
|
||||
|
||||
- `SetHeader`
|
||||
|
||||
Used to add some specific HTTP headers to the response.
|
||||
|
||||
- `Cors`
|
||||
|
||||
Used for Cross-Origin Resource Sharing.
|
||||
|
||||
- `Tracing`
|
||||
|
||||
Use [`tracing`](https://crates.io/crates/tracing) to record all requests and responses.
|
||||
|
||||
- `Compression`
|
||||
|
||||
Used for decompress request body and compress response body.
|
||||
|
||||
## Custom middleware
|
||||
|
||||
It is easy to implement your own middleware, you only need to implement the `Middleware` trait, which is a converter to
|
||||
convert an input endpoint to another endpoint.
|
||||
|
||||
The following example creates a custom middleware that reads the value of the HTTP request header named `X-Token` and
|
||||
adds it as the status of the request.
|
||||
|
||||
```rust
|
||||
use poem::{handler, web::Data, Endpoint, EndpointExt, Middleware, Request};
|
||||
|
||||
/// A middleware that extract token from HTTP headers.
|
||||
struct TokenMiddleware;
|
||||
|
||||
impl<E: Endpoint> Middleware<E> for TokenMiddleware {
|
||||
type Output = TokenMiddlewareImpl<E>;
|
||||
|
||||
fn transform(&self, ep: E) -> Self::Output {
|
||||
TokenMiddlewareImpl { ep }
|
||||
}
|
||||
}
|
||||
|
||||
/// The new endpoint type generated by the TokenMiddleware.
|
||||
struct TokenMiddlewareImpl<E> {
|
||||
ep: E,
|
||||
}
|
||||
|
||||
const TOKEN_HEADER: &str = "X-Token";
|
||||
|
||||
/// Token data
|
||||
struct Token(String);
|
||||
|
||||
#[poem::async_trait]
|
||||
impl<E: Endpoint> Endpoint for TokenMiddlewareImpl<E> {
|
||||
type Output = E::Output;
|
||||
|
||||
async fn call(&self, mut req: Request) -> Self::Output {
|
||||
if let Some(value) = req
|
||||
.headers()
|
||||
.get(TOKEN_HEADER)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
// Insert token data to extensions of request.
|
||||
let token = value.to_string();
|
||||
req.extensions_mut().insert(Token(token));
|
||||
}
|
||||
|
||||
// call the inner endpoint.
|
||||
self.ep.call(req).await
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn index(Data(token): Data<&Token>) -> String {
|
||||
token.0.clone()
|
||||
}
|
||||
|
||||
// Use the `TokenMiddleware` middleware to convert the `index` endpoint.
|
||||
let ep = index.with(TokenMiddleware);
|
||||
```
|
||||
|
||||
## Custom middleware with function
|
||||
|
||||
You can also use a function to implement a middleware.
|
||||
|
||||
```rust
|
||||
async fn extract_token<E: Endpoint>(next: E, mut req: Request) -> Response {
|
||||
if let Some(value) = req
|
||||
.headers()
|
||||
.get(TOKEN_HEADER)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
// Insert token data to extensions of request.
|
||||
let token = value.to_string();
|
||||
req.extensions_mut().insert(Token(token));
|
||||
}
|
||||
|
||||
// call the next endpoint.
|
||||
next.call(req).await
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn index(Data(token): Data<&Token>) -> String {
|
||||
token.0.clone()
|
||||
}
|
||||
|
||||
let ep = index.around(extract_token);
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
# Protocols
|
||||
@@ -1,28 +0,0 @@
|
||||
# Server-Sent Events (SSE)
|
||||
|
||||
SSE allows the server to continuously push data to the client.
|
||||
|
||||
You need to create a `SSE` response with a type that implements `Stream<Item=Event>`.
|
||||
|
||||
The endpoint in the example below will send three events.
|
||||
|
||||
```rust
|
||||
use futures_util::stream;
|
||||
use poem::{
|
||||
handler, Route, get,
|
||||
http::StatusCode,
|
||||
web::sse::{Event, SSE},
|
||||
Endpoint, Request,
|
||||
};
|
||||
|
||||
#[handler]
|
||||
fn index() -> SSE {
|
||||
SSE::new(stream::iter(vec![
|
||||
Event::message("a"),
|
||||
Event::message("b"),
|
||||
Event::message("c"),
|
||||
]))
|
||||
}
|
||||
|
||||
let app = Route::new().at("/", get(index));
|
||||
```
|
||||
@@ -1,32 +0,0 @@
|
||||
# Websocket
|
||||
|
||||
Websocket allows a long connection for two-way communication between the client and the server.
|
||||
|
||||
`Poem` provides a `WebSocket` extractor to create this connection.
|
||||
|
||||
When the connection is successfully upgraded, a specified closure is called to send and receive data.
|
||||
|
||||
The following example is an echo service, which always sends out the received data.
|
||||
|
||||
**Note that the output of this endpoint must be the return value of the `WebSocket::on_upgrade` function, otherwise the
|
||||
connection cannot be created correctly.**
|
||||
|
||||
```rust
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use poem::{
|
||||
handler, Route, get,
|
||||
web::websocket::{Message, WebSocket},
|
||||
IntoResponse,
|
||||
};
|
||||
|
||||
#[handler]
|
||||
async fn index(ws: WebSocket) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
if let Some(Ok(Message::Text(text))) = socket.next().await {
|
||||
let _ = socket.send(Message::Text(text)).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let app = Route::new().at("/", get(index));
|
||||
```
|
||||
@@ -1,65 +0,0 @@
|
||||
# Quickstart
|
||||
|
||||
## Add dependency libraries
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
poem = "1.0"
|
||||
serde = "1.0"
|
||||
tokio = { version = "1.12.0", features = ["rt-multi-thread", "macros"] }
|
||||
```
|
||||
|
||||
## Write a endpoint
|
||||
|
||||
The `handler` macro converts a function into a type that implements `Endpoint`, and the `Endpoint` trait represents
|
||||
a type that can handle HTTP requests.
|
||||
|
||||
This function can receive one or more parameters, and each parameter is an extractor that can extract something from
|
||||
the HTTP request.
|
||||
|
||||
The extractor implements the `FromRequest` trait, and you can also implement this trait to create your own extractor.
|
||||
|
||||
The return value of the function must be a type that implements the `IntoResponse` trait. It can convert itself into an
|
||||
HTTP response through the `IntoResponse::into_response` method.
|
||||
|
||||
The following function has an extractor, which extracts the `name` and `value` parameters from the query string of the
|
||||
request uri and return a `String`, the string will be converted into an HTTP response.
|
||||
|
||||
```rust
|
||||
use serde::Deserialize;
|
||||
use poem::{handler, listener::TcpListener, web::Query, Server};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
name: String,
|
||||
value: i32,
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn index(Query(Params { name, value }): Query<Params>) -> String {
|
||||
format!("{}={}", name, value)
|
||||
}
|
||||
```
|
||||
|
||||
## HTTP server
|
||||
|
||||
Let's start a server, it listens to `127.0.0.1:3000`, please ignore these `unwrap` calls, this is just an example.
|
||||
|
||||
The `Server::run` function accepts any type that implements the `Endpoint` trait. In this example we don't have a
|
||||
routing object, so any request path will be handled by the `index` function.
|
||||
|
||||
```rust
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let listener = TcpListener::bind("127.0.0.1:3000");
|
||||
Server::new(listener).run(index).await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
In this way, a simple example is implemented, we can run it and then use `curl` to do some tests.
|
||||
|
||||
```shell
|
||||
> curl http://localhost:3000?name=a&value=10
|
||||
name=10
|
||||
```
|
||||
@@ -1,125 +0,0 @@
|
||||
# Responses
|
||||
|
||||
All types that can be converted to HTTP response `Response` should implement `IntoResponse`, and they can be used as the
|
||||
return value of the handler function.
|
||||
|
||||
In the following example, the `string_response` and `status_response` functions return the `String` and `StatusCode`
|
||||
types, because `Poem` has implemented the `IntoResponse` feature for them.
|
||||
|
||||
The `no_response` function does not return a value. We can also think that its return type is `()`, and `Poem` also
|
||||
implements `IntoResponse` for `()`, which is always converted to `200 OK`.
|
||||
|
||||
```rust
|
||||
use poem::handler;
|
||||
use poem::http::StatusCode;
|
||||
|
||||
#[handler]
|
||||
fn string_response() -> String {
|
||||
"hello".to_string()
|
||||
}
|
||||
|
||||
#[handler]
|
||||
fn status_response() -> StatusCode {}
|
||||
|
||||
#[handler]
|
||||
fn no_response() {}
|
||||
|
||||
```
|
||||
|
||||
# Built-in responses
|
||||
|
||||
- **Result<T: IntoResponse, E: IntoResponse>**
|
||||
|
||||
if the result is `Ok`, use the `Ok` value as the response, otherwise use the `Err` value.
|
||||
|
||||
- **()**
|
||||
|
||||
Sets the status to `OK` with an empty body.
|
||||
|
||||
- **&'static str**
|
||||
|
||||
Sets the status to `OK` and the `Content-Type` to `text/plain`. The
|
||||
string is used as the body of the response.
|
||||
|
||||
- **String**
|
||||
|
||||
Sets the status to `OK` and the `Content-Type` to `text/plain`. The
|
||||
string is used as the body of the response.
|
||||
|
||||
- **&'static [u8]**
|
||||
|
||||
Sets the status to `OK` and the `Content-Type` to
|
||||
`application/octet-stream`. The slice is used as the body of the response.
|
||||
|
||||
- **Html<T>**
|
||||
|
||||
Sets the status to `OK` and the `Content-Type` to `text/html`. `T` is
|
||||
used as the body of the response.
|
||||
|
||||
- **Json<T>**
|
||||
|
||||
Sets the status to `OK` and the `Content-Type` to `application/json`. Use
|
||||
[`serde_json`](https://crates.io/crates/serde_json) to serialize `T` into a json string.
|
||||
|
||||
- **Bytes**
|
||||
|
||||
Sets the status to `OK` and the `Content-Type` to
|
||||
`application/octet-stream`. The bytes is used as the body of the response.
|
||||
|
||||
- **Vec<u8>**
|
||||
|
||||
Sets the status to `OK` and the `Content-Type` to
|
||||
`application/octet-stream`. The vector’s data is used as the body of the
|
||||
response.
|
||||
|
||||
- **Body**
|
||||
|
||||
Sets the status to `OK` and use the specified body.
|
||||
|
||||
- **StatusCode**
|
||||
|
||||
Sets the status to the specified status code `StatusCode` with an empty
|
||||
body.
|
||||
|
||||
- **(StatusCode, T)**
|
||||
|
||||
Convert `T` to response and set the specified status code `StatusCode`.
|
||||
|
||||
- **(StatusCode, HeaderMap, T)**
|
||||
|
||||
Convert `T` to response and set the specified status code `StatusCode`,
|
||||
and then merge the specified `HeaderMap`.
|
||||
|
||||
- **Response**
|
||||
|
||||
The implementation for `Response` always returns itself.
|
||||
|
||||
- **Compress<T>**
|
||||
|
||||
Call `T::into_response` to get the response, then compress the response
|
||||
body with the specified algorithm, and set the correct `Content-Encoding`
|
||||
header.
|
||||
|
||||
- **SSE**
|
||||
|
||||
Sets the status to `OK` and the `Content-Type` to `text/event-stream`
|
||||
with an event stream body. Use the `SSE::new` function to
|
||||
create it.
|
||||
|
||||
## Custom response
|
||||
|
||||
In the following example, we wrap a response called `PDF`, which adds a `Content-Type: applicationn/pdf` header to the response.
|
||||
|
||||
```rust
|
||||
use poem::{IntoResponse, Response};
|
||||
|
||||
struct PDF(Vec<u8>);
|
||||
|
||||
impl IntoResponse for PDF {
|
||||
fn into_response(self) -> Response {
|
||||
Response::builder()
|
||||
.header("Content-Type", "application/pdf")
|
||||
.body(self.0)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,80 +0,0 @@
|
||||
# Routing
|
||||
|
||||
The routing object is used to dispatch the request of the specified path and method to the specified endpoint.
|
||||
|
||||
The route object is actually an endpoint, which implements the `Endpoint` trait.
|
||||
|
||||
In the following example, we dispatch the requests of `/a` and `/b` to different endpoints.
|
||||
|
||||
```rust
|
||||
use poem::{handler, Route};
|
||||
|
||||
#[handler]
|
||||
async fn a() -> &'static str { "a" }
|
||||
|
||||
#[handler]
|
||||
async fn b() -> &'static str { "b" }
|
||||
|
||||
let ep = Route::new()
|
||||
.at("/a", a)
|
||||
.at("/b", b);
|
||||
```
|
||||
|
||||
## Capture the variables
|
||||
|
||||
Use `:NAME` to capture the value of the specified segment in the path, or use `*NAME` to capture all the values after
|
||||
the specified prefix.
|
||||
|
||||
In the following example, the captured values will be stored in the variable `value`, and you can use the path extractor to get them.
|
||||
|
||||
```rust
|
||||
#[handler]
|
||||
async fn a(Path(String): Path<String>) {}
|
||||
|
||||
let ep = Route::new()
|
||||
.at("/a/:value/b", handler)
|
||||
.at("/prefix/*value", handler);
|
||||
```
|
||||
|
||||
## Regular expressions
|
||||
|
||||
You can use regular expressions to match, `<REGEX>` or `:NAME<REGEX>`, the second one can capture the matched value into a variable.
|
||||
|
||||
```rust
|
||||
let ep = Route::new()
|
||||
.at("/a/<\\d+>", handler)
|
||||
.at("/b/:value<\\d+>", handler);
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
Sometimes we want to assign a path with a specified prefix to a specified endpoint, so that some functionally independent
|
||||
components can be created.
|
||||
|
||||
In the following example, the request path of the `hello` endpoint is `/api/hello`.
|
||||
|
||||
```rust
|
||||
let api = Route::new().at("/hello", hello);
|
||||
let ep = api.nest("/api", api);
|
||||
```
|
||||
|
||||
Static file service is such an independent component.
|
||||
|
||||
```rust
|
||||
let ep = Route::new().nest("/files", Files::new("./static_files"));
|
||||
```
|
||||
|
||||
## Method routing
|
||||
|
||||
The routing objects introduced above can only be dispatched by some specified paths, but dispatch by paths and methods
|
||||
is more common. `Poem` provides another route object `RouteMethod`, when it is combined with the `Route` object, it can
|
||||
provide this ability.
|
||||
|
||||
`Poem` provides some convenient functions to create `RouteMethod` objects, they are all named after HTTP standard methods.
|
||||
|
||||
```rust
|
||||
use poem::{Route, get, post};
|
||||
|
||||
let ep = Route::new()
|
||||
.at("/users", get(get_user).post(create_user).delete(delete_user).put(update_user));
|
||||
```
|
||||
@@ -1,31 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Poem Book</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<style>
|
||||
div {
|
||||
margin: 18pt;
|
||||
font-size: 24pt;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
if (/^zh\b/.test(navigator.language)) {
|
||||
location.href="/zh-CN/index.html"
|
||||
} else {
|
||||
location.href="/en/index.html"
|
||||
}
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>Poem Book</h1>
|
||||
<p>This book is available in multiple languages:</p>
|
||||
<ul>
|
||||
<li><a href="en/index.html">English</a></li>
|
||||
<li><a href="zh-CN/index.html">简体中文</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +0,0 @@
|
||||
[book]
|
||||
authors = ["sunli"]
|
||||
description = "Poem 使用手册"
|
||||
src = "src"
|
||||
language = "zh-CN"
|
||||
title = "Poem 使用手册"
|
||||
@@ -1,28 +0,0 @@
|
||||
# Poem Book
|
||||
|
||||
## Poem
|
||||
|
||||
- [Poem](poem.md)
|
||||
- [快速开始](poem/quickstart.md)
|
||||
- [Endpoint](poem/endpoint.md)
|
||||
- [路由](poem/routing.md)
|
||||
- [提取器](poem/extractors.md)
|
||||
- [响应](poem/responses.md)
|
||||
- [处理错误](poem/handling_errors.md)
|
||||
- [中间件](poem/middleware.md)
|
||||
- [协议](poem/protocols.md)
|
||||
- [Websocket](poem/protocols/websocket.md)
|
||||
- [服务端事件 (SSE)](poem/protocols/sse.md)
|
||||
- [监听器](poem/listeners.md)
|
||||
- [OpenAPI](openapi.md)
|
||||
- [快速开始](openapi/quickstart.md)
|
||||
- [类型系统](openapi/type_system.md)
|
||||
- [基础类型](openapi/type_system/basic_types.md)
|
||||
- [枚举](openapi/type_system/enum.md)
|
||||
- [对象](openapi/type_system/object.md)
|
||||
- [定义API](openapi/api.md)
|
||||
- [自定义请求](openapi/custom_request.md)
|
||||
- [自定义响应](openapi/custom_response.md)
|
||||
- [文件上传](openapi/upload_files.md)
|
||||
- [参数校验](openapi/validators.md)
|
||||
- [认证](openapi/authentication.md)
|
||||
@@ -1,7 +0,0 @@
|
||||
# OpenAPI
|
||||
|
||||
[OpenAPI]((https://swagger.io/specification/)) 规范为`RESTful API`定义了一个标准的并且与语言无关的接口,它允许人类和计算机在不访问源代码、文档或通过网络流量检查的情况下发现和理解服务的功能。若经良好定义,使调用者可以很容易的理解远程服务并与之交互, 并只需要很少的代码即可实现期望逻辑.
|
||||
|
||||
`Poem-openapi`是基于`Poem`的 [OpenAPI](https://swagger.io/specification/) 服务端框架。
|
||||
|
||||
通常,如果你希望让你的 API 支持该规范,首先需要创建一个 [接口定义文件](https://swagger.io/specification/) ,然后再按照接口定义编写对应的代码。或者创建接口定义文件后,用 `Swagger CodeGen` 来生成服务端代码框架。但`Poem-openapi`区别于这两种方法,它让你只需要编写 Rust 的业务代码,利用过程宏来自动生成符合 OpenAPI 规范的接口和接口定义文件(这相当于接口的文档)。
|
||||
@@ -1,77 +0,0 @@
|
||||
# 定义API
|
||||
|
||||
下面定义一组API对宠物表进行增删改查的操作。
|
||||
|
||||
一个方法代表一个API操作,必须使用`path`和`method`属性指定操作的路径和方法。
|
||||
|
||||
方法的参数可以有多个,可以使用以下类型:
|
||||
|
||||
- **poem_openapi::param::Query** 表示参数来自查询字符串
|
||||
|
||||
- **poem_openapi::param::Header** 表示参数来自请求头
|
||||
|
||||
- **poem_openapi::param::Path** 表示参数来自请求路径
|
||||
|
||||
- **poem_openapi::param::Cookie** 表示参数来自Cookie
|
||||
|
||||
- **poem_openapi::param::CookiePrivate** 表示参数来自加密的Cookie
|
||||
|
||||
- **poem_openapi::param::CookieSigned** 表示参数来自签名后的Cookie
|
||||
|
||||
- **poem_openapi::payload::Binary** 表示请求内容是二进制数据
|
||||
|
||||
- **poem_openapi::payload::Json** 表示请求内容用Json编码
|
||||
|
||||
- **poem_openapi::payload::PlainText** 表示请求内容是UTF8文本
|
||||
|
||||
- **ApiRequest** 使用`ApiRequest`宏生成的请求体
|
||||
|
||||
- **SecurityScheme** 使用`SecurityScheme`宏生成认证方法
|
||||
|
||||
- **T: FromRequest** 使用Poem的提取器
|
||||
|
||||
返回值可以是任意实现了`ApiResponse`的类型。
|
||||
|
||||
```rust
|
||||
use poem_api::{
|
||||
OpenApi,
|
||||
poem_api::payload::Json,
|
||||
param::{Path, Query},
|
||||
};
|
||||
use poem::Result;
|
||||
|
||||
struct Api;
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
/// 添加新Pet
|
||||
#[oai(path = "/pet", method = "post")]
|
||||
async fn add_pet(&self, pet: Json<Pet>) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// 更新已有的Pet
|
||||
#[oai(path = "/pet", method = "put")]
|
||||
async fn update_pet(&self, pet: Json<Pet>) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// 删除一个Pet
|
||||
#[oai(path = "/pet/:id", method = "delete")]
|
||||
async fn delete_pet(&self, id: Path<u64>) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// 根据ID查询Pet
|
||||
#[oai(path = "/pet", method = "get")]
|
||||
async fn find_pet_by_id(&self, id: Query<u64>) -> Result<Json<Pet>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// 根据状态查询Pet
|
||||
#[oai(path = "/pet/findByStatus", method = "get")]
|
||||
async fn find_pets_by_status(&self, status: Query<Status>) -> Result<Json<Vec<Pet>>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,80 +0,0 @@
|
||||
# 认证
|
||||
|
||||
OpenApi规范定义了`apikey`,`basic`,`bearer`,`oauth2`,`openIdConnect`五种认证模式,它们描述了指定的`API`接口需要的认证参数。
|
||||
|
||||
下面的例子是用`Github`登录,并提供一个获取所有公共仓库信息的接口。
|
||||
|
||||
```rust
|
||||
use poem_openapi::{
|
||||
SecurityScheme, SecurityScope, OpenApi,
|
||||
auth::Bearer,
|
||||
};
|
||||
|
||||
#[derive(OAuthScopes)]
|
||||
enum GithubScope {
|
||||
/// 可访问公共仓库信息。
|
||||
#[oai(rename = "public_repo")]
|
||||
PublicRepo,
|
||||
|
||||
/// 可访问用户的个人资料数据。
|
||||
#[oai(rename = "read:user")]
|
||||
ReadUser,
|
||||
}
|
||||
|
||||
/// Github 认证
|
||||
#[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(scope("GithubScope::PublicRepo"))] auth: GithubAuthorization,
|
||||
) -> Result<PlainText<String>> {
|
||||
// 使用GithubAuthorization得到的token向Github获取所有公共仓库信息。
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
完整的代码请参考[例子](https://github.com/poem-web/poem/tree/master/examples/openapi/auth-github)。
|
||||
|
||||
## 检查认证信息
|
||||
|
||||
您可以使用`checker`属性指定一个检查器函数来检查原始认证信息和将其转换为该函数的返回类型。 此函数必须返回`Option<T>`,如果检查失败则返回`None`。
|
||||
|
||||
```rust
|
||||
struct User {
|
||||
username: String,
|
||||
}
|
||||
|
||||
/// ApiKey 认证
|
||||
#[derive(SecurityScheme)]
|
||||
#[oai(
|
||||
type = "api_key",
|
||||
key_name = "X-API-Key",
|
||||
in = "header",
|
||||
checker = "api_checker"
|
||||
)]
|
||||
struct MyApiKeyAuthorization(User);
|
||||
|
||||
async fn api_checker(req: &Request, api_key: ApiKey) -> Option<User> {
|
||||
let connection = req.data::<DbConnection>().unwrap();
|
||||
|
||||
// 在数据库中检查
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
完整的代码请参考[例子](https://github.com/poem-web/poem/tree/master/examples/openapi/auth-apikey).
|
||||
@@ -1,49 +0,0 @@
|
||||
# 自定义请求
|
||||
|
||||
`OpenAPI`规范允许同一个接口支持处理不同`Content-Type`的请求,例如一个接口可以同时接受`application/json`和`text/plain`类型的Payload。
|
||||
|
||||
在`Poem-openapi`中,要支持此类型请求,需要用`ApiRequest`宏自定义一个实现了`Payload trait`的请求对象。
|
||||
|
||||
在下面的例子中,`create_post`函数接受`CreatePostRequest`请求,当创建成功后,返回`id`。
|
||||
|
||||
```rust
|
||||
use poem_open::{
|
||||
ApiRequest, Object,
|
||||
payload::{PlainText, Json},
|
||||
};
|
||||
use poem::Result;
|
||||
|
||||
#[derive(Object)]
|
||||
struct Post {
|
||||
title: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(ApiRequest)]
|
||||
enum CreatePostRequest {
|
||||
/// 从JSON创建
|
||||
Json(Json<Blog>),
|
||||
/// 从文本创建
|
||||
Text(PlainText<String>),
|
||||
}
|
||||
|
||||
struct Api;
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(path = "/hello", method = "post")]
|
||||
async fn create_post(
|
||||
&self,
|
||||
req: CreatePostRequest,
|
||||
) -> Result<Json<u64>> {
|
||||
match req {
|
||||
CreatePostRequest::Json(Json(blog)) => {
|
||||
todo!();
|
||||
}
|
||||
CreatePostRequest::Text(content) => {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,89 +0,0 @@
|
||||
# 自定义响应
|
||||
|
||||
在前面的例子中,我们的所有请求处理函数都返回的`Result`类型,当发生错误时返回一个`poem::Error`,它包含错误的原因以及状态码。但`OpenAPI`规范允许更详细的描述请求的响应,例如该接口可能会返回哪些状态码,以及状态码对应的原因和响应的内容。
|
||||
|
||||
在下面的例子中,我们修改`create_post`函数的返回值为`CreateBlogResponse`类型。
|
||||
|
||||
`Ok`,`Forbidden`和`InternalError`描述了特定状态码的响应类型。
|
||||
|
||||
```rust
|
||||
use poem_openapi::ApiResponse;
|
||||
use poem::http::StatusCode;
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
enum CreateBlogResponse {
|
||||
/// 创建完成
|
||||
#[oai(status = 200)]
|
||||
Ok(Json<u64>),
|
||||
|
||||
/// 没有权限
|
||||
#[oai(status = 403)]
|
||||
Forbidden,
|
||||
|
||||
/// 内部错误
|
||||
#[oai(status = 500)]
|
||||
InternalError,
|
||||
}
|
||||
|
||||
struct Api;
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(path = "/hello", method = "get")]
|
||||
async fn create_post(
|
||||
&self,
|
||||
req: CreatePostRequest,
|
||||
) -> CreateBlogResponse {
|
||||
match req {
|
||||
CreatePostRequest::Json(Json(blog)) => {
|
||||
todo!();
|
||||
}
|
||||
CreatePostRequest::Text(content) => {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当请求解析失败时,默认会返回`400 Bad Request`错误,但有时候我们想返回一个自定义的错误内容,可以使用`bad_request_handler`属性设置一个错误处理函数,这个函数用于转换`ParseRequestError`到指定的响应类型。
|
||||
|
||||
```rust
|
||||
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 {
|
||||
/// 创建完成
|
||||
#[oai(status = 200)]
|
||||
Ok(Json<u64>),
|
||||
|
||||
/// 没有权限
|
||||
#[oai(status = 403)]
|
||||
Forbidden,
|
||||
|
||||
/// 内部错误
|
||||
#[oai(status = 500)]
|
||||
InternalError,
|
||||
|
||||
/// 请求无效
|
||||
#[oai(status = 400)]
|
||||
BadRequest(Json<ErrorMessage>),
|
||||
}
|
||||
|
||||
fn bad_request_handler(err: ParseRequestError) -> CreateBlogResponse {
|
||||
// 当解析请求失败时,返回一个自定义的错误内容,它是一个JSON
|
||||
CreateBlogResponse::BadRequest(Json(ErrorMessage {
|
||||
code: -1,
|
||||
reason: err.to_string(),
|
||||
}))
|
||||
}
|
||||
```
|
||||
@@ -1,61 +0,0 @@
|
||||
# 快速开始
|
||||
|
||||
下面这个例子,我们定义了一个路径为`/hello`的API,它接受一个名为`name`的URL参数,并且返回一个字符串作为响应内容。`name`参数的类型是`Option<String>`,意味着这是一个可选参数。
|
||||
|
||||
运行以下代码后,用浏览器打开`http://localhost:3000`就能看到`Swagger UI`,你可以用它来浏览API的定义并且测试它们。
|
||||
|
||||
```rust
|
||||
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" 说明这个参数来自Url
|
||||
) -> PlainText<String> { // PlainText是响应类型,它表明该API的响应类型是一个字符串,Content-Type是`text/plain`
|
||||
match name {
|
||||
Some(name) => PlainText(format!("hello, {}!", name)),
|
||||
None => PlainText("hello!".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
// 创建一个TCP监听器
|
||||
let listener = TcpListener::bind("127.0.0.1:3000");
|
||||
|
||||
// 创建API服务
|
||||
let api_service = OpenApiService::new(Api, "Demo", "0.1.0")
|
||||
.title("Hello World")
|
||||
.server("http://localhost:3000/api");
|
||||
|
||||
// 创建Swagger UI端点
|
||||
let ui = api_service.swagger_ui();
|
||||
|
||||
// 创建OpenApi输出规范的端点
|
||||
let spec = api_service.spec_endpoint();
|
||||
|
||||
// 启动服务器,并指定api的根路径为 /api,Swagger UI的路径为 /
|
||||
poem::Server::new(listener)
|
||||
.await?
|
||||
.run(
|
||||
Route::new()
|
||||
.at("/openapi.json", spec)
|
||||
.nest("/api", api_service)
|
||||
.nest("/", ui)
|
||||
)
|
||||
.await
|
||||
}
|
||||
```
|
||||
|
||||
这是`poem-openapi`的一个例子,所以你也可以直接执行以下命令来验证:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/poem-web/poem
|
||||
cargo run --bin example-openapi-hello-world
|
||||
```
|
||||
@@ -1,5 +0,0 @@
|
||||
# 类型系统
|
||||
|
||||
|
||||
Poem-openapi 实现了 OpenAPI 类型到 Rust 类型的转换,简单易用。
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Basic types
|
||||
|
||||
基础类型可以作为请求的参数,请求内容或者请求响应内容。`Poem`定义了一个`Type trait`,实现了该`trait`的类型都是基础类型,它们能在运行时提供一些关于该类型的信息用于生成接口定义文件。
|
||||
|
||||
`Poem`为大部分常用类型实现了`Type`trait,你可以直接使用它们,同样也可以自定义新的类型,但你需要对 [Json Schema](https://json-schema.org/) 有一定了解。
|
||||
|
||||
下表是 Open API 中的数据类型对应的Rust数据类型(只是一小部分):
|
||||
|
||||
| Open API | Rust |
|
||||
|-----------------------------------------|-----------------------------------|
|
||||
| `{type: "integer", format: "int32" }` | i32 |
|
||||
| `{type: "integer", format: "float32" }` | f32 |
|
||||
| `{type: "bool" }` | bool |
|
||||
| `{type: "string" }` | String, &str |
|
||||
| `{type: "string", format: "binary" }` | Binary |
|
||||
| `{type: "string", format: "bytes" }` | Base64 |
|
||||
| `{type: "array" }` | Vec<T> |
|
||||
@@ -1,16 +0,0 @@
|
||||
# 枚举
|
||||
|
||||
使用过程宏 `Enum` 来定义枚举类型。
|
||||
|
||||
**Poem-openapi 会自动将每一项的名称改为`SCREAMING_SNAKE_CASE` 约定。 您可以使用 `rename_all` 属性来重命名所有项目。**
|
||||
|
||||
```rust
|
||||
use poem_api::Enum;
|
||||
|
||||
#[derive(Enum)]
|
||||
enum PetStatus {
|
||||
Available,
|
||||
Pending,
|
||||
Sold,
|
||||
}
|
||||
```
|
||||
@@ -1,28 +0,0 @@
|
||||
# 对象类型
|
||||
|
||||
用过程宏`Object`来定义一个对象,对象的成员必须是实现了`Type trait`的类型(除非你用`#[oai(skip)]`来标注它,那么序列化和反序列化时降忽略该字段用默认值代替)。
|
||||
|
||||
以下代码定义了一个对象类型,它包含四个字段,其中有一个字段是枚举类型。
|
||||
|
||||
_对象类型也是基础类型的一种,它同样实现了`Type trait`,所以它也可以作为另一个对象的成员。_
|
||||
|
||||
**Poem-openapi 会自动将每个成员的名称更改为 `camelCase` 约定。 你可以使用 `rename_all` 属性来重命名所有项。**
|
||||
|
||||
```rust
|
||||
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,
|
||||
}
|
||||
```
|
||||
@@ -1,27 +0,0 @@
|
||||
# 文件上传
|
||||
|
||||
`Multipart`宏通常用于文件上传,它可以定义一个表单来包含一个或者多个文件以及一些附加字段。下面的例子提供一个创建`Pet`对象的接口,它在创建`Pet`对象的同时上传一些图片文件。
|
||||
|
||||
```rust
|
||||
use poem_openapi::{Multipart, OpenApi};
|
||||
use poem::Result;
|
||||
|
||||
#[derive(Debug, Multipart)]
|
||||
struct CreatePetPayload {
|
||||
name: String,
|
||||
status: PetStatus,
|
||||
photos: Vec<Upload>, // 多个照片文件
|
||||
}
|
||||
|
||||
struct Api;
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(path = "/pet", method = "post")]
|
||||
async fn create_pet(&self, payload: CreatePetPayload) -> Result<Json<u64>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
完整的代码请参考[文件上传例子](https://github.com/poem-web/poem/tree/master/examples/openapi/upload`)。
|
||||
@@ -1,24 +0,0 @@
|
||||
# 参数校验
|
||||
|
||||
`OpenAPI`引用了`Json Schema`的校验规范,`Poem-openapi`同样支持它们。你可以在请求的参数,对象的成员和`Multipart`的字段三个地方应用校验器。校验器是类型安全的,如果待校验的数据类型和校验器所需要的不匹配,那么将无法编译通过。例如`maximum`只能用于数值类型,`max_items`只能用于数组类型。
|
||||
|
||||
更多的校验器请参考[文档](https://docs.rs/poem-openapi/*/poem_openapi/attr.OpenApi.html#operation-argument-parameters)。
|
||||
|
||||
```rust
|
||||
use poem_openapi::{Object, OpenApi, Multipart};
|
||||
|
||||
#[derive(Object)]
|
||||
struct Pet {
|
||||
id: u64,
|
||||
|
||||
/// 名字长度不能超过32
|
||||
#[oai(validator(max_length = "32"))]
|
||||
name: String,
|
||||
|
||||
/// 数组长度不能超过3,并且url长度不能超过256
|
||||
#[oai(validator(max_items = "3", max_length = "256"))]
|
||||
photo_urls: Vec<String>,
|
||||
|
||||
status: PetStatus,
|
||||
}
|
||||
```
|
||||
@@ -1,3 +0,0 @@
|
||||
# Poem
|
||||
|
||||
`Poem` 是一个功能齐全且易于使用的 Web 框架,采用 Rust 编程语言。
|
||||
@@ -1,84 +0,0 @@
|
||||
# Endpoint
|
||||
|
||||
Endpoint 可以处理 HTTP 请求。您可以实现`Endpoint` trait 来创建您自己的Endpoint。
|
||||
`Poem` 还提供了一些方便的功能来轻松创建自定义 Endpoint 类型。
|
||||
|
||||
在上一章中,我们学习了如何使用 `handler` 宏将函数转换为 Endpoint。
|
||||
|
||||
现在让我们看看如何通过实现 `Endpoint` trait 来创建自己的 Endpoint。
|
||||
|
||||
这是 `Endpoint` trait 的定义,你需要指定 `Output` 的类型并实现 `call` 方法。
|
||||
|
||||
```rust
|
||||
/// 一个 HTTP 请求处理程序。
|
||||
#[async_trait]
|
||||
pub trait Endpoint: Send + Sync + 'static {
|
||||
/// 代表 endpoint 的响应。
|
||||
type Output: IntoResponse;
|
||||
|
||||
/// 获取对请求的响应。
|
||||
async fn call(&self, req: Request) -> Self::Output;
|
||||
}
|
||||
```
|
||||
|
||||
现在我们实现一个 `Endpoint`,它接收 HTTP 请求并输出一个包含请求方法和路径的字符串。
|
||||
|
||||
`Output` 关联类型必须是实现 `IntoResponse` trait 的类型。Poem 已为大多数常用类型实现了它。
|
||||
|
||||
由于 `Endpoint` 包含一个异步方法 `call`,我们需要用 `async_trait` 宏来修饰它。
|
||||
|
||||
```rust
|
||||
struct MyEndpoint;
|
||||
|
||||
#[async_trait]
|
||||
impl Endpoint for MyEndpoint {
|
||||
type Output = String;
|
||||
|
||||
async fn call(&self, req: Request) -> Self::Output {
|
||||
format!("method={} path={}", req.method(), req.uri().path());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 从函数创建
|
||||
|
||||
你可以使用 `poem::endpoint::make` 和 `poem::endpoint::make_sync` 从异步函数和同步函数创建 Endpoint。
|
||||
|
||||
以下 Endpoint 执行相同的操作:
|
||||
|
||||
```rust
|
||||
let ep = poem::endpoint::make(|req| async move {
|
||||
format!("method={} path={}", req.method(), req.uri().path())
|
||||
});
|
||||
```
|
||||
|
||||
## EndpointExt
|
||||
|
||||
`EndpointExt` trait 提供了一些方便的函数来转换 Endpoint 的输入或输出。
|
||||
|
||||
- `EndpointExt::before` 用于转换请求。
|
||||
- `EndpointExt::after` 用于转换输出。
|
||||
- `EndpointExt::map_ok`、`EndpointExt::map_err`、`EndpointExt::and_then` 用于处理 `Result<T>` 类型的输出。
|
||||
|
||||
## 使用 Result 类型
|
||||
|
||||
`Poem` 还为 `poem::Result<T>` 类型实现了 `IntoResponse`,因此它也可以用作 Endpoint,因此你可以在 `call` 方法中使用 `?`。
|
||||
|
||||
```rust
|
||||
struct MyEndpoint;
|
||||
|
||||
#[async_trait]
|
||||
impl Endpoint for MyEndpoint {
|
||||
type Output = poem::Result<String>;
|
||||
|
||||
async fn call(&self, req: Request) -> Self::Output {
|
||||
Ok(req.take_body().into_string().await?)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
你可以使用 `EndpointExt::map_to_response` 方法将 Endpoint 的输出转换为 `Response` 类型,或者使用 `EndpointExt::map_to_result` 将输出转换为 `poem::Result<Response>` 类型。
|
||||
|
||||
```rust
|
||||
let ep = MyEndpoint.map_to_response() // impl Endpoint<Output = Response>
|
||||
```
|
||||
@@ -1,213 +0,0 @@
|
||||
# 提取器
|
||||
|
||||
提取器用于从 HTTP 请求中提取某些内容。
|
||||
|
||||
`Poem` 提供了一些常用的提取器来从 HTTP 请求中提取一些东西。
|
||||
|
||||
你可以使用一个或多个提取器作为函数的参数,最多 16 个。
|
||||
|
||||
在下面的例子中,`index` 函数使用 3 个提取器来提取远程地址、HTTP 方法和 URI。
|
||||
|
||||
```rust
|
||||
#[handler]
|
||||
fn index(remote_addr: SocketAddr, method: Method, uri: &Uri) {}
|
||||
```
|
||||
|
||||
# 内置提取器
|
||||
|
||||
- **Option<T>**
|
||||
|
||||
从传入的请求中提取 `T`,如果失败就返回 `None`。
|
||||
|
||||
- **&Request**
|
||||
|
||||
从传入的请求中提取 `Request`.
|
||||
|
||||
- **&RemoteAddr**
|
||||
|
||||
从请求中提取远端对等地址 [`RemoteAddr`]。
|
||||
|
||||
- **&LocalAddr**
|
||||
|
||||
从请求中提取本地服务器的地址 [`LocalAddr`]。
|
||||
|
||||
- **Method**
|
||||
|
||||
从传入的请求中提取 `Method`。
|
||||
|
||||
- **Version**
|
||||
|
||||
从传入的请求中提取 `Version`。
|
||||
|
||||
- **&Uri**
|
||||
|
||||
从传入的请求中提取 `Uri`。
|
||||
|
||||
- **&HeaderMap**
|
||||
|
||||
从传入的请求中提取 `HeaderMap`。
|
||||
|
||||
- **Data<&T>**
|
||||
|
||||
从传入的请求中提取 `Data` 。
|
||||
|
||||
- **TypedHeader<T>**
|
||||
|
||||
从传入的请求中提取 `TypedHeader`。
|
||||
|
||||
- **Path<T>**
|
||||
|
||||
从传入的请求中提取 `Path`。
|
||||
|
||||
- **Query<T>**
|
||||
|
||||
从传入的请求中提取 `Query`。
|
||||
|
||||
- **Form<T>**
|
||||
|
||||
从传入的请求中提取 `Form`。
|
||||
|
||||
- **Json<T>**
|
||||
|
||||
从传入的请求中提取 `Json` 。
|
||||
|
||||
_这个提取器将接管请求的主体,所以你应该避免在一个处理程序中使用多个这种类型的提取器。_
|
||||
|
||||
- **TempFile**
|
||||
|
||||
从传入的请求中提取 `TempFile`。
|
||||
|
||||
_这个提取器将接管请求的主体,所以你应该避免在一个处理程序中使用多个这种类型的提取器。_
|
||||
|
||||
- **Multipart**
|
||||
|
||||
从传入的请求中提取 `Multipart`。
|
||||
|
||||
_这个提取器将接管请求的主体,所以你应该避免在一个处理程序中使用多个这种类型的提取器。_
|
||||
|
||||
- **&CookieJar**
|
||||
|
||||
从传入的请求中提取 `CookieJar`](cookie::CookieJar)。
|
||||
|
||||
_需要 `CookieJarManager` 中间件。_
|
||||
|
||||
- **&Session**
|
||||
|
||||
从传入的请求中提取 [`Session`](crate::session::Session)。
|
||||
|
||||
_需要 `CookieSession` 或 `RedisSession` 中间件。_
|
||||
|
||||
- **Body**
|
||||
|
||||
从传入的请求中提取 `Body`。
|
||||
|
||||
_这个提取器将接管请求的主体,所以你应该避免在一个处理程序中使用多个这种类型的提取器。_
|
||||
|
||||
- **String**
|
||||
|
||||
从传入的请求中提取 body 并将其解析为 utf8 字符串。
|
||||
|
||||
_这个提取器将接管请求的主体,所以你应该避免在一个处理程序中使用多个这种类型的提取器。_
|
||||
|
||||
- **Vec<u8>**
|
||||
|
||||
从传入的请求中提取 body 并将其收集到 `Vec<u8>`.
|
||||
|
||||
_这个提取器将接管请求的主体,所以你应该避免在一个处理程序中使用多个这种类型的提取器。_
|
||||
|
||||
- **Bytes**
|
||||
|
||||
从传入的请求中提取 body 并将其收集到 `Bytes`.
|
||||
|
||||
_这个提取器将接管请求的主体,所以你应该避免在一个处理程序中使用多个这种类型的提取器。_
|
||||
|
||||
- **WebSocket**
|
||||
|
||||
准备接受 websocket 连接。
|
||||
|
||||
## 处理提取器错误
|
||||
|
||||
默认情况下,当发生错误时,提取器会返回`400 Bad Request`,但有时您可能想要更改这种行为,因此您可以自己处理错误。
|
||||
|
||||
在下面的例子中,当 `Query` 提取器失败时,它将返回一个 `500 Internal Server Error` 响应以及错误原因。
|
||||
|
||||
```rust
|
||||
use poem::web::Query;
|
||||
use poem::error::ParseQueryError;
|
||||
use poem::{IntoResponse, Response};
|
||||
use poem::http::StatusCode;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Params {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[handler]
|
||||
fn index(res: Result<Query<Params>, ParseQueryError>) -> Response {
|
||||
match res {
|
||||
Ok(Query(params)) => params.name.into_response(),
|
||||
Err(err) => Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(err.to_string()),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义提取器
|
||||
|
||||
您还可以实现自己的提取器。
|
||||
|
||||
以下是自定义 token 提取器的示例,它提取来自 `MyToken` 标头的 token。
|
||||
|
||||
```rust
|
||||
use poem::{
|
||||
get, handler, http::StatusCode, listener::TcpListener, FromRequest, Request,
|
||||
RequestBody, Response, Route, Server,
|
||||
};
|
||||
|
||||
struct Token(String);
|
||||
|
||||
// Token 提取器的错误类型
|
||||
#[derive(Debug)]
|
||||
struct MissingToken;
|
||||
|
||||
/// 自定义错误也可以重用
|
||||
impl IntoResponse for MissingToken {
|
||||
fn into_response(self) -> Response {
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body("missing token")
|
||||
}
|
||||
}
|
||||
|
||||
// 实现一个 token 提取器
|
||||
#[poem::async_trait]
|
||||
impl<'a> FromRequest<'a> for Token {
|
||||
type Error = MissingToken;
|
||||
|
||||
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self, Self::Error> {
|
||||
let token = req
|
||||
.headers()
|
||||
.get("MyToken")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.ok_or(MissingToken)?;
|
||||
Ok(Token(token.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn index(token: Token) {
|
||||
assert_eq!(token.0, "token123");
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), std::io::Error> {
|
||||
if std::env::var_os("RUST_LOG").is_none() {
|
||||
std::env::set_var("RUST_LOG", "poem=debug");
|
||||
}
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let app = Route::new().at("/", get(index));
|
||||
let listener = TcpListener::bind("127.0.0.1:3000");
|
||||
let server = Server::new(listener).await?;
|
||||
server.run(app).await
|
||||
}
|
||||
```
|
||||
@@ -1,114 +0,0 @@
|
||||
# 处理错误
|
||||
|
||||
在 `Poem` 中,我们根据响应状态代码处理错误。当状态码在`400-599`时,我们可以认为处理此请求时出错。
|
||||
|
||||
我们可以使用 `EndpointExt::after` 创建一个新的 Endpoint 类型来自定义错误响应。
|
||||
|
||||
在下面的例子中,`after`函数用于转换`index`函数的输出,并在发生服务器错误时输出错误响应。
|
||||
|
||||
**注意`handler`宏生成的 Endpoint 类型总是`Endpoint<Output=Response>`,即使它返回一个 `Result<T>`.**
|
||||
|
||||
```rust
|
||||
use poem::{handler, Result, Error};
|
||||
use poem::http::StatusCode;
|
||||
|
||||
#[handler]
|
||||
async fn index() -> Result<()> {
|
||||
Err(Error::new(StatusCode::BAD_REQUEST))
|
||||
}
|
||||
|
||||
let ep = index.after(|resp| {
|
||||
if resp.status().is_server_error() {
|
||||
Response::builder()
|
||||
.status(resp.status())
|
||||
.body("custom error")
|
||||
} else {
|
||||
resp
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`EndpointExt::map_to_result` 函数可以帮助我们将任何类型的 Endpoint 转换为 `Endpoint<Output = Response>`,所以我们只需要检查状态码就知道是否发生了错误。
|
||||
|
||||
```rust
|
||||
use poem::endpoint::make;
|
||||
use poem::{Error, EndpointExt};
|
||||
use poem::http::StatusCode;
|
||||
|
||||
let ep = make(|_| Ok::<(), Error>(Error::new(StatusCode::new(Status::BAD_REQUEST))))
|
||||
.map_to_response();
|
||||
|
||||
let ep = ep.after(|resp| {
|
||||
if resp.status().is_server_error() {
|
||||
Response::builder()
|
||||
.status(resp.status())
|
||||
.body("custom error")
|
||||
} else {
|
||||
resp
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## poem::Error
|
||||
|
||||
`poem::Error` 是一个通用的错误类型,它实现了 `From<T: Display>`,所以你可以很容易地使用 `?` 运算符来将任何错误类型转换为它。默认状态代码是`503 Internal Server Error`。
|
||||
|
||||
```rust
|
||||
use poem::Result;
|
||||
|
||||
#[handler]
|
||||
fn index(data: Vec<u8>) -> Result<i32> {
|
||||
let value: i32 = serde_json::from_slice(&data)?;
|
||||
Ok(value)
|
||||
}
|
||||
```
|
||||
|
||||
但是有时候我们不想总是使用 `503` 状态码,`Poem` 提供了一些辅助函数来转换错误类型。
|
||||
|
||||
```rust
|
||||
use poem::{Result, web::Json, error::BadRequest};
|
||||
|
||||
#[handler]
|
||||
fn index(data: Vec<u8>) -> Result<Json<i32>> {
|
||||
let value: i32 = serde_json::from_slice(&data).map_err(BadRequest)?;
|
||||
Ok(Json(value))
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义错误类型
|
||||
|
||||
有时我们可以使用自定义错误类型来减少重复的代码。
|
||||
|
||||
注意:`Poem` 的错误类型通常只需要实现 `IntoResponse`。
|
||||
|
||||
```rust
|
||||
use poem::{
|
||||
Response,
|
||||
error::ReadBodyError,
|
||||
http::StatusCode,
|
||||
};
|
||||
|
||||
enum MyError {
|
||||
InvalidValue,
|
||||
ReadBodyError(ReadBodyError),
|
||||
}
|
||||
|
||||
impl IntoResponse for MyError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
MyError::InvalidValue => Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body("invalid value"),
|
||||
MyError::ReadBodyError(err) => err.into(), // ReadBodyError 已经实现了 `IntoResponse`.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
fn index(data: Result<String, ReadBodyError>) -> Result<(), MyError> {
|
||||
let data = data?;
|
||||
if data.len() > 10 {
|
||||
return Err(MyError::InvalidValue);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,56 +0,0 @@
|
||||
# 监听器
|
||||
|
||||
`Poem` 提供了一些常用的监听器。
|
||||
|
||||
- TcpListener
|
||||
|
||||
侦听传入的 TCP 连接。
|
||||
|
||||
- UnixListener
|
||||
|
||||
侦听传入的 Unix 域套接字连接。
|
||||
|
||||
## TLS
|
||||
|
||||
你可以调用`Listener::tls` 函数来包装一个侦听器并使其支持TLS 连接。
|
||||
|
||||
```rust
|
||||
let listener = TcpListener::bind("127.0.0.1:3000")
|
||||
.tls(TlsConfig::new().key(KEY).cert(CERT));
|
||||
```
|
||||
|
||||
## TLS配置热加载
|
||||
|
||||
你可以使用异步流将最新的 Tls 配置传递给 `Poem`。
|
||||
|
||||
以下示例每 1 分钟从文件中加载最新的 TLS 配置:
|
||||
|
||||
```rust
|
||||
use async_trait::async_trait;
|
||||
|
||||
fn load_tls_config() -> Result<TlsConfig, std::io::Error> {
|
||||
Ok(TlsConfig::new()
|
||||
.cert(std::fs::read("cert.pem")?)
|
||||
.key(std::fs::read("key.pem")?))
|
||||
}
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:3000")
|
||||
.tls(async_stream::stream! {
|
||||
loop {
|
||||
if let Ok(tls_config) = load_tls_config() {
|
||||
yield tls_config;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 组合多个监听器。
|
||||
|
||||
调用`Listener::combine`将两个监听器合二为一,也可以多次调用该函数来合并更多的监听器。
|
||||
|
||||
```rust
|
||||
let listener = TcpListener::bind("127.0.0.1:3000")
|
||||
.combine(TcpListener::bind("127.0.0.1:3001"))
|
||||
.combine(TcpListener::bind("127.0.0.1:3002"));
|
||||
```
|
||||
@@ -1,113 +0,0 @@
|
||||
# 中间件
|
||||
|
||||
中间件可以在处理请求之前或之后做一些事情。
|
||||
|
||||
`Poem` 提供了一些常用的中间件实现。
|
||||
|
||||
- `AddData`
|
||||
|
||||
用于将状态附加到请求,例如用于身份验证的 token。
|
||||
|
||||
- `SetHeader`
|
||||
|
||||
用于向响应添加一些特定的 HTTP 标头。
|
||||
|
||||
- `Cors`
|
||||
|
||||
用于 CORS 跨域资源共享。
|
||||
|
||||
- `Tracing`
|
||||
|
||||
使用 [`tracing`](https://crates.io/crates/tracing) 记录所有请求和响应。
|
||||
|
||||
- `Compression`
|
||||
|
||||
用于解压请求体和压缩响应体。
|
||||
|
||||
## 自定义中间件
|
||||
|
||||
实现你自己的中间件很容易,你只需要实现 `Middleware` trait,它是一个转换器
|
||||
将输入 Endpoint 转换为另一个 Endpoint。
|
||||
|
||||
以下示例创建一个自定义中间件,该中间件读取名为“X-Token”的 HTTP 请求标头的值和将其添加为请求的状态。
|
||||
|
||||
```rust
|
||||
use poem::{handler, web::Data, Endpoint, EndpointExt, Middleware, Request};
|
||||
|
||||
/// 从 HTTP 标头中提取 token 的中间件。
|
||||
struct TokenMiddleware;
|
||||
|
||||
impl<E: Endpoint> Middleware<E> for TokenMiddleware {
|
||||
type Output = TokenMiddlewareImpl<E>;
|
||||
|
||||
fn transform(&self, ep: E) -> Self::Output {
|
||||
TokenMiddlewareImpl { ep }
|
||||
}
|
||||
}
|
||||
|
||||
/// TokenMiddleware 生成的新 Endpoint 类型。
|
||||
struct TokenMiddlewareImpl<E> {
|
||||
ep: E,
|
||||
}
|
||||
|
||||
const TOKEN_HEADER: &str = "X-Token";
|
||||
|
||||
/// Token 数据
|
||||
struct Token(String);
|
||||
|
||||
#[poem::async_trait]
|
||||
impl<E: Endpoint> Endpoint for TokenMiddlewareImpl<E> {
|
||||
type Output = E::Output;
|
||||
|
||||
async fn call(&self, mut req: Request) -> Self::Output {
|
||||
if let Some(value) = req
|
||||
.headers()
|
||||
.get(TOKEN_HEADER)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
// 将 token 数据插入到请求的扩展中。
|
||||
let token = value.to_string();
|
||||
req.extensions_mut().insert(Token(token));
|
||||
}
|
||||
|
||||
// 调用内部 endpoint。
|
||||
self.ep.call(req).await
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn index(Data(token): Data<&Token>) -> String {
|
||||
token.0.clone()
|
||||
}
|
||||
|
||||
// 使用 `TokenMiddleware` 中间件转换 `index` endpoint。
|
||||
let ep = index.with(TokenMiddleware);
|
||||
```
|
||||
|
||||
## 带函数的自定义中间件
|
||||
|
||||
您还可以使用函数来实现中间件。
|
||||
|
||||
```rust
|
||||
async fn extract_token<E: Endpoint>(next: E, mut req: Request) -> Response {
|
||||
if let Some(value) = req
|
||||
.headers()
|
||||
.get(TOKEN_HEADER)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
// 将 token 数据插入到请求的扩展中。
|
||||
let token = value.to_string();
|
||||
req.extensions_mut().insert(Token(token));
|
||||
}
|
||||
|
||||
// 调用下一个 endpoint。
|
||||
next.call(req).await
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn index(Data(token): Data<&Token>) -> String {
|
||||
token.0.clone()
|
||||
}
|
||||
|
||||
let ep = index.around(extract_token);
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
# 协议
|
||||
@@ -1,28 +0,0 @@
|
||||
# 服务器发送的事件 (SSE)
|
||||
|
||||
SSE 允许服务器不断地向客户端推送数据。
|
||||
|
||||
你需要使用实现 `Stream<Item=Event>` 的类型创建一个 `SSE` 响应。
|
||||
|
||||
下面示例中的端点将发送三个事件。
|
||||
|
||||
```rust
|
||||
use futures_util::stream;
|
||||
use poem::{
|
||||
handler, Route, get,
|
||||
http::StatusCode,
|
||||
web::sse::{Event, SSE},
|
||||
Endpoint, Request,
|
||||
};
|
||||
|
||||
#[handler]
|
||||
fn index() -> SSE {
|
||||
SSE::new(stream::iter(vec![
|
||||
Event::message("a"),
|
||||
Event::message("b"),
|
||||
Event::message("c"),
|
||||
]))
|
||||
}
|
||||
|
||||
let app = Route::new().at("/", get(index));
|
||||
```
|
||||
@@ -1,31 +0,0 @@
|
||||
# Websocket
|
||||
|
||||
Websocket 允许在客户端和服务器之间进行双向通信的长连接。
|
||||
|
||||
`Poem` 提供了一个 `WebSocket` 提取器来创建这个连接。
|
||||
|
||||
当连接升级成功时,调用指定的闭包来发送和接收数据。
|
||||
|
||||
下面的例子是一个回显服务,它总是发送接收到的数据。
|
||||
|
||||
**注意这个 Endpoint 的输出必须是`WebSocket::on_upgrade`函数的返回值,否则无法正确创建连接。**
|
||||
|
||||
```rust
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use poem::{
|
||||
handler, Route, get,
|
||||
web::websocket::{Message, WebSocket},
|
||||
IntoResponse,
|
||||
};
|
||||
|
||||
#[handler]
|
||||
async fn index(ws: WebSocket) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
if let Some(Ok(Message::Text(text))) = socket.next().await {
|
||||
let _ = socket.send(Message::Text(text)).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let app = Route::new().at("/", get(index));
|
||||
```
|
||||
@@ -1,60 +0,0 @@
|
||||
# 快速开始
|
||||
|
||||
## 添加依赖库
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
poem = "1.0"
|
||||
serde = "1.0"
|
||||
tokio = { version = "1.12.0", features = ["rt-multi-thread", "macros"] }
|
||||
```
|
||||
|
||||
## 实现一个Endpoint
|
||||
|
||||
`handler` 宏将函数转换为实现了 `Endpoint` 的类型,`Endpoint` trait 表示一种可以处理 HTTP 请求的类型。
|
||||
|
||||
这个函数可以接收一个或多个参数,每个参数都是一个提取器,可以从 HTTP 请求中提取你想要的信息。
|
||||
|
||||
提取器实现了 `FromRequest` trait,你也可以实现这个 trait 来创建你自己的提取器。
|
||||
|
||||
函数的返回值必须是实现了 `IntoResponse` trait 的类型。它可以通过 `IntoResponse::into_response` 方法将自己转化为一个 HTTP 响应。
|
||||
|
||||
下面的函数有一个提取器,它从 uri 请求的 query 中提取 `name` 和 `value` 参数并返回一个 `String`,该字符串将被转换为 HTTP 响应。
|
||||
|
||||
```rust
|
||||
use serde::Deserialize;
|
||||
use poem::{handler, listener::TcpListener, web::Query, Server};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Params {
|
||||
name: String,
|
||||
value: i32,
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn index(Query(Params { name, value }): Query<Params>) -> String {
|
||||
format!("{}={}", name, value)
|
||||
}
|
||||
```
|
||||
|
||||
## HTTP 服务器
|
||||
|
||||
让我们启动一个服务器,它监听 `127.0.0.1:3000`,请忽略这些 `unwrap` 调用,这只是一个例子。
|
||||
|
||||
`Server::run` 函数接受任何实现了 `Endpoint` Trait 的类型。在这个例子中,我们没有路由对象,因此任何请求路径都将由 `index` 函数处理。
|
||||
|
||||
```rust
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let listener = TcpListener::bind("127.0.0.1:3000");
|
||||
Server::new(listener).run(index).await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
这样,一个简单的例子就实现了,我们可以运行它,然后使用 `curl` 做一些测试。
|
||||
|
||||
```shell
|
||||
> curl http://localhost:3000?name=a&value=10
|
||||
name=10
|
||||
```
|
||||
@@ -1,109 +0,0 @@
|
||||
# 响应
|
||||
|
||||
所有可以转换为 HTTP 响应 `Response` 的类型都应该实现 `IntoResponse`,它们可以用作处理函数的返回值。
|
||||
|
||||
在下面的例子中,`string_response` 和 `status_response` 函数返回 `String` 和 `StatusCode`类型,因为 `Poem` 已经为它们实现了 `IntoResponse` 功能。
|
||||
|
||||
`no_response` 函数不返回值。我们也可以认为它的返回类型是`()`,`Poem`也为 `()` 实现 `IntoResponse`,它总是转换为 `200 OK`。
|
||||
|
||||
```rust
|
||||
use poem::handler;
|
||||
use poem::http::StatusCode;
|
||||
|
||||
#[handler]
|
||||
fn string_response() -> String {
|
||||
"hello".to_string()
|
||||
}
|
||||
|
||||
#[handler]
|
||||
fn status_response() -> StatusCode {}
|
||||
|
||||
#[handler]
|
||||
fn no_response() {}
|
||||
|
||||
```
|
||||
|
||||
# 内置响应类型
|
||||
|
||||
- **Result<T: IntoResponse, E: IntoResponse>**
|
||||
|
||||
如果结果为`Ok`, 则用`Ok`的值作为响应, 否则使用`Err`的值。
|
||||
|
||||
- **()**
|
||||
|
||||
将状态设置为`OK`,body 为空。
|
||||
|
||||
- **&'static str**
|
||||
|
||||
将状态设置为`OK`,将`Content-Type`设置为`text/plain`。字符串用作 body。
|
||||
|
||||
- **String**
|
||||
|
||||
将状态设置为`OK`,将`Content-Type`设置为`text/plain`。字符串用作 body。
|
||||
|
||||
- **&'static [u8]**
|
||||
|
||||
将状态设置为 `OK`,将 `Content-Type` 设置为 `application/octet-stream`。切片用作响应的 body。
|
||||
|
||||
- **Html<T>**
|
||||
|
||||
将状态设置为 `OK`,将 `Content-Type` 设置为 `text/html`. `T` 用作响应的 body。
|
||||
|
||||
- **Json<T>**
|
||||
|
||||
将状态设置为 `OK` ,将 `Content-Type` 设置为 `application/json`. 使用 [`serde_json`](https://crates.io/crates/serde_json) 将 `T` 序列化为 json 字符串。
|
||||
|
||||
- **Bytes**
|
||||
|
||||
将状态设置为 `OK` ,将 `Content-Type` 设置为 `application/octet-stream`。字节串用作响应的 body。
|
||||
|
||||
- **Vec<u8>**
|
||||
|
||||
将状态设置为 `OK` ,将 `Content-Type` 设置为
|
||||
`application/octet-stream`. vector 的数据用作 body。
|
||||
|
||||
- **Body**
|
||||
|
||||
将状态设置为 `OK` 并使用指定的 body。
|
||||
|
||||
- **StatusCode**
|
||||
|
||||
将状态设置为指定的状态代码 `StatusCode` ,body 为空。
|
||||
|
||||
- **(StatusCode, T)**
|
||||
|
||||
将 `T` 转换为响应并设置指定的状态代码 `StatusCode`。
|
||||
|
||||
- **(StatusCode, HeaderMap, T)**
|
||||
|
||||
将 `T` 转换为响应并设置指定的状态代码 `StatusCode`,然后合并指定的`HeaderMap`。
|
||||
|
||||
- **Response**
|
||||
|
||||
`Response` 的实现者总是返回自身。
|
||||
|
||||
- **Compress<T>**
|
||||
|
||||
调用 `T::into_response` 获取响应,然后使用指定的算法压缩响应 body ,并设置正确的 `Content-Encoding`标头。
|
||||
|
||||
- **SSE**
|
||||
|
||||
将状态设置为 `OK` ,将 `Content-Type` 设置为 `text/event-stream`,并带有事件流 body。使用 `SSE::new` 函数来创建它。
|
||||
|
||||
## 自定义响应
|
||||
|
||||
在下面的示例中,我们包装了一个名为 `PDF` 的响应,它向响应添加了一个 `Content-Type: applicationn/pdf` 标头。
|
||||
|
||||
```rust
|
||||
use poem::{IntoResponse, Response};
|
||||
|
||||
struct PDF(Vec<u8>);
|
||||
|
||||
impl IntoResponse for PDF {
|
||||
fn into_response(self) -> Response {
|
||||
Response::builder()
|
||||
.header("Content-Type", "application/pdf")
|
||||
.body(self.0)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,76 +0,0 @@
|
||||
# 路由
|
||||
|
||||
路由对象用于将指定路径和方法的请求分派到指定 Endpoint。
|
||||
|
||||
路由对象实际上是一个 Endpoint,它实现了 `Endpoint` trait。
|
||||
|
||||
在下面的例子中,我们将 `/a` 和 `/b` 的请求分派到不同的 Endpoint。
|
||||
|
||||
```rust
|
||||
use poem::{handler, Route};
|
||||
|
||||
#[handler]
|
||||
async fn a() -> &'static str { "a" }
|
||||
|
||||
#[handler]
|
||||
async fn b() -> &'static str { "b" }
|
||||
|
||||
let ep = Route::new()
|
||||
.at("/a", a)
|
||||
.at("/b", b);
|
||||
```
|
||||
|
||||
## 捕获变量
|
||||
|
||||
使用`:NAME`捕获路径中指定段的值,或者使用`*NAME`捕获路径中的所有指定前缀的值。
|
||||
|
||||
在下面的示例中,捕获的值将存储在变量 `value` 中,你可以使用路径提取器来获取它们。
|
||||
|
||||
```rust
|
||||
#[handler]
|
||||
async fn a(Path(String): Path<String>) {}
|
||||
|
||||
let ep = Route::new()
|
||||
.at("/a/:value/b", handler)
|
||||
.at("/prefix/*value", handler);
|
||||
```
|
||||
|
||||
## 正则表达式
|
||||
|
||||
可以使用正则表达式进行匹配,`<REGEX>` 或`:NAME<REGEX>`,第二个可以将匹配的值捕获到一个变量中。
|
||||
|
||||
```rust
|
||||
let ep = Route::new()
|
||||
.at("/a/<\\d+>", handler)
|
||||
.at("/b/:value<\\d+>", handler);
|
||||
```
|
||||
|
||||
## 嵌套
|
||||
|
||||
有时我们想为指定的 Endpoint 分配一个带有指定前缀的路径,以便创建一些功能独立的组件。
|
||||
|
||||
在下面的例子中,`hello` Endpoint 的请求路径是 `/api/hello`。
|
||||
|
||||
```rust
|
||||
let api = Route::new().at("/hello", hello);
|
||||
let ep = api.nest("/api", api);
|
||||
```
|
||||
|
||||
静态文件服务就是这样一个独立的组件。
|
||||
|
||||
```rust
|
||||
let ep = Route::new().nest("/files", Files::new("./static_files"));
|
||||
```
|
||||
|
||||
## 方法路由
|
||||
|
||||
上面介绍的路由对象只能通过一些指定的路径进行调度,但是通过路径和方法进行调度更常见。 `Poem` 提供了另一个路由对象 `RouteMethod`,当它与 `Route` 对象结合时,它可以提供这种能力。
|
||||
|
||||
`Poem` 提供了一些方便的函数来创建 `RouteMethod` 对象,它们都以 HTTP 标准方法命名。
|
||||
|
||||
```rust
|
||||
use poem::{Route, get, post};
|
||||
|
||||
let ep = Route::new()
|
||||
.at("/users", get(get_user).post(create_user).delete(delete_user).put(update_user));
|
||||
```
|
||||
@@ -16,7 +16,7 @@ impl Api {
|
||||
#[oai(path = "/basic", method = "get")]
|
||||
async fn auth_basic(&self, auth: MyBasicAuthorization) -> Result<PlainText<String>> {
|
||||
if auth.0.username != "test" || auth.0.password != "123456" {
|
||||
return Err(Error::new(StatusCode::UNAUTHORIZED));
|
||||
return Err(Error::new_with_status(StatusCode::UNAUTHORIZED));
|
||||
}
|
||||
Ok(PlainText(format!("hello: {}", auth.0.username)))
|
||||
}
|
||||
|
||||
@@ -34,15 +34,15 @@ struct BasicAuthEndpoint<E> {
|
||||
|
||||
#[poem::async_trait]
|
||||
impl<E: Endpoint> Endpoint for BasicAuthEndpoint<E> {
|
||||
type Output = Result<E::Output>;
|
||||
type Output = E::Output;
|
||||
|
||||
async fn call(&self, req: Request) -> Self::Output {
|
||||
async fn call(&self, req: Request) -> Result<Self::Output> {
|
||||
if let Some(auth) = req.headers().typed_get::<headers::Authorization<Basic>>() {
|
||||
if auth.0.username() == self.username && auth.0.password() == self.password {
|
||||
return Ok(self.ep.call(req).await);
|
||||
return self.ep.call(req).await;
|
||||
}
|
||||
}
|
||||
Err(Error::new(StatusCode::UNAUTHORIZED))
|
||||
Err(Error::new_with_status(StatusCode::UNAUTHORIZED))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ struct LoginRequest {
|
||||
#[handler]
|
||||
async fn login(verifier: &CsrfVerifier, Form(req): Form<LoginRequest>) -> Result<String> {
|
||||
if !verifier.is_valid(&req.csrf_token) {
|
||||
return Err(Error::new(StatusCode::UNAUTHORIZED).with_reason("unauthorized"));
|
||||
return Err(Error::new_with_status(StatusCode::UNAUTHORIZED));
|
||||
}
|
||||
|
||||
Ok(format!("login success: {}", req.username))
|
||||
|
||||
@@ -7,5 +7,5 @@ publish = false
|
||||
[dependencies]
|
||||
poem = { path = "../../../poem" }
|
||||
tokio = { version = "1.12.0", features = ["rt-multi-thread", "macros"] }
|
||||
serde = { version = "1.0.130", features = ["derive"] }
|
||||
tracing-subscriber = "0.2.24"
|
||||
thiserror = "1.0.30"
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
use poem::{
|
||||
get, handler, http::StatusCode, listener::TcpListener, web::Json, IntoResponse, Response,
|
||||
Route, Server,
|
||||
error::ResponseError, get, handler, http::StatusCode, listener::TcpListener, Result, Route,
|
||||
Server,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("{message}")]
|
||||
struct CustomError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl IntoResponse for CustomError {
|
||||
fn into_response(self) -> Response {
|
||||
Json(self)
|
||||
.with_status(StatusCode::BAD_REQUEST)
|
||||
.into_response()
|
||||
impl ResponseError for CustomError {
|
||||
fn status(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
fn hello() -> Result<String, CustomError> {
|
||||
fn hello() -> Result<String> {
|
||||
Err(CustomError {
|
||||
message: "custom error".to_string(),
|
||||
})
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
||||
@@ -1,34 +1,21 @@
|
||||
use poem::{
|
||||
get, handler, http::StatusCode, listener::TcpListener, FromRequest, IntoResponse, Request,
|
||||
RequestBody, Response, Route, Server,
|
||||
get, handler, http::StatusCode, listener::TcpListener, Error, FromRequest, Request,
|
||||
RequestBody, Result, Route, Server,
|
||||
};
|
||||
|
||||
struct Token(String);
|
||||
|
||||
// Error type for Token extractor
|
||||
#[derive(Debug)]
|
||||
struct MissingToken;
|
||||
|
||||
/// custom-error can also be reused
|
||||
impl IntoResponse for MissingToken {
|
||||
fn into_response(self) -> Response {
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body("missing token")
|
||||
}
|
||||
}
|
||||
|
||||
// Implements a token extractor
|
||||
#[poem::async_trait]
|
||||
impl<'a> FromRequest<'a> for Token {
|
||||
type Error = MissingToken;
|
||||
|
||||
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self, Self::Error> {
|
||||
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
|
||||
let token = req
|
||||
.headers()
|
||||
.get("MyToken")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.ok_or(MissingToken)?;
|
||||
.ok_or_else(|| {
|
||||
Error::new_with_string("missing token").with_status(StatusCode::BAD_REQUEST)
|
||||
})?;
|
||||
Ok(Token(token.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use poem::{
|
||||
get, handler, http::StatusCode, listener::TcpListener, web::Path, EndpointExt, Response, Route,
|
||||
Server,
|
||||
error::NotFoundError, get, handler, http::StatusCode, listener::TcpListener, web::Path,
|
||||
EndpointExt, Response, Route, Server,
|
||||
};
|
||||
|
||||
#[handler]
|
||||
@@ -15,17 +15,14 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
}
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let app = Route::new()
|
||||
.at("/hello/:name", get(hello))
|
||||
.after(|resp| async move {
|
||||
if resp.status() == StatusCode::NOT_FOUND {
|
||||
let app =
|
||||
Route::new()
|
||||
.at("/hello/:name", get(hello))
|
||||
.catch_error(|_: NotFoundError| async move {
|
||||
Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body("haha")
|
||||
} else {
|
||||
resp
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Server::new(TcpListener::bind("127.0.0.1:3000"))
|
||||
.run(app)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use poem::{
|
||||
async_trait, get, handler, listener::TcpListener, Endpoint, EndpointExt, IntoResponse,
|
||||
Middleware, Request, Response, Route, Server,
|
||||
Middleware, Request, Response, Result, Route, Server,
|
||||
};
|
||||
|
||||
struct Log;
|
||||
@@ -19,15 +19,21 @@ struct LogImpl<E>(E);
|
||||
impl<E: Endpoint> Endpoint for LogImpl<E> {
|
||||
type Output = Response;
|
||||
|
||||
async fn call(&self, req: Request) -> Self::Output {
|
||||
async fn call(&self, req: Request) -> Result<Self::Output> {
|
||||
println!("request: {}", req.uri().path());
|
||||
let resp = self.0.call(req).await.into_response();
|
||||
if resp.status().is_success() {
|
||||
println!("response: {}", resp.status());
|
||||
} else {
|
||||
println!("error: {}", resp.status());
|
||||
let res = self.0.call(req).await;
|
||||
|
||||
match res {
|
||||
Ok(resp) => {
|
||||
let resp = resp.into_response();
|
||||
println!("response: {}", resp.status());
|
||||
Ok(resp)
|
||||
}
|
||||
Err(err) => {
|
||||
println!("error: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
resp
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use poem::{
|
||||
get, handler, listener::TcpListener, Endpoint, EndpointExt, IntoResponse, Request, Response,
|
||||
Route, Server,
|
||||
Result, Route, Server,
|
||||
};
|
||||
|
||||
#[handler]
|
||||
@@ -8,15 +8,21 @@ fn index() -> String {
|
||||
"hello".to_string()
|
||||
}
|
||||
|
||||
async fn log<E: Endpoint>(next: E, req: Request) -> Response {
|
||||
async fn log<E: Endpoint>(next: E, req: Request) -> Result<Response> {
|
||||
println!("request: {}", req.uri().path());
|
||||
let resp = next.call(req).await.into_response();
|
||||
if resp.status().is_success() {
|
||||
println!("response: {}", resp.status());
|
||||
} else {
|
||||
println!("error: {}", resp.status());
|
||||
let res = next.call(req).await;
|
||||
|
||||
match res {
|
||||
Ok(resp) => {
|
||||
let resp = resp.into_response();
|
||||
println!("response: {}", resp.status());
|
||||
Ok(resp)
|
||||
}
|
||||
Err(err) => {
|
||||
println!("error: {}", err);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
resp
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@@ -32,8 +32,8 @@
|
||||
//! let route = Route::new().at("/", index).with(ServerSession::new(CookieConfig::new(),storage));
|
||||
//! ```
|
||||
|
||||
#![doc(html_favicon_url = "https://poem.rs/assets/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://poem.rs/en/assets/logo.png")]
|
||||
#![doc(html_favicon_url = "https://raw.githubusercontent.com/poem-web/poem/master/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/poem-web/poem/master/logo.png")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(private_in_public, unreachable_pub)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::BTreeMap, time::Duration};
|
||||
|
||||
use chrono::Utc;
|
||||
use poem::{session::SessionStorage, Result};
|
||||
use poem::{error::InternalServerError, session::SessionStorage, Result};
|
||||
use sqlx::{mysql::MySqlStatement, types::Json, Executor, MySqlPool, Statement};
|
||||
|
||||
use crate::DatabaseConfig;
|
||||
@@ -28,6 +28,10 @@ const CLEANUP_SQL: &str = r#"
|
||||
|
||||
/// Session storage using Mysql.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`sqlx::Error`]
|
||||
///
|
||||
/// # Create the table for session storage
|
||||
///
|
||||
/// ```sql
|
||||
@@ -103,14 +107,15 @@ impl MysqlSessionStorage {
|
||||
#[poem::async_trait]
|
||||
impl SessionStorage for MysqlSessionStorage {
|
||||
async fn load_session(&self, session_id: &str) -> Result<Option<BTreeMap<String, String>>> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let mut conn = self.pool.acquire().await.map_err(InternalServerError)?;
|
||||
let res: Option<(Json<BTreeMap<String, String>>,)> = self
|
||||
.load_stmt
|
||||
.query_as()
|
||||
.bind(session_id)
|
||||
.bind(Utc::now())
|
||||
.fetch_optional(&mut conn)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(res.map(|(value,)| value.0))
|
||||
}
|
||||
|
||||
@@ -120,10 +125,12 @@ impl SessionStorage for MysqlSessionStorage {
|
||||
entries: &BTreeMap<String, String>,
|
||||
expires: Option<Duration>,
|
||||
) -> Result<()> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let mut conn = self.pool.acquire().await.map_err(InternalServerError)?;
|
||||
|
||||
let expires = match expires {
|
||||
Some(expires) => Some(chrono::Duration::from_std(expires)?),
|
||||
Some(expires) => {
|
||||
Some(chrono::Duration::from_std(expires).map_err(InternalServerError)?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
@@ -133,17 +140,19 @@ impl SessionStorage for MysqlSessionStorage {
|
||||
.bind(Json(entries))
|
||||
.bind(expires.map(|expires| Utc::now() + expires))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_session(&self, session_id: &str) -> Result<()> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let mut conn = self.pool.acquire().await.map_err(InternalServerError)?;
|
||||
self.remove_stmt
|
||||
.query()
|
||||
.bind(session_id)
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::BTreeMap, time::Duration};
|
||||
|
||||
use chrono::Utc;
|
||||
use poem::{session::SessionStorage, Result};
|
||||
use poem::{error::InternalServerError, session::SessionStorage, Result};
|
||||
use sqlx::{postgres::PgStatement, types::Json, Executor, PgPool, Statement};
|
||||
|
||||
use crate::DatabaseConfig;
|
||||
@@ -28,6 +28,10 @@ const CLEANUP_SQL: &str = r#"
|
||||
|
||||
/// Session storage using Postgres.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`sqlx::Error`]
|
||||
///
|
||||
/// # Create the table for session storage
|
||||
///
|
||||
/// ```sql
|
||||
@@ -101,14 +105,15 @@ impl PgSessionStorage {
|
||||
#[poem::async_trait]
|
||||
impl SessionStorage for PgSessionStorage {
|
||||
async fn load_session(&self, session_id: &str) -> Result<Option<BTreeMap<String, String>>> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let mut conn = self.pool.acquire().await.map_err(InternalServerError)?;
|
||||
let res: Option<(Json<BTreeMap<String, String>>,)> = self
|
||||
.load_stmt
|
||||
.query_as()
|
||||
.bind(session_id)
|
||||
.bind(Utc::now())
|
||||
.fetch_optional(&mut conn)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(res.map(|(value,)| value.0))
|
||||
}
|
||||
|
||||
@@ -118,10 +123,12 @@ impl SessionStorage for PgSessionStorage {
|
||||
entries: &BTreeMap<String, String>,
|
||||
expires: Option<Duration>,
|
||||
) -> Result<()> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let mut conn = self.pool.acquire().await.map_err(InternalServerError)?;
|
||||
|
||||
let expires = match expires {
|
||||
Some(expires) => Some(chrono::Duration::from_std(expires)?),
|
||||
Some(expires) => {
|
||||
Some(chrono::Duration::from_std(expires).map_err(InternalServerError)?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
@@ -131,17 +138,19 @@ impl SessionStorage for PgSessionStorage {
|
||||
.bind(Json(entries))
|
||||
.bind(expires.map(|expires| Utc::now() + expires))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_session(&self, session_id: &str) -> Result<()> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let mut conn = self.pool.acquire().await.map_err(InternalServerError)?;
|
||||
self.remove_stmt
|
||||
.query()
|
||||
.bind(session_id)
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{collections::BTreeMap, time::Duration};
|
||||
|
||||
use chrono::Utc;
|
||||
use poem::{session::SessionStorage, Result};
|
||||
use poem::{error::InternalServerError, session::SessionStorage, Result};
|
||||
use sqlx::{sqlite::SqliteStatement, types::Json, Executor, SqlitePool, Statement};
|
||||
|
||||
use crate::DatabaseConfig;
|
||||
@@ -28,6 +28,10 @@ const CLEANUP_SQL: &str = r#"
|
||||
|
||||
/// Session storage using Sqlite.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`sqlx::Error`]
|
||||
///
|
||||
/// # Create the table for session storage
|
||||
///
|
||||
/// ```sql
|
||||
@@ -99,14 +103,15 @@ impl SqliteSessionStorage {
|
||||
#[poem::async_trait]
|
||||
impl SessionStorage for SqliteSessionStorage {
|
||||
async fn load_session(&self, session_id: &str) -> Result<Option<BTreeMap<String, String>>> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let mut conn = self.pool.acquire().await.map_err(InternalServerError)?;
|
||||
let res: Option<(Json<BTreeMap<String, String>>,)> = self
|
||||
.load_stmt
|
||||
.query_as()
|
||||
.bind(session_id)
|
||||
.bind(Utc::now())
|
||||
.fetch_optional(&mut conn)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(res.map(|(value,)| value.0))
|
||||
}
|
||||
|
||||
@@ -116,10 +121,12 @@ impl SessionStorage for SqliteSessionStorage {
|
||||
entries: &BTreeMap<String, String>,
|
||||
expires: Option<Duration>,
|
||||
) -> Result<()> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let mut conn = self.pool.acquire().await.map_err(InternalServerError)?;
|
||||
|
||||
let expires = match expires {
|
||||
Some(expires) => Some(chrono::Duration::from_std(expires)?),
|
||||
Some(expires) => {
|
||||
Some(chrono::Duration::from_std(expires).map_err(InternalServerError)?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
@@ -129,17 +136,19 @@ impl SessionStorage for SqliteSessionStorage {
|
||||
.bind(Json(entries))
|
||||
.bind(expires.map(|expires| Utc::now() + expires))
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_session(&self, session_id: &str) -> Result<()> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let mut conn = self.pool.acquire().await.map_err(InternalServerError)?;
|
||||
self.remove_stmt
|
||||
.query()
|
||||
.bind(session_id)
|
||||
.execute(&mut conn)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(InternalServerError)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Macros for poem
|
||||
|
||||
#![doc(html_favicon_url = "https://poem.rs/assets/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://poem.rs/en/assets/logo.png")]
|
||||
#![doc(html_favicon_url = "https://raw.githubusercontent.com/poem-web/poem/master/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/poem-web/poem/master/logo.png")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(private_in_public, unreachable_pub)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
@@ -64,10 +64,7 @@ fn generate_handler(internal: bool, input: TokenStream) -> Result<TokenStream> {
|
||||
let id = quote::format_ident!("p{}", idx);
|
||||
args.push(id.clone());
|
||||
extractors.push(quote! {
|
||||
let #id = match <#ty as #crate_name::FromRequest>::from_request(&req, &mut body).await {
|
||||
Ok(value) => value,
|
||||
Err(err) => return #crate_name::IntoResponse::into_response(err),
|
||||
};
|
||||
let #id = <#ty as #crate_name::FromRequest>::from_request(&req, &mut body).await?;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -82,11 +79,13 @@ fn generate_handler(internal: bool, input: TokenStream) -> Result<TokenStream> {
|
||||
type Output = #crate_name::Response;
|
||||
|
||||
#[allow(unused_mut)]
|
||||
async fn call(&self, mut req: #crate_name::Request) -> Self::Output {
|
||||
async fn call(&self, mut req: #crate_name::Request) -> #crate_name::Result<Self::Output> {
|
||||
let (req, mut body) = req.split();
|
||||
#(#extractors)*
|
||||
#item_fn
|
||||
#crate_name::IntoResponse::into_response(#ident(#(#args),*)#call_await)
|
||||
let res = #ident(#(#args),*)#call_await;
|
||||
let res = #crate_name::error::IntoResult::into_result(res);
|
||||
std::result::Result::map(res, #crate_name::IntoResponse::into_response)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
//! Poem for AWS Lambda.
|
||||
|
||||
#![doc(html_favicon_url = "https://poem.rs/assets/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://poem.rs/en/assets/logo.png")]
|
||||
#![doc(html_favicon_url = "https://raw.githubusercontent.com/poem-web/poem/master/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/poem-web/poem/master/logo.png")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(private_in_public, unreachable_pub)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use std::{convert::Infallible, io::ErrorKind, ops::Deref, sync::Arc};
|
||||
use std::{io::ErrorKind, ops::Deref, sync::Arc};
|
||||
|
||||
pub use lambda_http::lambda_runtime::Error;
|
||||
use lambda_http::{handler, lambda_runtime, Body as LambdaBody, Request as LambdaRequest};
|
||||
use poem::{Body, Endpoint, EndpointExt, FromRequest, IntoEndpoint, Request, RequestBody};
|
||||
use poem::{Body, Endpoint, EndpointExt, FromRequest, IntoEndpoint, Request, RequestBody, Result};
|
||||
|
||||
/// The Lambda function execution context.
|
||||
///
|
||||
@@ -65,7 +65,10 @@ pub async fn run(ep: impl IntoEndpoint) -> Result<(), Error> {
|
||||
let mut req: Request = from_lambda_request(req);
|
||||
req.extensions_mut().insert(Context(ctx));
|
||||
|
||||
let resp = ep.call(req).await;
|
||||
let resp = match ep.call(req).await {
|
||||
Ok(resp) => resp,
|
||||
Err(err) => err.as_response(),
|
||||
};
|
||||
|
||||
let (parts, body) = resp.into_parts();
|
||||
let data = body
|
||||
@@ -111,9 +114,7 @@ fn from_lambda_request(req: LambdaRequest) -> Request {
|
||||
|
||||
#[poem::async_trait]
|
||||
impl<'a> FromRequest<'a> for &'a Context {
|
||||
type Error = Infallible;
|
||||
|
||||
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self, Self::Error> {
|
||||
async fn from_request(req: &'a Request, _body: &mut RequestBody) -> Result<Self> {
|
||||
let ctx = match req.extensions().get::<Context>() {
|
||||
Some(ctx) => ctx,
|
||||
None => panic!("Lambda runtime is required."),
|
||||
|
||||
@@ -289,10 +289,11 @@ fn generate_operation(
|
||||
let #pname = match <#arg_ty as #crate_name::ApiExtractor>::from_request(&request, &mut body, param_opts).await {
|
||||
::std::result::Result::Ok(value) => value,
|
||||
::std::result::Result::Err(err) if <#res_ty as #crate_name::ApiResponse>::BAD_REQUEST_HANDLER => {
|
||||
let resp = <#res_ty as #crate_name::ApiResponse>::from_parse_request_error(err);
|
||||
return #crate_name::__private::poem::IntoResponse::into_response(resp);
|
||||
let res = <#res_ty as #crate_name::ApiResponse>::from_parse_request_error(err);
|
||||
let res = #crate_name::__private::poem::error::IntoResult::into_result(res);
|
||||
return ::std::result::Result::map(res, #crate_name::__private::poem::IntoResponse::into_response);
|
||||
}
|
||||
::std::result::Result::Err(err) => return #crate_name::__private::poem::IntoResponse::into_response(err),
|
||||
::std::result::Result::Err(err) => return ::std::result::Result::Err(::std::convert::Into::into(err)),
|
||||
};
|
||||
#param_checker
|
||||
});
|
||||
@@ -358,8 +359,9 @@ fn generate_operation(
|
||||
async move {
|
||||
let (request, mut body) = request.split();
|
||||
#(#parse_args)*
|
||||
let resp = api_obj.#fn_ident(#(#use_args),*).await;
|
||||
#crate_name::__private::poem::IntoResponse::into_response(resp)
|
||||
let res = api_obj.#fn_ident(#(#use_args),*).await;
|
||||
let res = #crate_name::__private::poem::error::IntoResult::into_result(res);
|
||||
::std::result::Result::map(res, #crate_name::__private::poem::IntoResponse::into_response)
|
||||
}
|
||||
});
|
||||
#transform
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Macros for poem-openapi
|
||||
|
||||
#![doc(html_favicon_url = "https://poem.rs/assets/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://poem.rs/en/assets/logo.png")]
|
||||
#![doc(html_favicon_url = "https://raw.githubusercontent.com/poem-web/poem/master/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/poem-web/poem/master/logo.png")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(private_in_public, unreachable_pub)]
|
||||
|
||||
|
||||
@@ -95,10 +95,9 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
fields.push(field_ident);
|
||||
|
||||
let parse_err = quote! {{
|
||||
let resp = #crate_name::__private::poem::Response::builder()
|
||||
.status(#crate_name::__private::poem::http::StatusCode::BAD_REQUEST)
|
||||
.body(::std::format!("failed to parse field `{}`: {}", #field_name, err.into_message()));
|
||||
#crate_name::ParseRequestError::ParseRequestBody(resp)
|
||||
#crate_name::error::ParseMultipartError {
|
||||
reason: ::std::format!("failed to parse field `{}`: {}", #field_name, err.into_message()),
|
||||
}
|
||||
}};
|
||||
|
||||
deserialize_fields.push(quote! {
|
||||
@@ -143,11 +142,9 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
},
|
||||
::std::option::Option::None => {
|
||||
<#field_ty as #crate_name::types::ParseFromMultipartField>::parse_from_multipart(::std::option::Option::None).await.map_err(|_|
|
||||
#crate_name::ParseRequestError::ParseRequestBody(
|
||||
#crate_name::__private::poem::Response::builder()
|
||||
.status(#crate_name::__private::poem::http::StatusCode::BAD_REQUEST)
|
||||
.body(::std::format!("field `{}` is required", #field_name))
|
||||
)
|
||||
#crate_name::error::ParseMultipartError {
|
||||
reason: ::std::format!("field `{}` is required", #field_name),
|
||||
}
|
||||
)?
|
||||
}
|
||||
};
|
||||
@@ -211,10 +208,9 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
let deny_unknown_fields = if args.deny_unknown_fields {
|
||||
Some(quote! {
|
||||
if let ::std::option::Option::Some(name) = field.name() {
|
||||
let resp = #crate_name::__private::poem::Response::builder()
|
||||
.status(#crate_name::__private::poem::http::StatusCode::BAD_REQUEST)
|
||||
.body(::std::format!("unknown field `{}`", name));
|
||||
return ::std::result::Result::Err(#crate_name::ParseRequestError::ParseRequestBody(resp));
|
||||
return ::std::result::Result::Err(::std::convert::Into::into(#crate_name::error::ParseMultipartError {
|
||||
reason: ::std::format!("unknown field `{}`", name),
|
||||
}));
|
||||
}
|
||||
})
|
||||
} else {
|
||||
@@ -248,12 +244,11 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
impl #impl_generics #crate_name::payload::ParsePayload for #ident #ty_generics #where_clause {
|
||||
const IS_REQUIRED: bool = true;
|
||||
|
||||
async fn from_request(request: &#crate_name::__private::poem::Request, body: &mut #crate_name::__private::poem::RequestBody) -> ::std::result::Result<Self, #crate_name::ParseRequestError> {
|
||||
let mut multipart = <#crate_name::__private::poem::web::Multipart as #crate_name::__private::poem::FromRequest>::from_request(request, body).await
|
||||
.map_err(|err| #crate_name::ParseRequestError::ParseRequestBody(#crate_name::__private::poem::IntoResponse::into_response(err)))?;
|
||||
async fn from_request(request: &#crate_name::__private::poem::Request, body: &mut #crate_name::__private::poem::RequestBody) -> #crate_name::__private::poem::Result<Self> {
|
||||
let mut multipart = <#crate_name::__private::poem::web::Multipart as #crate_name::__private::poem::FromRequest>::from_request(request, body).await?;
|
||||
#(#skip_fields)*
|
||||
#(let mut #fields = ::std::option::Option::None;)*
|
||||
while let ::std::option::Option::Some(field) = multipart.next_field().await.map_err(|err| #crate_name::ParseRequestError::ParseRequestBody(#crate_name::__private::poem::IntoResponse::into_response(err)))? {
|
||||
while let ::std::option::Option::Some(field) = multipart.next_field().await? {
|
||||
#(#deserialize_fields)*
|
||||
#deny_unknown_fields
|
||||
}
|
||||
@@ -288,27 +283,27 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
request: &'__request #crate_name::__private::poem::Request,
|
||||
body: &mut #crate_name::__private::poem::RequestBody,
|
||||
_param_opts: #crate_name::ExtractParamOptions<Self::ParamType>,
|
||||
) -> ::std::result::Result<Self, #crate_name::ParseRequestError> {
|
||||
) -> #crate_name::__private::poem::Result<Self> {
|
||||
match request.content_type() {
|
||||
::std::option::Option::Some(content_type) => {
|
||||
let mime: #crate_name::__private::mime::Mime = match content_type.parse() {
|
||||
::std::result::Result::Ok(mime) => mime,
|
||||
::std::result::Result::Err(_) => {
|
||||
return ::std::result::Result::Err(#crate_name::ParseRequestError::ContentTypeNotSupported {
|
||||
return ::std::result::Result::Err(::std::convert::Into::into(#crate_name::error::ContentTypeError::NotSupported {
|
||||
content_type: ::std::string::ToString::to_string(&content_type),
|
||||
});
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
if mime.essence_str() != <Self as #crate_name::payload::Payload>::CONTENT_TYPE {
|
||||
return ::std::result::Result::Err(#crate_name::ParseRequestError::ContentTypeNotSupported {
|
||||
return ::std::result::Result::Err(::std::convert::Into::into(#crate_name::error::ContentTypeError::NotSupported {
|
||||
content_type: ::std::string::ToString::to_string(&content_type),
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
<Self as #crate_name::payload::ParsePayload>::from_request(request, body).await
|
||||
}
|
||||
::std::option::Option::None => ::std::result::Result::Err(#crate_name::ParseRequestError::ExpectContentType),
|
||||
::std::option::Option::None => ::std::result::Result::Err(::std::convert::Into::into(#crate_name::error::ContentTypeError::ExpectContentType)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,14 +117,15 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
request: &'__request #crate_name::__private::poem::Request,
|
||||
body: &mut #crate_name::__private::poem::RequestBody,
|
||||
_param_opts: #crate_name::ExtractParamOptions<Self::ParamType>,
|
||||
) -> ::std::result::Result<Self, #crate_name::ParseRequestError> {
|
||||
) -> #crate_name::__private::poem::Result<Self> {
|
||||
let content_type = request.content_type();
|
||||
match content_type {
|
||||
#(#from_requests)*
|
||||
::std::option::Option::Some(content_type) => ::std::result::Result::Err(#crate_name::ParseRequestError::ContentTypeNotSupported {
|
||||
content_type: ::std::string::ToString::to_string(content_type),
|
||||
}),
|
||||
::std::option::Option::None => ::std::result::Result::Err(#crate_name::ParseRequestError::ExpectContentType),
|
||||
::std::option::Option::Some(content_type) => ::std::result::Result::Err(
|
||||
::std::convert::Into::into(#crate_name::error::ContentTypeError::NotSupported {
|
||||
content_type: ::std::string::ToString::to_string(content_type),
|
||||
})),
|
||||
::std::option::Option::None => ::std::result::Result::Err(::std::convert::Into::into(#crate_name::error::ContentTypeError::ExpectContentType)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
};
|
||||
let bad_request_handler = args.bad_request_handler.as_ref().map(|path| {
|
||||
quote! {
|
||||
fn from_parse_request_error(err: #crate_name::ParseRequestError) -> Self {
|
||||
fn from_parse_request_error(err: #crate_name::__private::poem::Error) -> Self {
|
||||
#path(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,7 +446,7 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
args.generate_register_security_scheme(&crate_name, &oai_typename)?;
|
||||
let from_request = args.generate_from_request(&crate_name);
|
||||
let checker = args.checker.as_ref().map(|path| quote! {
|
||||
let output = ::std::option::Option::ok_or(#path(&req, output).await, #crate_name::ParseRequestError::Authorization)?;
|
||||
let output = ::std::option::Option::ok_or(#path(&req, output).await, #crate_name::error::AuthorizationError)?;
|
||||
});
|
||||
|
||||
let expanded = quote! {
|
||||
@@ -469,7 +469,7 @@ pub(crate) fn generate(args: DeriveInput) -> GeneratorResult<TokenStream> {
|
||||
req: &'a #crate_name::__private::poem::Request,
|
||||
body: &mut #crate_name::__private::poem::RequestBody,
|
||||
_param_opts: #crate_name::ExtractParamOptions<Self::ParamType>,
|
||||
) -> ::std::result::Result<Self, #crate_name::ParseRequestError> {
|
||||
) -> #crate_name::__private::poem::Result<Self> {
|
||||
let query = req.extensions().get::<#crate_name::__private::UrlQuery>().unwrap();
|
||||
let output = #from_request?;
|
||||
#checker
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use darling::{util::SpannedValue, FromMeta};
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use regex::Regex;
|
||||
use syn::{Error, Type};
|
||||
@@ -134,16 +134,6 @@ impl Validators {
|
||||
Ok((container_validators, elem_validators))
|
||||
}
|
||||
|
||||
fn first_container_validator_span(&self) -> Option<Span> {
|
||||
self.max_items
|
||||
.as_ref()
|
||||
.map(SpannedValue::span)
|
||||
.or_else(|| self.min_items.as_ref().map(SpannedValue::span))
|
||||
.or_else(|| self.unique_items.as_ref().map(SpannedValue::span))
|
||||
.or_else(|| self.max_properties.as_ref().map(SpannedValue::span))
|
||||
.or_else(|| self.min_properties.as_ref().map(SpannedValue::span))
|
||||
}
|
||||
|
||||
pub(crate) fn create_obj_field_checker(
|
||||
&self,
|
||||
crate_name: &TokenStream,
|
||||
@@ -162,8 +152,8 @@ impl Validators {
|
||||
)*
|
||||
|
||||
#(
|
||||
if let ::std::option::Option::Some(value) = #crate_name::types::Type::as_raw_value(&value) { let
|
||||
validator = #container_validators;
|
||||
if let ::std::option::Option::Some(value) = #crate_name::types::Type::as_raw_value(&value) {
|
||||
let validator = #container_validators;
|
||||
if !#crate_name::validation::Validator::check(&validator, value) {
|
||||
return Err(#crate_name::types::ParseError::<Self>::custom(format!("field `{}` verification failed. {}", #field_name, validator)));
|
||||
}
|
||||
@@ -180,33 +170,40 @@ impl Validators {
|
||||
) -> GeneratorResult<Option<TokenStream>> {
|
||||
let (container_validators, elem_validators) = self.create_validators(crate_name)?;
|
||||
|
||||
if !container_validators.is_empty() {
|
||||
return Err(Error::new(
|
||||
self.first_container_validator_span().unwrap(),
|
||||
"The `container` validators is not supported for parameters.",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if elem_validators.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(quote! {
|
||||
#(
|
||||
if let ::std::option::Option::Some(value) = #crate_name::types::Type::as_raw_value(&value) {
|
||||
let validator = #elem_validators;
|
||||
let validator = #container_validators;
|
||||
if !#crate_name::validation::Validator::check(&validator, value) {
|
||||
let err = #crate_name::ParseRequestError::ParseParam {
|
||||
let err = #crate_name::error::ParseParamError {
|
||||
name: #arg_name,
|
||||
reason: ::std::format!("verification failed. {}", validator),
|
||||
};
|
||||
|
||||
if <#res_ty as #crate_name::ApiResponse>::BAD_REQUEST_HANDLER {
|
||||
let resp = <#res_ty as #crate_name::ApiResponse>::from_parse_request_error(err);
|
||||
return #crate_name::__private::poem::IntoResponse::into_response(resp);
|
||||
let resp = <#res_ty as #crate_name::ApiResponse>::from_parse_request_error(std::convert::Into::into(err));
|
||||
return ::std::result::Result::Ok(#crate_name::__private::poem::IntoResponse::into_response(resp));
|
||||
} else {
|
||||
return #crate_name::__private::poem::IntoResponse::into_response(err);
|
||||
return ::std::result::Result::Err(std::convert::Into::into(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
#(
|
||||
if let ::std::option::Option::Some(value) = #crate_name::types::Type::as_raw_value(&value) {
|
||||
let validator = #elem_validators;
|
||||
if !#crate_name::validation::Validator::check(&validator, value) {
|
||||
let err = #crate_name::error::ParseParamError {
|
||||
name: #arg_name,
|
||||
reason: ::std::format!("verification failed. {}", validator),
|
||||
};
|
||||
|
||||
if <#res_ty as #crate_name::ApiResponse>::BAD_REQUEST_HANDLER {
|
||||
let resp = <#res_ty as #crate_name::ApiResponse>::from_parse_request_error(std::convert::Into::into(err));
|
||||
return ::std::result::Result::Ok(#crate_name::__private::poem::IntoResponse::into_response(resp));
|
||||
} else {
|
||||
return ::std::result::Result::Err(std::convert::Into::into(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,11 +223,9 @@ impl Validators {
|
||||
for item in #crate_name::types::Type::raw_element_iter(&value) {
|
||||
let validator = #elem_validators;
|
||||
if !#crate_name::validation::Validator::check(&validator, item) {
|
||||
return Err(#crate_name::ParseRequestError::ParseRequestBody(
|
||||
#crate_name::__private::poem::Response::builder()
|
||||
.status(#crate_name::__private::poem::http::StatusCode::BAD_REQUEST)
|
||||
.body(::std::format!("field `{}` verification failed. {}", #field_name, validator))
|
||||
));
|
||||
return Err(::std::convert::Into::into(#crate_name::error::ParseMultipartError {
|
||||
reason: ::std::format!("field `{}` verification failed. {}", #field_name, validator),
|
||||
}));
|
||||
}
|
||||
}
|
||||
)*
|
||||
@@ -239,13 +234,11 @@ impl Validators {
|
||||
if let ::std::option::Option::Some(value) = #crate_name::types::Type::as_raw_value(&value) {
|
||||
let validator = #container_validators;
|
||||
if !#crate_name::validation::Validator::check(&validator, value) {
|
||||
return Err(#crate_name::ParseRequestError::ParseRequestBody(
|
||||
#crate_name::__private::poem::Response::builder()
|
||||
.status(#crate_name::__private::poem::http::StatusCode::BAD_REQUEST)
|
||||
.body(::std::format!("field `{}` verification failed. {}", #field_name, validator))
|
||||
));
|
||||
}
|
||||
return Err(::std::convert::Into::into(#crate_name::error::ParseMultipartError {
|
||||
reason: ::std::format!("field `{}` verification failed. {}", #field_name, validator),
|
||||
}));
|
||||
}
|
||||
}
|
||||
)*
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use poem::Request;
|
||||
use poem::{Request, Result};
|
||||
|
||||
use crate::{auth::ApiKeyAuthorization, base::UrlQuery, registry::MetaParamIn, ParseRequestError};
|
||||
use crate::{
|
||||
auth::ApiKeyAuthorization, base::UrlQuery, error::AuthorizationError, registry::MetaParamIn,
|
||||
};
|
||||
|
||||
/// Used to extract the Api Key from the request.
|
||||
pub struct ApiKey {
|
||||
@@ -14,13 +16,13 @@ impl ApiKeyAuthorization for ApiKey {
|
||||
query: &UrlQuery,
|
||||
name: &str,
|
||||
in_type: MetaParamIn,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
) -> Result<Self> {
|
||||
match in_type {
|
||||
MetaParamIn::Query => query
|
||||
.get(name)
|
||||
.cloned()
|
||||
.map(|value| Self { key: value })
|
||||
.ok_or(ParseRequestError::Authorization),
|
||||
.ok_or_else(|| AuthorizationError.into()),
|
||||
MetaParamIn::Header => req
|
||||
.headers()
|
||||
.get(name)
|
||||
@@ -28,7 +30,7 @@ impl ApiKeyAuthorization for ApiKey {
|
||||
.map(|value| Self {
|
||||
key: value.to_string(),
|
||||
})
|
||||
.ok_or(ParseRequestError::Authorization),
|
||||
.ok_or_else(|| AuthorizationError.into()),
|
||||
MetaParamIn::Cookie => req
|
||||
.cookie()
|
||||
.get(name)
|
||||
@@ -36,7 +38,7 @@ impl ApiKeyAuthorization for ApiKey {
|
||||
.map(|cookie| Self {
|
||||
key: cookie.value_str().to_string(),
|
||||
})
|
||||
.ok_or(ParseRequestError::Authorization),
|
||||
.ok_or_else(|| AuthorizationError.into()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use poem::Request;
|
||||
use poem::{Request, Result};
|
||||
use typed_headers::{AuthScheme, Authorization, HeaderMapExt};
|
||||
|
||||
use crate::{auth::BasicAuthorization, ParseRequestError};
|
||||
use crate::{auth::BasicAuthorization, error::AuthorizationError};
|
||||
|
||||
/// Used to extract the username/password from the request.
|
||||
pub struct Basic {
|
||||
@@ -13,7 +13,7 @@ pub struct Basic {
|
||||
}
|
||||
|
||||
impl BasicAuthorization for Basic {
|
||||
fn from_request(req: &Request) -> Result<Self, ParseRequestError> {
|
||||
fn from_request(req: &Request) -> Result<Self> {
|
||||
if let Some(auth) = req.headers().typed_get::<Authorization>().ok().flatten() {
|
||||
if auth.0.scheme() == &AuthScheme::BASIC {
|
||||
if let Some(token68) = auth.token68() {
|
||||
@@ -34,6 +34,6 @@ impl BasicAuthorization for Basic {
|
||||
}
|
||||
}
|
||||
|
||||
Err(ParseRequestError::Authorization)
|
||||
Err(AuthorizationError.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use poem::Request;
|
||||
use poem::{Request, Result};
|
||||
use typed_headers::{AuthScheme, Authorization, HeaderMapExt};
|
||||
|
||||
use crate::{auth::BearerAuthorization, ParseRequestError};
|
||||
use crate::{auth::BearerAuthorization, error::AuthorizationError};
|
||||
|
||||
/// Used to extract the token68 from the request.
|
||||
pub struct Bearer {
|
||||
@@ -10,7 +10,7 @@ pub struct Bearer {
|
||||
}
|
||||
|
||||
impl BearerAuthorization for Bearer {
|
||||
fn from_request(req: &Request) -> Result<Self, ParseRequestError> {
|
||||
fn from_request(req: &Request) -> Result<Self> {
|
||||
if let Some(auth) = req.headers().typed_get::<Authorization>().ok().flatten() {
|
||||
if auth.0.scheme() == &AuthScheme::BEARER {
|
||||
if let Some(token68) = auth.token68() {
|
||||
@@ -21,6 +21,6 @@ impl BearerAuthorization for Bearer {
|
||||
}
|
||||
}
|
||||
|
||||
Err(ParseRequestError::Authorization)
|
||||
Err(AuthorizationError.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,21 @@ mod api_key;
|
||||
mod basic;
|
||||
mod bearer;
|
||||
|
||||
use poem::Request;
|
||||
use poem::{Request, Result};
|
||||
|
||||
pub use self::{api_key::ApiKey, basic::Basic, bearer::Bearer};
|
||||
use crate::{base::UrlQuery, registry::MetaParamIn, ParseRequestError};
|
||||
use crate::{base::UrlQuery, registry::MetaParamIn};
|
||||
|
||||
/// Represents a basic authorization extractor.
|
||||
pub trait BasicAuthorization: Sized {
|
||||
/// Extract from the HTTP request.
|
||||
fn from_request(req: &Request) -> Result<Self, ParseRequestError>;
|
||||
fn from_request(req: &Request) -> Result<Self>;
|
||||
}
|
||||
|
||||
/// Represents a bearer authorization extractor.
|
||||
pub trait BearerAuthorization: Sized {
|
||||
/// Extract from the HTTP request.
|
||||
fn from_request(req: &Request) -> Result<Self, ParseRequestError>;
|
||||
fn from_request(req: &Request) -> Result<Self>;
|
||||
}
|
||||
|
||||
/// Represents an api key authorization extractor.
|
||||
@@ -29,5 +29,5 @@ pub trait ApiKeyAuthorization: Sized {
|
||||
query: &UrlQuery,
|
||||
name: &str,
|
||||
in_type: MetaParamIn,
|
||||
) -> Result<Self, ParseRequestError>;
|
||||
) -> Result<Self>;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use poem::{FromRequest, IntoResponse, Request, RequestBody, Result, Route};
|
||||
use poem::{Error, FromRequest, Request, RequestBody, Result, Route};
|
||||
|
||||
use crate::{
|
||||
registry::{
|
||||
MetaApi, MetaOAuthScope, MetaParamIn, MetaRequest, MetaResponse, MetaResponses,
|
||||
MetaSchemaRef, Registry,
|
||||
},
|
||||
ParseRequestError,
|
||||
use crate::registry::{
|
||||
MetaApi, MetaOAuthScope, MetaParamIn, MetaRequest, MetaResponse, MetaResponses, MetaSchemaRef,
|
||||
Registry,
|
||||
};
|
||||
|
||||
/// API extractor types.
|
||||
@@ -38,6 +35,7 @@ impl Deref for UrlQuery {
|
||||
}
|
||||
|
||||
impl UrlQuery {
|
||||
#[allow(missing_docs)]
|
||||
pub fn get_all<'a, 'b: 'a>(&'b self, name: &'a str) -> impl Iterator<Item = &'b String> + 'a {
|
||||
self.0
|
||||
.iter()
|
||||
@@ -45,6 +43,7 @@ impl UrlQuery {
|
||||
.map(|(_, value)| value)
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub fn get(&self, name: &str) -> Option<&String> {
|
||||
self.get_all(name).next()
|
||||
}
|
||||
@@ -117,7 +116,7 @@ pub trait ApiExtractor<'a>: Sized {
|
||||
request: &'a Request,
|
||||
body: &mut RequestBody,
|
||||
param_opts: ExtractParamOptions<Self::ParamType>,
|
||||
) -> Result<Self, ParseRequestError>;
|
||||
) -> Result<Self>;
|
||||
}
|
||||
|
||||
#[poem::async_trait]
|
||||
@@ -131,18 +130,15 @@ impl<'a, T: FromRequest<'a>> ApiExtractor<'a> for T {
|
||||
request: &'a Request,
|
||||
body: &mut RequestBody,
|
||||
_param_opts: ExtractParamOptions<Self::ParamType>,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
match T::from_request(request, body).await {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => Err(ParseRequestError::Extractor(err.into_response())),
|
||||
}
|
||||
) -> Result<Self> {
|
||||
T::from_request(request, body).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a OpenAPI responses object.
|
||||
///
|
||||
/// Reference: <https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#responsesObject>
|
||||
pub trait ApiResponse: IntoResponse + Sized {
|
||||
pub trait ApiResponse: Sized {
|
||||
/// If true, it means that the response object has a custom bad request
|
||||
/// handler.
|
||||
const BAD_REQUEST_HANDLER: bool = false;
|
||||
@@ -155,7 +151,7 @@ pub trait ApiResponse: IntoResponse + Sized {
|
||||
|
||||
/// Convert [`ParseRequestError`] to this response object.
|
||||
#[allow(unused_variables)]
|
||||
fn from_parse_request_error(err: ParseRequestError) -> Self {
|
||||
fn from_parse_request_error(err: Error) -> Self {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
@@ -175,7 +171,9 @@ impl ApiResponse for () {
|
||||
fn register(_registry: &mut Registry) {}
|
||||
}
|
||||
|
||||
impl<T: ApiResponse, E: IntoResponse> ApiResponse for Result<T, E> {
|
||||
impl<T: ApiResponse> ApiResponse for Result<T> {
|
||||
const BAD_REQUEST_HANDLER: bool = T::BAD_REQUEST_HANDLER;
|
||||
|
||||
fn meta() -> MetaResponses {
|
||||
T::meta()
|
||||
}
|
||||
@@ -183,6 +181,10 @@ impl<T: ApiResponse, E: IntoResponse> ApiResponse for Result<T, E> {
|
||||
fn register(registry: &mut Registry) {
|
||||
T::register(registry);
|
||||
}
|
||||
|
||||
fn from_parse_request_error(err: Error) -> Self {
|
||||
Ok(T::from_parse_request_error(err))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a OpenAPI tags.
|
||||
|
||||
@@ -22,7 +22,8 @@ Define a OpenAPI response.
|
||||
# Examples
|
||||
|
||||
```rust
|
||||
use poem_openapi::{payload::PlainText, ApiResponse, ParseRequestError};
|
||||
use poem::Error;
|
||||
use poem_openapi::{payload::PlainText, ApiResponse};
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
#[oai(bad_request_handler = "bad_request_handler")]
|
||||
@@ -39,7 +40,7 @@ enum CreateUserResponse {
|
||||
}
|
||||
|
||||
// Convert error to `CreateUserResponse::BadRequest`.
|
||||
fn bad_request_handler(err: ParseRequestError) -> CreateUserResponse {
|
||||
fn bad_request_handler(err: Error) -> CreateUserResponse {
|
||||
CreateUserResponse::BadRequest(PlainText(format!("error: {}", err.to_string())))
|
||||
}
|
||||
```
|
||||
@@ -1,60 +1,81 @@
|
||||
use poem::{http::StatusCode, IntoResponse, Response};
|
||||
//! Some common error types.
|
||||
|
||||
use poem::{error::ResponseError, http::StatusCode};
|
||||
use thiserror::Error;
|
||||
|
||||
/// This type represents errors that occur when parsing the HTTP request.
|
||||
/// Parameter error.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ParseRequestError {
|
||||
/// Failed to parse a parameter.
|
||||
#[error("Failed to parse parameter `{name}`: {reason}")]
|
||||
ParseParam {
|
||||
/// The name of the parameter.
|
||||
name: &'static str,
|
||||
#[error("failed to parse parameter `{name}`: {reason}")]
|
||||
pub struct ParseParamError {
|
||||
/// The name of the parameter.
|
||||
pub name: &'static str,
|
||||
|
||||
/// The reason for the error.
|
||||
reason: String,
|
||||
},
|
||||
/// The reason for the error.
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
/// Failed to parse a request body.
|
||||
#[error("Failed to parse a request body")]
|
||||
ParseRequestBody(Response),
|
||||
impl ResponseError for ParseParamError {
|
||||
fn status(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
/// The `Content-Type` requested by the client is not supported.
|
||||
#[error("The `Content-Type` requested by the client is not supported: {content_type}")]
|
||||
ContentTypeNotSupported {
|
||||
/// Parse JSON error.
|
||||
#[derive(Debug, Error)]
|
||||
#[error("parse JSON error: {reason}")]
|
||||
pub struct ParseJsonError {
|
||||
/// The reason for the error.
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
impl ResponseError for ParseJsonError {
|
||||
fn status(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse multipart error.
|
||||
#[derive(Debug, Error)]
|
||||
#[error("parse multipart error: {reason}")]
|
||||
pub struct ParseMultipartError {
|
||||
/// The reason for the error.
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
impl ResponseError for ParseMultipartError {
|
||||
fn status(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
/// Content type error.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ContentTypeError {
|
||||
/// Not supported.
|
||||
#[error("the `Content-Type` requested by the client is not supported: {content_type}")]
|
||||
NotSupported {
|
||||
/// The `Content-Type` header requested by the client.
|
||||
content_type: String,
|
||||
},
|
||||
|
||||
/// The client request does not include the `Content-Type` header.
|
||||
#[error("The client request does not include the `Content-Type` header")]
|
||||
/// Expect content type header.
|
||||
#[error("the client request does not include the `Content-Type` header")]
|
||||
ExpectContentType,
|
||||
|
||||
/// Poem extractor error.
|
||||
#[error("Poem extractor error")]
|
||||
Extractor(Response),
|
||||
|
||||
/// Authorization error.
|
||||
#[error("Authorization error")]
|
||||
Authorization,
|
||||
}
|
||||
|
||||
impl IntoResponse for ParseRequestError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
ParseRequestError::ParseParam { .. } => self
|
||||
.to_string()
|
||||
.with_status(StatusCode::BAD_REQUEST)
|
||||
.into_response(),
|
||||
ParseRequestError::ContentTypeNotSupported { .. }
|
||||
| ParseRequestError::ExpectContentType => self
|
||||
.to_string()
|
||||
.with_status(StatusCode::METHOD_NOT_ALLOWED)
|
||||
.into_response(),
|
||||
ParseRequestError::ParseRequestBody(resp) | ParseRequestError::Extractor(resp) => resp,
|
||||
ParseRequestError::Authorization => self
|
||||
.to_string()
|
||||
.with_status(StatusCode::UNAUTHORIZED)
|
||||
.into_response(),
|
||||
}
|
||||
impl ResponseError for ContentTypeError {
|
||||
fn status(&self) -> StatusCode {
|
||||
StatusCode::METHOD_NOT_ALLOWED
|
||||
}
|
||||
}
|
||||
|
||||
/// Authorization error.
|
||||
#[derive(Debug, Error)]
|
||||
#[error("authorization error")]
|
||||
pub struct AuthorizationError;
|
||||
|
||||
impl ResponseError for AuthorizationError {
|
||||
fn status(&self) -> StatusCode {
|
||||
StatusCode::UNAUTHORIZED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,8 +81,8 @@
|
||||
//! hello, sunli!
|
||||
//! ```
|
||||
|
||||
#![doc(html_favicon_url = "https://poem.rs/assets/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://poem.rs/en/assets/logo.png")]
|
||||
#![doc(html_favicon_url = "https://raw.githubusercontent.com/poem-web/poem/master/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/poem-web/poem/master/logo.png")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(private_in_public, unreachable_pub)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
@@ -92,6 +92,7 @@
|
||||
mod macros;
|
||||
|
||||
pub mod auth;
|
||||
pub mod error;
|
||||
pub mod param;
|
||||
pub mod payload;
|
||||
#[doc(hidden)]
|
||||
@@ -101,7 +102,6 @@ pub mod types;
|
||||
pub mod validation;
|
||||
|
||||
mod base;
|
||||
mod error;
|
||||
mod openapi;
|
||||
#[cfg(any(feature = "swagger-ui", feature = "rapidoc", feature = "redoc"))]
|
||||
mod ui;
|
||||
@@ -110,7 +110,6 @@ pub use base::{
|
||||
ApiExtractor, ApiExtractorType, ApiResponse, CombinedAPI, ExtractParamOptions, OAuthScopes,
|
||||
OpenApi, Tags,
|
||||
};
|
||||
pub use error::ParseRequestError;
|
||||
pub use openapi::{LicenseObject, OpenApiService, ServerObject};
|
||||
#[doc = include_str!("docs/request.md")]
|
||||
pub use poem_openapi_derive::ApiRequest;
|
||||
|
||||
@@ -30,27 +30,27 @@ macro_rules! impl_apirequest_for_payload {
|
||||
request: &'a poem::Request,
|
||||
body: &mut poem::RequestBody,
|
||||
_param_opts: $crate::ExtractParamOptions<Self::ParamType>,
|
||||
) -> Result<Self, $crate::ParseRequestError> {
|
||||
) -> poem::Result<Self> {
|
||||
match request.content_type() {
|
||||
Some(content_type) => {
|
||||
let mime: mime::Mime = match content_type.parse() {
|
||||
Ok(mime) => mime,
|
||||
Err(_) => {
|
||||
return Err($crate::ParseRequestError::ContentTypeNotSupported {
|
||||
return Err($crate::error::ContentTypeError::NotSupported {
|
||||
content_type: content_type.to_string(),
|
||||
});
|
||||
}.into());
|
||||
}
|
||||
};
|
||||
|
||||
if mime.essence_str() != <Self as $crate::payload::Payload>::CONTENT_TYPE {
|
||||
return Err($crate::ParseRequestError::ContentTypeNotSupported {
|
||||
return Err($crate::error::ContentTypeError::NotSupported {
|
||||
content_type: content_type.to_string(),
|
||||
});
|
||||
}.into());
|
||||
}
|
||||
|
||||
<Self as $crate::payload::ParsePayload>::from_request(request, body).await
|
||||
}
|
||||
None => Err($crate::ParseRequestError::ExpectContentType),
|
||||
None => Err($crate::error::ContentTypeError::ExpectContentType.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use poem::{
|
||||
endpoint::{make_sync, BoxEndpoint},
|
||||
middleware::CookieJarManager,
|
||||
web::cookie::CookieKey,
|
||||
Endpoint, EndpointExt, IntoEndpoint, Request, Response, Route,
|
||||
Endpoint, EndpointExt, IntoEndpoint, Request, Response, Result, Route,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -234,10 +234,10 @@ impl<T: OpenApi> IntoEndpoint for OpenApiService<T> {
|
||||
type Endpoint = BoxEndpoint<'static, Response>;
|
||||
|
||||
fn into_endpoint(self) -> Self::Endpoint {
|
||||
async fn extract_query(mut req: Request) -> Request {
|
||||
async fn extract_query(mut req: Request) -> Result<Request> {
|
||||
let url_query: Vec<(String, String)> = req.params().unwrap_or_default();
|
||||
req.extensions_mut().insert(UrlQuery(url_query));
|
||||
req
|
||||
Ok(req)
|
||||
}
|
||||
|
||||
let cookie_jar_manager = match self.cookie_key {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use poem::{Request, RequestBody};
|
||||
use poem::{Request, RequestBody, Result};
|
||||
|
||||
use crate::{
|
||||
error::ParseParamError,
|
||||
registry::{MetaParamIn, MetaSchemaRef, Registry},
|
||||
types::ParseFromParameter,
|
||||
ApiExtractor, ApiExtractorType, ExtractParamOptions, ParseRequestError,
|
||||
ApiExtractor, ApiExtractorType, ExtractParamOptions,
|
||||
};
|
||||
|
||||
/// Represents the parameters passed by the cookie.
|
||||
@@ -53,7 +54,7 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for Cookie<T> {
|
||||
request: &'a Request,
|
||||
_body: &mut RequestBody,
|
||||
param_opts: ExtractParamOptions<Self::ParamType>,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
) -> Result<Self> {
|
||||
let value = request
|
||||
.cookie()
|
||||
.get(param_opts.name)
|
||||
@@ -67,9 +68,12 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for Cookie<T> {
|
||||
|
||||
ParseFromParameter::parse_from_parameters(value.as_deref())
|
||||
.map(Self)
|
||||
.map_err(|err| ParseRequestError::ParseParam {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
.map_err(|err| {
|
||||
ParseParamError {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -119,7 +123,7 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for CookiePrivate<T> {
|
||||
request: &'a Request,
|
||||
_body: &mut RequestBody,
|
||||
param_opts: ExtractParamOptions<Self::ParamType>,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
) -> Result<Self> {
|
||||
let value = request
|
||||
.cookie()
|
||||
.private()
|
||||
@@ -134,9 +138,12 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for CookiePrivate<T> {
|
||||
|
||||
ParseFromParameter::parse_from_parameters(value.as_deref())
|
||||
.map(Self)
|
||||
.map_err(|err| ParseRequestError::ParseParam {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
.map_err(|err| {
|
||||
ParseParamError {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -186,7 +193,7 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for CookieSigned<T> {
|
||||
request: &'a Request,
|
||||
_body: &mut RequestBody,
|
||||
param_opts: ExtractParamOptions<Self::ParamType>,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
) -> Result<Self> {
|
||||
let value = request
|
||||
.cookie()
|
||||
.signed()
|
||||
@@ -201,9 +208,12 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for CookieSigned<T> {
|
||||
|
||||
ParseFromParameter::parse_from_parameters(value.as_deref())
|
||||
.map(Self)
|
||||
.map_err(|err| ParseRequestError::ParseParam {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
.map_err(|err| {
|
||||
ParseParamError {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use poem::{Request, RequestBody};
|
||||
use poem::{Request, RequestBody, Result};
|
||||
|
||||
use crate::{
|
||||
error::ParseParamError,
|
||||
registry::{MetaParamIn, MetaSchemaRef, Registry},
|
||||
types::ParseFromParameter,
|
||||
ApiExtractor, ApiExtractorType, ExtractParamOptions, ParseRequestError,
|
||||
ApiExtractor, ApiExtractorType, ExtractParamOptions,
|
||||
};
|
||||
|
||||
/// Represents the parameters passed by the request header.
|
||||
@@ -53,7 +54,7 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for Header<T> {
|
||||
request: &'a Request,
|
||||
_body: &mut RequestBody,
|
||||
param_opts: ExtractParamOptions<Self::ParamType>,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
) -> Result<Self> {
|
||||
let mut values = request
|
||||
.headers()
|
||||
.get_all(param_opts.name)
|
||||
@@ -70,9 +71,12 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for Header<T> {
|
||||
|
||||
ParseFromParameter::parse_from_parameters(values)
|
||||
.map(Self)
|
||||
.map_err(|err| ParseRequestError::ParseParam {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
.map_err(|err| {
|
||||
ParseParamError {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use poem::{Request, RequestBody};
|
||||
use poem::{Request, RequestBody, Result};
|
||||
|
||||
use crate::{
|
||||
error::ParseParamError,
|
||||
registry::{MetaParamIn, MetaSchemaRef, Registry},
|
||||
types::ParseFromParameter,
|
||||
ApiExtractor, ApiExtractorType, ExtractParamOptions, ParseRequestError,
|
||||
ApiExtractor, ApiExtractorType, ExtractParamOptions,
|
||||
};
|
||||
|
||||
/// Represents the parameters passed by the URI path.
|
||||
@@ -53,7 +54,7 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for Path<T> {
|
||||
request: &'a Request,
|
||||
_body: &mut RequestBody,
|
||||
param_opts: ExtractParamOptions<Self::ParamType>,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
) -> Result<Self> {
|
||||
let value = match (
|
||||
request.raw_path_param(param_opts.name),
|
||||
¶m_opts.default_value,
|
||||
@@ -65,9 +66,12 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for Path<T> {
|
||||
|
||||
ParseFromParameter::parse_from_parameters(value)
|
||||
.map(Self)
|
||||
.map_err(|err| ParseRequestError::ParseParam {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
.map_err(|err| {
|
||||
ParseParamError {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use poem::{Request, RequestBody};
|
||||
use poem::{Request, RequestBody, Result};
|
||||
|
||||
use crate::{
|
||||
base::UrlQuery,
|
||||
error::ParseParamError,
|
||||
registry::{MetaParamIn, MetaSchemaRef, Registry},
|
||||
types::ParseFromParameter,
|
||||
ApiExtractor, ApiExtractorType, ExtractParamOptions, ParseRequestError,
|
||||
ApiExtractor, ApiExtractorType, ExtractParamOptions,
|
||||
};
|
||||
|
||||
/// Represents the parameters passed by the query string.
|
||||
@@ -54,7 +55,7 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for Query<T> {
|
||||
request: &'a Request,
|
||||
_body: &mut RequestBody,
|
||||
param_opts: ExtractParamOptions<Self::ParamType>,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
) -> Result<Self> {
|
||||
let mut values = request
|
||||
.extensions()
|
||||
.get::<UrlQuery>()
|
||||
@@ -71,9 +72,12 @@ impl<'a, T: ParseFromParameter> ApiExtractor<'a> for Query<T> {
|
||||
|
||||
ParseFromParameter::parse_from_parameters(values)
|
||||
.map(Self)
|
||||
.map_err(|err| ParseRequestError::ParseParam {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
.map_err(|err| {
|
||||
ParseParamError {
|
||||
name: param_opts.name,
|
||||
reason: err.into_message(),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use bytes::Bytes;
|
||||
use poem::{Body, FromRequest, IntoResponse, Request, RequestBody, Response};
|
||||
use poem::{Body, FromRequest, IntoResponse, Request, RequestBody, Response, Result};
|
||||
|
||||
use crate::{
|
||||
payload::{ParsePayload, Payload},
|
||||
registry::{MetaMediaType, MetaResponse, MetaResponses, MetaSchema, MetaSchemaRef, Registry},
|
||||
ApiResponse, ParseRequestError,
|
||||
ApiResponse,
|
||||
};
|
||||
|
||||
/// A binary payload.
|
||||
@@ -54,7 +54,8 @@ use crate::{
|
||||
/// .uri(Uri::from_static("/upload"))
|
||||
/// .body("abcdef"),
|
||||
/// )
|
||||
/// .await;
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// assert_eq!(resp.status(), StatusCode::OK);
|
||||
/// assert_eq!(resp.into_body().into_string().await.unwrap(), "6");
|
||||
///
|
||||
@@ -66,7 +67,8 @@ use crate::{
|
||||
/// .uri(Uri::from_static("/upload_stream"))
|
||||
/// .body("abcdef"),
|
||||
/// )
|
||||
/// .await;
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// assert_eq!(resp.status(), StatusCode::OK);
|
||||
/// assert_eq!(resp.into_body().into_string().await.unwrap(), "6");
|
||||
/// # });
|
||||
@@ -103,13 +105,8 @@ impl<T: Send> Payload for Binary<T> {
|
||||
impl ParsePayload for Binary<Vec<u8>> {
|
||||
const IS_REQUIRED: bool = true;
|
||||
|
||||
async fn from_request(
|
||||
request: &Request,
|
||||
body: &mut RequestBody,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
Ok(Self(<Vec<u8>>::from_request(request, body).await.map_err(
|
||||
|err| ParseRequestError::ParseRequestBody(err.into_response()),
|
||||
)?))
|
||||
async fn from_request(request: &Request, body: &mut RequestBody) -> Result<Self> {
|
||||
Ok(Self(<Vec<u8>>::from_request(request, body).await?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,13 +114,8 @@ impl ParsePayload for Binary<Vec<u8>> {
|
||||
impl ParsePayload for Binary<Bytes> {
|
||||
const IS_REQUIRED: bool = true;
|
||||
|
||||
async fn from_request(
|
||||
request: &Request,
|
||||
body: &mut RequestBody,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
Ok(Self(Bytes::from_request(request, body).await.map_err(
|
||||
|err| ParseRequestError::ParseRequestBody(err.into_response()),
|
||||
)?))
|
||||
async fn from_request(request: &Request, body: &mut RequestBody) -> Result<Self> {
|
||||
Ok(Self(Bytes::from_request(request, body).await?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,13 +123,8 @@ impl ParsePayload for Binary<Bytes> {
|
||||
impl ParsePayload for Binary<Body> {
|
||||
const IS_REQUIRED: bool = true;
|
||||
|
||||
async fn from_request(
|
||||
request: &Request,
|
||||
body: &mut RequestBody,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
Ok(Self(Body::from_request(request, body).await.map_err(
|
||||
|err| ParseRequestError::ParseRequestBody(err.into_response()),
|
||||
)?))
|
||||
async fn from_request(request: &Request, body: &mut RequestBody) -> Result<Self> {
|
||||
Ok(Self(Body::from_request(request, body).await?))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use poem::{
|
||||
error::{ParseJsonError, ReadBodyError},
|
||||
http::StatusCode,
|
||||
FromRequest, IntoResponse, Request, RequestBody, Response,
|
||||
};
|
||||
use poem::{FromRequest, IntoResponse, Request, RequestBody, Response, Result};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
error::ParseJsonError,
|
||||
payload::{ParsePayload, Payload},
|
||||
registry::{MetaMediaType, MetaResponse, MetaResponses, MetaSchemaRef, Registry},
|
||||
types::{ParseFromJSON, ToJSON, Type},
|
||||
ApiResponse, ParseRequestError,
|
||||
ApiResponse,
|
||||
};
|
||||
|
||||
/// A JSON payload.
|
||||
@@ -49,30 +46,18 @@ impl<T: Type> Payload for Json<T> {
|
||||
impl<T: ParseFromJSON> ParsePayload for Json<T> {
|
||||
const IS_REQUIRED: bool = T::IS_REQUIRED;
|
||||
|
||||
async fn from_request(
|
||||
request: &Request,
|
||||
body: &mut RequestBody,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
let data: Vec<u8> =
|
||||
FromRequest::from_request(request, body)
|
||||
.await
|
||||
.map_err(|err: ReadBodyError| {
|
||||
ParseRequestError::ParseRequestBody(err.into_response())
|
||||
})?;
|
||||
async fn from_request(request: &Request, body: &mut RequestBody) -> Result<Self> {
|
||||
let data: Vec<u8> = FromRequest::from_request(request, body).await?;
|
||||
let value = if data.is_empty() {
|
||||
Value::Null
|
||||
} else {
|
||||
serde_json::from_slice(&data)
|
||||
.map_err(ParseJsonError::Json)
|
||||
.map_err(|err| ParseRequestError::ParseRequestBody(err.into_response()))?
|
||||
serde_json::from_slice(&data).map_err(|err| ParseJsonError {
|
||||
reason: err.to_string(),
|
||||
})?
|
||||
};
|
||||
|
||||
let value = T::parse_from_json(value).map_err(|err| {
|
||||
ParseRequestError::ParseRequestBody(
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.body(err.into_message()),
|
||||
)
|
||||
let value = T::parse_from_json(value).map_err(|err| ParseJsonError {
|
||||
reason: err.into_message(),
|
||||
})?;
|
||||
Ok(Self(value))
|
||||
}
|
||||
|
||||
@@ -11,10 +11,7 @@ use poem::{Request, RequestBody, Result};
|
||||
pub use self::{
|
||||
attachment::Attachment, binary::Binary, json::Json, plain_text::PlainText, response::Response,
|
||||
};
|
||||
use crate::{
|
||||
registry::{MetaSchemaRef, Registry},
|
||||
ParseRequestError,
|
||||
};
|
||||
use crate::registry::{MetaSchemaRef, Registry};
|
||||
|
||||
/// Represents a payload type.
|
||||
pub trait Payload: Send {
|
||||
@@ -36,8 +33,5 @@ pub trait ParsePayload: Sized {
|
||||
const IS_REQUIRED: bool;
|
||||
|
||||
/// Parse the payload object from the HTTP request.
|
||||
async fn from_request(
|
||||
request: &Request,
|
||||
body: &mut RequestBody,
|
||||
) -> Result<Self, ParseRequestError>;
|
||||
async fn from_request(request: &Request, body: &mut RequestBody) -> Result<Self>;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use poem::{FromRequest, IntoResponse, Request, RequestBody, Response};
|
||||
use poem::{FromRequest, IntoResponse, Request, RequestBody, Response, Result};
|
||||
|
||||
use crate::{
|
||||
payload::{ParsePayload, Payload},
|
||||
registry::{MetaMediaType, MetaResponse, MetaResponses, MetaSchemaRef, Registry},
|
||||
types::Type,
|
||||
ApiResponse, ParseRequestError,
|
||||
ApiResponse,
|
||||
};
|
||||
|
||||
/// A UTF8 string payload.
|
||||
@@ -39,13 +39,8 @@ impl<T: Send> Payload for PlainText<T> {
|
||||
impl ParsePayload for PlainText<String> {
|
||||
const IS_REQUIRED: bool = false;
|
||||
|
||||
async fn from_request(
|
||||
request: &Request,
|
||||
body: &mut RequestBody,
|
||||
) -> Result<Self, ParseRequestError> {
|
||||
Ok(Self(String::from_request(request, body).await.map_err(
|
||||
|err| ParseRequestError::ParseRequestBody(err.into_response()),
|
||||
)?))
|
||||
async fn from_request(request: &Request, body: &mut RequestBody) -> Result<Self> {
|
||||
Ok(Self(String::from_request(request, body).await?))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use poem::{
|
||||
http::{header::HeaderName, HeaderMap, HeaderValue, StatusCode},
|
||||
IntoResponse,
|
||||
Error, IntoResponse,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
registry::{MetaResponses, Registry},
|
||||
ApiResponse, ParseRequestError,
|
||||
ApiResponse,
|
||||
};
|
||||
|
||||
/// A response type wrapper.
|
||||
@@ -53,7 +53,7 @@ impl<T> Response<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ApiResponse> IntoResponse for Response<T> {
|
||||
impl<T: IntoResponse> IntoResponse for Response<T> {
|
||||
fn into_response(self) -> poem::Response {
|
||||
let mut resp = self.inner.into_response();
|
||||
if let Some(status) = self.status {
|
||||
@@ -75,7 +75,7 @@ impl<T: ApiResponse> ApiResponse for Response<T> {
|
||||
T::register(registry);
|
||||
}
|
||||
|
||||
fn from_parse_request_error(err: ParseRequestError) -> Self {
|
||||
fn from_parse_request_error(err: Error) -> Self {
|
||||
Self::new(T::from_parse_request_error(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use poem::{
|
||||
http::{Method, StatusCode, Uri},
|
||||
web::Data,
|
||||
Endpoint, EndpointExt, IntoEndpoint,
|
||||
Endpoint, EndpointExt, Error, IntoEndpoint,
|
||||
};
|
||||
use poem_openapi::{
|
||||
param::Query,
|
||||
payload::{Binary, Json, PlainText},
|
||||
registry::{MetaApi, MetaSchema},
|
||||
types::Type,
|
||||
ApiRequest, ApiResponse, OpenApi, OpenApiService, ParseRequestError, Tags,
|
||||
ApiRequest, ApiResponse, OpenApi, OpenApiService, Tags,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
@@ -33,7 +33,8 @@ async fn path_and_method() {
|
||||
.uri(Uri::from_static("/abc"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
@@ -112,7 +113,8 @@ async fn common_attributes() {
|
||||
.uri(Uri::from_static("/hello/world"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
@@ -178,7 +180,8 @@ async fn request() {
|
||||
.content_type("application/json")
|
||||
.body("100"),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let resp = ep
|
||||
@@ -189,7 +192,8 @@ async fn request() {
|
||||
.content_type("text/plain")
|
||||
.body("abc"),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let resp = ep
|
||||
@@ -200,7 +204,8 @@ async fn request() {
|
||||
.content_type("application/octet-stream")
|
||||
.body(vec![1, 2, 3]),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
@@ -232,10 +237,11 @@ async fn payload_request() {
|
||||
.content_type("application/json")
|
||||
.body("100"),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let resp = ep
|
||||
let err = ep
|
||||
.call(
|
||||
poem::Request::builder()
|
||||
.method(Method::POST)
|
||||
@@ -243,8 +249,9 @@ async fn payload_request() {
|
||||
.content_type("text/plain")
|
||||
.body("100"),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(err.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -275,7 +282,8 @@ async fn optional_payload_request() {
|
||||
.content_type("application/json")
|
||||
.body("100"),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.into_body().into_string().await.unwrap(), "100");
|
||||
|
||||
@@ -288,7 +296,8 @@ async fn optional_payload_request() {
|
||||
.content_type("application/json")
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.into_body().into_string().await.unwrap(), "999");
|
||||
}
|
||||
@@ -361,7 +370,8 @@ async fn response() {
|
||||
.uri(Uri::from_static("/?code=200"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.take_body().into_string().await.unwrap(), "");
|
||||
|
||||
@@ -372,7 +382,8 @@ async fn response() {
|
||||
.uri(Uri::from_static("/?code=409"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::CONFLICT);
|
||||
assert_eq!(resp.content_type(), Some("application/json"));
|
||||
assert_eq!(resp.take_body().into_string().await.unwrap(), "409");
|
||||
@@ -384,7 +395,8 @@ async fn response() {
|
||||
.uri(Uri::from_static("/?code=404"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
assert_eq!(resp.content_type(), Some("text/plain"));
|
||||
assert_eq!(resp.take_body().into_string().await.unwrap(), "code: 404");
|
||||
@@ -403,7 +415,7 @@ async fn bad_request_handler() {
|
||||
BadRequest(PlainText<String>),
|
||||
}
|
||||
|
||||
fn bad_request_handler(err: ParseRequestError) -> MyResponse {
|
||||
fn bad_request_handler(err: Error) -> MyResponse {
|
||||
MyResponse::BadRequest(PlainText(format!("!!! {}", err.to_string())))
|
||||
}
|
||||
|
||||
@@ -426,7 +438,8 @@ async fn bad_request_handler() {
|
||||
.uri(Uri::from_static("/?code=200"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.content_type(), Some("text/plain"));
|
||||
assert_eq!(resp.take_body().into_string().await.unwrap(), "code: 200");
|
||||
@@ -438,12 +451,13 @@ async fn bad_request_handler() {
|
||||
.uri(Uri::from_static("/"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(resp.content_type(), Some("text/plain"));
|
||||
assert_eq!(
|
||||
resp.take_body().into_string().await.unwrap(),
|
||||
r#"!!! Failed to parse parameter `code`: Type "integer(uint16)" expects an input value."#
|
||||
r#"!!! failed to parse parameter `code`: Type "integer(uint16)" expects an input value."#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -460,7 +474,7 @@ async fn bad_request_handler_for_validator() {
|
||||
BadRequest(PlainText<String>),
|
||||
}
|
||||
|
||||
fn bad_request_handler(err: ParseRequestError) -> MyResponse {
|
||||
fn bad_request_handler(err: Error) -> MyResponse {
|
||||
MyResponse::BadRequest(PlainText(format!("!!! {}", err.to_string())))
|
||||
}
|
||||
|
||||
@@ -486,7 +500,8 @@ async fn bad_request_handler_for_validator() {
|
||||
.uri(Uri::from_static("/?code=50"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.content_type(), Some("text/plain"));
|
||||
assert_eq!(resp.take_body().into_string().await.unwrap(), "code: 50");
|
||||
@@ -498,12 +513,13 @@ async fn bad_request_handler_for_validator() {
|
||||
.uri(Uri::from_static("/?code=200"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(resp.content_type(), Some("text/plain"));
|
||||
assert_eq!(
|
||||
resp.take_body().into_string().await.unwrap(),
|
||||
r#"!!! Failed to parse parameter `code`: verification failed. maximum(100, exclusive: false)"#
|
||||
r#"!!! failed to parse parameter `code`: verification failed. maximum(100, exclusive: false)"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -529,7 +545,8 @@ async fn poem_extract() {
|
||||
.uri(Uri::from_static("/"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
@@ -583,7 +600,8 @@ async fn returning_borrowed_value() {
|
||||
.uri(Uri::from_static("/value1"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.into_body().into_string().await.unwrap(), "999");
|
||||
|
||||
@@ -594,7 +612,8 @@ async fn returning_borrowed_value() {
|
||||
.uri(Uri::from_static("/value2"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.into_body().into_string().await.unwrap(), "\"abc\"");
|
||||
|
||||
@@ -605,7 +624,8 @@ async fn returning_borrowed_value() {
|
||||
.uri(Uri::from_static("/value3"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.into_body().into_string().await.unwrap(), "888");
|
||||
|
||||
@@ -616,7 +636,8 @@ async fn returning_borrowed_value() {
|
||||
.uri(Uri::from_static("/values"))
|
||||
.finish(),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.into_body().into_string().await.unwrap(), "[1,2,3,4,5]");
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user