mirror of
https://github.com/poem-web/poem.git
synced 2026-01-25 04:18:25 +00:00
Initial commit
This commit is contained in:
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: sunli829
|
||||||
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: 'Bug Report'
|
||||||
|
about: 'Report a new bug'
|
||||||
|
title: '<Title>'
|
||||||
|
labels: bug
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
|
||||||
|
|
||||||
|
## Steps to Reproduce the Problem
|
||||||
|
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
## Specifications
|
||||||
|
|
||||||
|
- Version:
|
||||||
|
- Platform:
|
||||||
|
- Subsystem:
|
||||||
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: 'Feature Request'
|
||||||
|
about: 'Report a new feature to be implemented'
|
||||||
|
title: '<Title>'
|
||||||
|
labels: enhancement
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description of the feature
|
||||||
|
|
||||||
|
|
||||||
|
## Code example (if possible)
|
||||||
8
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: 'Question'
|
||||||
|
about: 'If something needs clarification'
|
||||||
|
title: '<Title>'
|
||||||
|
labels: question
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- What is your question? Please be as specific as possible! -->
|
||||||
19
.github/workflows/ci.yml
vendored
Normal file
19
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Check format
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --all --verbose
|
||||||
|
- name: Run tests with async-std
|
||||||
|
run: cargo test --all --verbose
|
||||||
30
.github/workflows/code-coverage.yml
vendored
Normal file
30
.github/workflows/code-coverage.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Code Coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cover:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
override: true
|
||||||
|
- name: Run cargo-tarpaulin
|
||||||
|
uses: actions-rs/tarpaulin@v0.1
|
||||||
|
with:
|
||||||
|
version: '0.15.0'
|
||||||
|
# args: --out Xml --all --all-features
|
||||||
|
- name: Upload to codecov.io
|
||||||
|
uses: codecov/codecov-action@v1.0.2
|
||||||
|
with:
|
||||||
|
token: ${{secrets.CODECOV_TOKEN}}
|
||||||
|
- name: Archive code coverage results
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: code-coverage-report
|
||||||
|
path: cobertura.xml
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
2
.rustfmt.toml
Normal file
2
.rustfmt.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
edition = "2018"
|
||||||
|
newline_style = "unix"
|
||||||
34
Cargo.toml
Normal file
34
Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "poem"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["sunli <scott_s829@163.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "Poem is an easy-to-use web framework for Rust"
|
||||||
|
license = "MIT/Apache-2.0"
|
||||||
|
documentation = "https://docs.rs/poem/"
|
||||||
|
homepage = "https://github.com/poem/poem"
|
||||||
|
repository = "https://github.com/poem/poem"
|
||||||
|
keywords = ["http", "web", "framework", "async"]
|
||||||
|
categories = [
|
||||||
|
"network-programming",
|
||||||
|
"asynchronous",
|
||||||
|
"web-programming::http-server",
|
||||||
|
"web-programming::websocket",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1.51"
|
||||||
|
anyhow = "1.0.42"
|
||||||
|
bytes = "1.0.1"
|
||||||
|
futures-util = "0.3.16"
|
||||||
|
http = "0.2.4"
|
||||||
|
hyper = { version = "0.14.11", features = ["http1", "http2", "server", "runtime", "stream"] }
|
||||||
|
mime = "0.3.16"
|
||||||
|
multer = { version = "2.0.1", features = ["tokio"] }
|
||||||
|
tokio = { version = "1.9.0", features = ["sync", "rt"] }
|
||||||
|
tokio-util = { version = "0.6.7", features = ["io"] }
|
||||||
|
serde = { version = "1.0.127", features = ["derive"] }
|
||||||
|
serde_json = "1.0.66"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.9.0", features = ["rt-multi-thread", "macros"] }
|
||||||
201
LICENSE-APACHE
Normal file
201
LICENSE-APACHE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
23
LICENSE-MIT
Normal file
23
LICENSE-MIT
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Permission is hereby granted, free of charge, to any
|
||||||
|
person obtaining a copy of this software and associated
|
||||||
|
documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without
|
||||||
|
limitation the rights to use, copy, modify, merge,
|
||||||
|
publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software
|
||||||
|
is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice
|
||||||
|
shall be included in all copies or substantial portions
|
||||||
|
of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
18
examples/hello_world.rs
Normal file
18
examples/hello_world.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use poem::middlewares::StripPrefix;
|
||||||
|
use poem::route::{self, Route};
|
||||||
|
use poem::EndpointExt;
|
||||||
|
|
||||||
|
async fn hello() -> &'static str {
|
||||||
|
"hello"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let route = Route::new().at("/hello", route::get(hello));
|
||||||
|
let api = Route::new().at("/api/*", route.with(StripPrefix::new("/api")));
|
||||||
|
|
||||||
|
poem::Server::new(api)
|
||||||
|
.serve(&"127.0.0.1:3000".parse().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
94
src/body.rs
Normal file
94
src/body.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
use std::pin::Pin;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use futures_util::Stream;
|
||||||
|
use hyper::body::HttpBody;
|
||||||
|
use tokio::io::AsyncRead;
|
||||||
|
|
||||||
|
use crate::{Error, Result};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Body(pub(crate) hyper::Body);
|
||||||
|
|
||||||
|
impl From<&'static [u8]> for Body {
|
||||||
|
#[inline]
|
||||||
|
fn from(data: &'static [u8]) -> Self {
|
||||||
|
Self(data.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&'static str> for Body {
|
||||||
|
#[inline]
|
||||||
|
fn from(data: &'static str) -> Self {
|
||||||
|
Self(data.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Bytes> for Body {
|
||||||
|
#[inline]
|
||||||
|
fn from(data: Bytes) -> Self {
|
||||||
|
Self(data.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<u8>> for Body {
|
||||||
|
#[inline]
|
||||||
|
fn from(data: Vec<u8>) -> Self {
|
||||||
|
Self(data.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Body {
|
||||||
|
#[inline]
|
||||||
|
fn from(data: String) -> Self {
|
||||||
|
Self(data.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Body {
|
||||||
|
#[inline]
|
||||||
|
pub fn from_bytes(data: Bytes) -> Self {
|
||||||
|
data.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn from_string(data: String) -> Self {
|
||||||
|
data.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn from_async_read(reader: impl AsyncRead + Send + 'static) -> Self {
|
||||||
|
Self(hyper::Body::wrap_stream(tokio_util::io::ReaderStream::new(
|
||||||
|
reader,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self(hyper::Body::empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn into_bytes(self) -> Result<Bytes> {
|
||||||
|
hyper::body::to_bytes(self.0)
|
||||||
|
.await
|
||||||
|
.map_err(Error::internal_server_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_async_read(self) -> impl AsyncRead + Send + 'static {
|
||||||
|
tokio_util::io::StreamReader::new(BodyStream(self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BodyStream(hyper::Body);
|
||||||
|
|
||||||
|
impl Stream for BodyStream {
|
||||||
|
type Item = Result<Bytes, std::io::Error>;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
|
Pin::new(&mut self.0)
|
||||||
|
.poll_data(cx)
|
||||||
|
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/endpoint.rs
Normal file
108
src/endpoint.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use std::future::Future;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{FromRequest, IntoResponse, Middleware, Request, Response, Result};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait FnHandler<In>: Send + Sync {
|
||||||
|
async fn call(&self, req: Request) -> Result<Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_fn_handler {
|
||||||
|
() => {};
|
||||||
|
|
||||||
|
($head: ident, $($tail:ident),* $(,)?) => {
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<F, Fut, Res, $head, $($tail,)*> FnHandler<($head, $($tail,)*)> for F
|
||||||
|
where
|
||||||
|
F: Fn($head, $($tail,)*) -> Fut + Send + Sync,
|
||||||
|
Fut: Future<Output = Res> + Send,
|
||||||
|
Res: IntoResponse,
|
||||||
|
$head: FromRequest + Send,
|
||||||
|
$($tail: FromRequest + Send,)* {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
async fn call(&self, mut req: Request) -> Result<Response> {
|
||||||
|
let $head = $head::from_request(&mut req).await?;
|
||||||
|
$(
|
||||||
|
let $tail = $tail::from_request(&mut req).await?;
|
||||||
|
)*
|
||||||
|
self($head, $($tail,)*).await.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_fn_handler!($($tail,)*);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_fn_handler!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<F, Fut, Res> FnHandler<()> for F
|
||||||
|
where
|
||||||
|
F: Fn() -> Fut + Send + Sync,
|
||||||
|
Fut: Future<Output = Res> + Send,
|
||||||
|
Res: IntoResponse,
|
||||||
|
{
|
||||||
|
async fn call(&self, _req: Request) -> Result<Response> {
|
||||||
|
self().await.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait Endpoint: Send + Sync + 'static {
|
||||||
|
async fn call(&self, req: Request) -> Result<Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T: Endpoint + ?Sized> Endpoint for Box<T> {
|
||||||
|
async fn call(&self, req: Request) -> Result<Response> {
|
||||||
|
self.as_ref().call(req).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T: Endpoint + ?Sized> Endpoint for Arc<T> {
|
||||||
|
async fn call(&self, req: Request) -> Result<Response> {
|
||||||
|
self.as_ref().call(req).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct FnHandlerWrapper<F, In> {
|
||||||
|
f: F,
|
||||||
|
_mark: PhantomData<In>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F, In> FnHandlerWrapper<F, In>
|
||||||
|
where
|
||||||
|
F: FnHandler<In>,
|
||||||
|
{
|
||||||
|
pub(crate) fn new(f: F) -> Self {
|
||||||
|
Self {
|
||||||
|
f,
|
||||||
|
_mark: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<In, F> Endpoint for FnHandlerWrapper<F, In>
|
||||||
|
where
|
||||||
|
In: Send + Sync + 'static,
|
||||||
|
F: FnHandler<In> + 'static,
|
||||||
|
{
|
||||||
|
async fn call(&self, req: Request) -> Result<Response> {
|
||||||
|
self.f.call(req).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait EndpointExt {
|
||||||
|
fn with<T: Middleware>(self, middleware: T) -> Box<dyn Endpoint>
|
||||||
|
where
|
||||||
|
Self: Endpoint + Sized,
|
||||||
|
{
|
||||||
|
middleware.transform(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Endpoint> EndpointExt for T {}
|
||||||
161
src/error.rs
Normal file
161
src/error.rs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
use std::fmt::{self, Debug, Display, Formatter};
|
||||||
|
|
||||||
|
use crate::{Body, HeaderName, Response, StatusCode};
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
macro_rules! define_error {
|
||||||
|
($($(#[$docs:meta])* ($name:ident, $code:ident);)*) => {
|
||||||
|
$(
|
||||||
|
$(#[$docs])*
|
||||||
|
#[inline]
|
||||||
|
pub fn $name(error: impl Into<anyhow::Error>) -> Self {
|
||||||
|
Self {
|
||||||
|
status: StatusCode::$code,
|
||||||
|
error: error.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Error {
|
||||||
|
status: StatusCode,
|
||||||
|
error: anyhow::Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Infallible> for Error {
|
||||||
|
fn from(_: Infallible) -> Self {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(status: StatusCode, error: impl Into<anyhow::Error>) -> Self {
|
||||||
|
Self {
|
||||||
|
status,
|
||||||
|
error: error.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn downcast_ref<T>(&self) -> Option<&T>
|
||||||
|
where
|
||||||
|
T: Display + Debug + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.error.downcast_ref::<T>()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is<T>(&self) -> bool
|
||||||
|
where
|
||||||
|
T: Display + Debug + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.error.is::<T>()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_not_found(&self) -> bool {
|
||||||
|
self.is::<ErrorNotFound>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn as_response(&self) -> Response {
|
||||||
|
Response::builder()
|
||||||
|
.status(self.status)
|
||||||
|
.header(HeaderName::CONTENT_TYPE, "text/plain")
|
||||||
|
.body(Body::from_string(self.error.to_string()))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
define_error!(
|
||||||
|
(bad_request, BAD_REQUEST);
|
||||||
|
(unauthorized, UNAUTHORIZED);
|
||||||
|
(payment_required, PAYMENT_REQUIRED);
|
||||||
|
(forbidden, FORBIDDEN);
|
||||||
|
(not_found, NOT_FOUND);
|
||||||
|
(method_not_allowed, METHOD_NOT_ALLOWED);
|
||||||
|
(not_acceptable, NOT_ACCEPTABLE);
|
||||||
|
(proxy_authentication_required, PROXY_AUTHENTICATION_REQUIRED);
|
||||||
|
(request_timeout, REQUEST_TIMEOUT);
|
||||||
|
(conflict, CONFLICT);
|
||||||
|
(gone, GONE);
|
||||||
|
(length_required, LENGTH_REQUIRED);
|
||||||
|
(payload_too_large, PAYLOAD_TOO_LARGE);
|
||||||
|
(uri_too_long, URI_TOO_LONG);
|
||||||
|
(unsupported_media_type, UNSUPPORTED_MEDIA_TYPE);
|
||||||
|
(range_not_satisfiable, RANGE_NOT_SATISFIABLE);
|
||||||
|
(im_a_teapot, IM_A_TEAPOT);
|
||||||
|
(misdirected_request, MISDIRECTED_REQUEST);
|
||||||
|
(unprocessable_entity, UNPROCESSABLE_ENTITY);
|
||||||
|
(locked, LOCKED);
|
||||||
|
(failed_dependency, FAILED_DEPENDENCY);
|
||||||
|
(upgrade_required, UPGRADE_REQUIRED);
|
||||||
|
(precondition_failed, PRECONDITION_FAILED);
|
||||||
|
(precondition_required, PRECONDITION_REQUIRED);
|
||||||
|
(too_many_requests, TOO_MANY_REQUESTS);
|
||||||
|
(request_header_fields_too_large, REQUEST_HEADER_FIELDS_TOO_LARGE);
|
||||||
|
(unavailable_for_legal_reasons, UNAVAILABLE_FOR_LEGAL_REASONS);
|
||||||
|
(expectation_failed, EXPECTATION_FAILED);
|
||||||
|
(internal_server_error, INTERNAL_SERVER_ERROR);
|
||||||
|
(not_implemented, NOT_IMPLEMENTED);
|
||||||
|
(bad_gateway, BAD_GATEWAY);
|
||||||
|
(service_unavailable, SERVICE_UNAVAILABLE);
|
||||||
|
(gateway_timeout, GATEWAY_TIMEOUT);
|
||||||
|
(http_version_not_supported, HTTP_VERSION_NOT_SUPPORTED);
|
||||||
|
(variant_also_negotiates, VARIANT_ALSO_NEGOTIATES);
|
||||||
|
(insufficient_storage, INSUFFICIENT_STORAGE);
|
||||||
|
(loop_detected, LOOP_DETECTED);
|
||||||
|
(not_extended, NOT_EXTENDED);
|
||||||
|
(network_authentication_required, NETWORK_AUTHENTICATION_REQUIRED);
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! define_simple_errors {
|
||||||
|
($($(#[$docs:meta])* ($name:ident, $err_msg:literal);)*) => {
|
||||||
|
$(
|
||||||
|
$(#[$docs])*
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub struct $name;
|
||||||
|
|
||||||
|
impl Display for $name {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", $err_msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for $name {}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
define_simple_errors!(
|
||||||
|
/// ErrorNotFound
|
||||||
|
(ErrorNotFound, "not found");
|
||||||
|
|
||||||
|
/// ErrorInvalidMethod
|
||||||
|
(ErrorInvalidMethod, "invalid method");
|
||||||
|
|
||||||
|
/// ErrorInvalidHeaderName
|
||||||
|
(ErrorInvalidHeaderName, "invalid header name");
|
||||||
|
|
||||||
|
/// ErrorInvalidHeaderValue
|
||||||
|
(ErrorInvalidHeaderValue, "invalid header value");
|
||||||
|
|
||||||
|
/// ErrorInvalidMime
|
||||||
|
(ErrorInvalidMime, "invalid mime");
|
||||||
|
|
||||||
|
/// ErrorInvalidUri
|
||||||
|
(ErrorInvalidUri, "invalid uri");
|
||||||
|
|
||||||
|
/// ErrorInvalidStatusCode
|
||||||
|
(ErrorInvalidStatusCode, "invalid status code");
|
||||||
|
|
||||||
|
/// ErrorMissingRouteParams
|
||||||
|
(ErrorMissingRouteParams, "missing route params");
|
||||||
|
|
||||||
|
/// ErrorInvalidPathParams
|
||||||
|
(ErrorInvalidPathParams, "invalid path params");
|
||||||
|
);
|
||||||
|
|
||||||
|
pub type Result<T, E = Error> = ::std::result::Result<T, E>;
|
||||||
185
src/header/map.rs
Normal file
185
src/header/map.rs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
use crate::{HeaderName, HeaderValue};
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||||
|
pub struct HeaderMap(pub(crate) http::header::HeaderMap);
|
||||||
|
|
||||||
|
impl HeaderMap {
|
||||||
|
#[inline]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of headers the map can hold without reallocating.
|
||||||
|
///
|
||||||
|
/// This number is an approximation as certain usage patterns could cause additional allocations before the returned capacity is filled.
|
||||||
|
pub fn capacity(&self) -> usize {
|
||||||
|
self.0.capacity()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reserves capacity for at least `additional` more headers to be inserted
|
||||||
|
/// into the `HeaderMap`.
|
||||||
|
///
|
||||||
|
/// The header map may reserve more space to avoid frequent reallocations.
|
||||||
|
/// Like with `with_capacity`, this will be a "best effort" to avoid
|
||||||
|
/// allocations until `additional` more headers are inserted. Certain usage
|
||||||
|
/// patterns could cause additional allocations before the number is
|
||||||
|
/// reached.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the new allocation size overflows `usize`.
|
||||||
|
pub fn reserve(&mut self, additional: usize) {
|
||||||
|
self.0.reserve(additional);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the value associated with the key.
|
||||||
|
///
|
||||||
|
/// If there are multiple values associated with the key, then the first one
|
||||||
|
/// is returned. Use `get_all` to get all values associated with a given
|
||||||
|
/// key. Returns `None` if there are no values associated with the key.
|
||||||
|
pub fn get(&self, key: HeaderName) -> Option<HeaderValue> {
|
||||||
|
self.0.get(key.into_inner()).cloned().map(HeaderValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a view of all values associated with a key.
|
||||||
|
///
|
||||||
|
/// The returned view does not incur any allocations and allows iterating
|
||||||
|
/// the values associated with the key.
|
||||||
|
pub fn get_all(&self, key: HeaderName) -> impl Iterator<Item = HeaderValue> + '_ {
|
||||||
|
self.0
|
||||||
|
.get_all(key.into_inner())
|
||||||
|
.into_iter()
|
||||||
|
.cloned()
|
||||||
|
.map(HeaderValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the map contains a value for the specified key.
|
||||||
|
pub fn contains_key(&self, key: HeaderName) -> bool {
|
||||||
|
self.0.contains_key(key.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator visiting all key-value pairs.
|
||||||
|
///
|
||||||
|
/// The iteration order is arbitrary, but consistent across platforms for
|
||||||
|
/// the same crate version. Each key will be yielded once per associated
|
||||||
|
/// value. So, if a key has 3 associated values, it will be yielded 3 times.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = (HeaderName, HeaderValue)> + '_ {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| (HeaderName(key.clone()), HeaderValue(value.clone())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator visiting all values.
|
||||||
|
///
|
||||||
|
/// The iteration order is arbitrary, but consistent across platforms for
|
||||||
|
/// the same crate version.
|
||||||
|
pub fn keys(&self) -> impl Iterator<Item = HeaderName> + '_ {
|
||||||
|
self.0.keys().cloned().map(HeaderName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator visiting all values mutably.
|
||||||
|
///
|
||||||
|
/// The iteration order is arbitrary, but consistent across platforms for
|
||||||
|
/// the same crate version.
|
||||||
|
pub fn values(&self) -> impl Iterator<Item = HeaderValue> + '_ {
|
||||||
|
self.0.values().cloned().map(HeaderValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a key-value pair into the map.
|
||||||
|
///
|
||||||
|
/// If the map did not previously have this key present, then `None` is
|
||||||
|
/// returned.
|
||||||
|
///
|
||||||
|
/// If the map did have this key present, the new value is associated with
|
||||||
|
/// the key and all previous values are removed. **Note** that only a single
|
||||||
|
/// one of the previous values is returned. If there are multiple values
|
||||||
|
/// that have been previously associated with the key, then the first one is
|
||||||
|
/// returned.
|
||||||
|
///
|
||||||
|
/// The key is not updated, though; this matters for types that can be `==`
|
||||||
|
/// without being identical.
|
||||||
|
pub fn insert(&mut self, key: HeaderName, value: HeaderValue) -> Option<HeaderValue> {
|
||||||
|
self.0
|
||||||
|
.insert(key.into_inner(), value.into_inner())
|
||||||
|
.map(HeaderValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a key-value pair into the map.
|
||||||
|
///
|
||||||
|
/// If the map did not previously have this key present, then `false` is
|
||||||
|
/// returned.
|
||||||
|
///
|
||||||
|
/// If the map did have this key present, the new value is pushed to the end
|
||||||
|
/// of the list of values currently associated with the key. The key is not
|
||||||
|
/// updated, though; this matters for types that can be `==` without being
|
||||||
|
/// identical.
|
||||||
|
pub fn append(&mut self, key: HeaderName, value: HeaderValue) -> bool {
|
||||||
|
self.0.append(key.into_inner(), value.into_inner())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a key from the map, returning the value associated with the key.
|
||||||
|
///
|
||||||
|
/// Returns `None` if the map does not contain the key. If there are
|
||||||
|
/// multiple values associated with the key, then the first one is returned.
|
||||||
|
pub fn remove(&mut self, key: HeaderName) -> Option<HeaderValue> {
|
||||||
|
self.0.remove(key.into_inner()).map(HeaderValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the entry from the map.
|
||||||
|
///
|
||||||
|
/// All values associated with the entry are removed and the first one is
|
||||||
|
/// returned. See [HeaderMap::remove_entry_mult] for an API that returns all values.
|
||||||
|
pub fn remove_entry(&mut self, key: HeaderName) -> Option<HeaderValue> {
|
||||||
|
if let http::header::Entry::Occupied(e) = self.0.entry(key.into_inner()) {
|
||||||
|
let (_, value) = e.remove_entry();
|
||||||
|
Some(HeaderValue(value))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the entry from the map.
|
||||||
|
///
|
||||||
|
/// The key and all values associated with the entry are removed and
|
||||||
|
/// returned.
|
||||||
|
pub fn remove_entry_mult(&mut self, key: HeaderName) -> impl Iterator<Item = HeaderValue> + '_ {
|
||||||
|
if let http::header::Entry::Occupied(e) = self.0.entry(key.into_inner()) {
|
||||||
|
let (_, values) = e.remove_entry_mult();
|
||||||
|
Box::new(values.into_iter().map(HeaderValue)) as Box<dyn Iterator<Item = HeaderValue>>
|
||||||
|
} else {
|
||||||
|
Box::new(std::iter::empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<(HeaderName, HeaderValue)> for HeaderMap {
|
||||||
|
fn from_iter<T: IntoIterator<Item = (HeaderName, HeaderValue)>>(iter: T) -> Self {
|
||||||
|
let mut map = HeaderMap::new();
|
||||||
|
for (name, value) in iter {
|
||||||
|
map.append(name, value);
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoIterator for &'a HeaderMap {
|
||||||
|
type Item = (HeaderName, HeaderValue);
|
||||||
|
type IntoIter = Box<dyn Iterator<Item = (HeaderName, HeaderValue)> + 'a>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
Box::new(
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| (HeaderName(name.clone()), HeaderValue(value.clone()))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Extend<(HeaderName, HeaderValue)> for HeaderMap {
|
||||||
|
fn extend<T: IntoIterator<Item = (HeaderName, HeaderValue)>>(&mut self, iter: T) {
|
||||||
|
for (name, value) in iter {
|
||||||
|
self.append(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/header/mod.rs
Normal file
3
src/header/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub(crate) mod map;
|
||||||
|
pub(crate) mod name;
|
||||||
|
pub(crate) mod value;
|
||||||
888
src/header/name.rs
Normal file
888
src/header/name.rs
Normal file
@@ -0,0 +1,888 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
|
use crate::error::{Error, ErrorInvalidHeaderName};
|
||||||
|
|
||||||
|
macro_rules! define_header_names {
|
||||||
|
($($(#[$docs:meta])* $name:ident;)*) => {
|
||||||
|
$(
|
||||||
|
$(#[$docs])*
|
||||||
|
pub const $name: HeaderName = HeaderName(http::header::$name);
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct HeaderName(pub(crate) http::header::HeaderName);
|
||||||
|
|
||||||
|
impl HeaderName {
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn into_inner(self) -> http::header::HeaderName {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn from_static(src: &'static str) -> Self {
|
||||||
|
Self(http::header::HeaderName::from_static(src))
|
||||||
|
}
|
||||||
|
|
||||||
|
define_header_names!(
|
||||||
|
/// Advertises which content types the client is able to understand.
|
||||||
|
///
|
||||||
|
/// The Accept request HTTP header advertises which content types, expressed
|
||||||
|
/// as MIME types, the client is able to understand. Using content
|
||||||
|
/// negotiation, the server then selects one of the proposals, uses it and
|
||||||
|
/// informs the client of its choice with the Content-Type response header.
|
||||||
|
/// Browsers set adequate values for this header depending of the context
|
||||||
|
/// where the request is done: when fetching a CSS stylesheet a different
|
||||||
|
/// value is set for the request than when fetching an image, video or a
|
||||||
|
/// script.
|
||||||
|
ACCEPT;
|
||||||
|
|
||||||
|
/// Advertises which character set the client is able to understand.
|
||||||
|
///
|
||||||
|
/// The Accept-Charset request HTTP header advertises which character set
|
||||||
|
/// the client is able to understand. Using content negotiation, the server
|
||||||
|
/// then selects one of the proposals, uses it and informs the client of its
|
||||||
|
/// choice within the Content-Type response header. Browsers usually don't
|
||||||
|
/// set this header as the default value for each content type is usually
|
||||||
|
/// correct and transmitting it would allow easier fingerprinting.
|
||||||
|
///
|
||||||
|
/// If the server cannot serve any matching character set, it can
|
||||||
|
/// theoretically send back a 406 (Not Acceptable) error code. But, for a
|
||||||
|
/// better user experience, this is rarely done and the more common way is
|
||||||
|
/// to ignore the Accept-Charset header in this case.
|
||||||
|
ACCEPT_CHARSET;
|
||||||
|
|
||||||
|
/// Advertises which content encoding the client is able to understand.
|
||||||
|
///
|
||||||
|
/// The Accept-Encoding request HTTP header advertises which content
|
||||||
|
/// encoding, usually a compression algorithm, the client is able to
|
||||||
|
/// understand. Using content negotiation, the server selects one of the
|
||||||
|
/// proposals, uses it and informs the client of its choice with the
|
||||||
|
/// Content-Encoding response header.
|
||||||
|
///
|
||||||
|
/// Even if both the client and the server supports the same compression
|
||||||
|
/// algorithms, the server may choose not to compress the body of a
|
||||||
|
/// response, if the identity value is also acceptable. Two common cases
|
||||||
|
/// lead to this:
|
||||||
|
///
|
||||||
|
/// * The data to be sent is already compressed and a second compression
|
||||||
|
/// won't lead to smaller data to be transmitted. This may the case with
|
||||||
|
/// some image formats;
|
||||||
|
///
|
||||||
|
/// * The server is overloaded and cannot afford the computational overhead
|
||||||
|
/// induced by the compression requirement. Typically, Microsoft recommends
|
||||||
|
/// not to compress if a server use more than 80 % of its computational
|
||||||
|
/// power.
|
||||||
|
///
|
||||||
|
/// As long as the identity value, meaning no encryption, is not explicitly
|
||||||
|
/// forbidden, by an identity;q=0 or a *;q=0 without another explicitly set
|
||||||
|
/// value for identity, the server must never send back a 406 Not Acceptable
|
||||||
|
/// error.
|
||||||
|
ACCEPT_ENCODING;
|
||||||
|
|
||||||
|
/// Advertises which languages the client is able to understand.
|
||||||
|
///
|
||||||
|
/// The Accept-Language request HTTP header advertises which languages the
|
||||||
|
/// client is able to understand, and which locale variant is preferred.
|
||||||
|
/// Using content negotiation, the server then selects one of the proposals,
|
||||||
|
/// uses it and informs the client of its choice with the Content-Language
|
||||||
|
/// response header. Browsers set adequate values for this header according
|
||||||
|
/// their user interface language and even if a user can change it, this
|
||||||
|
/// happens rarely (and is frown upon as it leads to fingerprinting).
|
||||||
|
///
|
||||||
|
/// This header is a hint to be used when the server has no way of
|
||||||
|
/// determining the language via another way, like a specific URL, that is
|
||||||
|
/// controlled by an explicit user decision. It is recommended that the
|
||||||
|
/// server never overrides an explicit decision. The content of the
|
||||||
|
/// Accept-Language is often out of the control of the user (like when
|
||||||
|
/// traveling and using an Internet Cafe in a different country); the user
|
||||||
|
/// may also want to visit a page in another language than the locale of
|
||||||
|
/// their user interface.
|
||||||
|
///
|
||||||
|
/// If the server cannot serve any matching language, it can theoretically
|
||||||
|
/// send back a 406 (Not Acceptable) error code. But, for a better user
|
||||||
|
/// experience, this is rarely done and more common way is to ignore the
|
||||||
|
/// Accept-Language header in this case.
|
||||||
|
ACCEPT_LANGUAGE;
|
||||||
|
|
||||||
|
/// Marker used by the server to advertise partial request support.
|
||||||
|
///
|
||||||
|
/// The Accept-Ranges response HTTP header is a marker used by the server to
|
||||||
|
/// advertise its support of partial requests. The value of this field
|
||||||
|
/// indicates the unit that can be used to define a range.
|
||||||
|
///
|
||||||
|
/// In presence of an Accept-Ranges header, the browser may try to resume an
|
||||||
|
/// interrupted download, rather than to start it from the start again.
|
||||||
|
ACCEPT_RANGES;
|
||||||
|
|
||||||
|
/// Preflight response indicating if the response to the request can be
|
||||||
|
/// exposed to the page.
|
||||||
|
///
|
||||||
|
/// The Access-Control-Allow-Credentials response header indicates whether
|
||||||
|
/// or not the response to the request can be exposed to the page. It can be
|
||||||
|
/// exposed when the true value is returned; it can't in other cases.
|
||||||
|
///
|
||||||
|
/// Credentials are cookies, authorization headers or TLS client
|
||||||
|
/// certificates.
|
||||||
|
///
|
||||||
|
/// When used as part of a response to a preflight request, this indicates
|
||||||
|
/// whether or not the actual request can be made using credentials. Note
|
||||||
|
/// that simple GET requests are not preflighted, and so if a request is
|
||||||
|
/// made for a resource with credentials, if this header is not returned
|
||||||
|
/// with the resource, the response is ignored by the browser and not
|
||||||
|
/// returned to web content.
|
||||||
|
///
|
||||||
|
/// The Access-Control-Allow-Credentials header works in conjunction with
|
||||||
|
/// the XMLHttpRequest.withCredentials property or with the credentials
|
||||||
|
/// option in the Request() constructor of the Fetch API. Credentials must
|
||||||
|
/// be set on both sides (the Access-Control-Allow-Credentials header and in
|
||||||
|
/// the XHR or Fetch request) in order for the CORS request with credentials
|
||||||
|
/// to succeed.
|
||||||
|
ACCESS_CONTROL_ALLOW_CREDENTIALS;
|
||||||
|
|
||||||
|
/// Preflight response indicating permitted HTTP headers.
|
||||||
|
///
|
||||||
|
/// The Access-Control-Allow-Headers response header is used in response to
|
||||||
|
/// a preflight request to indicate which HTTP headers will be available via
|
||||||
|
/// Access-Control-Expose-Headers when making the actual request.
|
||||||
|
///
|
||||||
|
/// The simple headers, Accept, Accept-Language, Content-Language,
|
||||||
|
/// Content-Type (but only with a MIME type of its parsed value (ignoring
|
||||||
|
/// parameters) of either application/x-www-form-urlencoded,
|
||||||
|
/// multipart/form-data, or text/plain), are always available and don't need
|
||||||
|
/// to be listed by this header.
|
||||||
|
///
|
||||||
|
/// This header is required if the request has an
|
||||||
|
/// Access-Control-Request-Headers header.
|
||||||
|
ACCESS_CONTROL_ALLOW_HEADERS;
|
||||||
|
|
||||||
|
/// Preflight header response indicating permitted access methods.
|
||||||
|
///
|
||||||
|
/// The Access-Control-Allow-Methods response header specifies the method or
|
||||||
|
/// methods allowed when accessing the resource in response to a preflight
|
||||||
|
/// request.
|
||||||
|
ACCESS_CONTROL_ALLOW_METHODS;
|
||||||
|
|
||||||
|
/// Indicates whether the response can be shared with resources with the
|
||||||
|
/// given origin.
|
||||||
|
ACCESS_CONTROL_ALLOW_ORIGIN;
|
||||||
|
|
||||||
|
/// Indicates which headers can be exposed as part of the response by
|
||||||
|
/// listing their names.
|
||||||
|
ACCESS_CONTROL_EXPOSE_HEADERS;
|
||||||
|
|
||||||
|
/// Indicates how long the results of a preflight request can be cached.
|
||||||
|
ACCESS_CONTROL_MAX_AGE;
|
||||||
|
|
||||||
|
/// Informs the server which HTTP headers will be used when an actual
|
||||||
|
/// request is made.
|
||||||
|
ACCESS_CONTROL_REQUEST_HEADERS;
|
||||||
|
|
||||||
|
/// Informs the server know which HTTP method will be used when the actual
|
||||||
|
/// request is made.
|
||||||
|
ACCESS_CONTROL_REQUEST_METHOD;
|
||||||
|
|
||||||
|
/// Indicates the time in seconds the object has been in a proxy cache.
|
||||||
|
///
|
||||||
|
/// The Age header is usually close to zero. If it is Age: 0, it was
|
||||||
|
/// probably just fetched from the origin server; otherwise It is usually
|
||||||
|
/// calculated as a difference between the proxy's current date and the Date
|
||||||
|
/// general header included in the HTTP response.
|
||||||
|
AGE;
|
||||||
|
|
||||||
|
/// Lists the set of methods support by a resource.
|
||||||
|
///
|
||||||
|
/// This header must be sent if the server responds with a 405 Method Not
|
||||||
|
/// Allowed status code to indicate which request methods can be used. An
|
||||||
|
/// empty Allow header indicates that the resource allows no request
|
||||||
|
/// methods, which might occur temporarily for a given resource, for
|
||||||
|
/// example.
|
||||||
|
ALLOW;
|
||||||
|
|
||||||
|
/// Advertises the availability of alternate services to clients.
|
||||||
|
ALT_SVC;
|
||||||
|
|
||||||
|
/// Contains the credentials to authenticate a user agent with a server.
|
||||||
|
///
|
||||||
|
/// Usually this header is included after the server has responded with a
|
||||||
|
/// 401 Unauthorized status and the WWW-Authenticate header.
|
||||||
|
AUTHORIZATION;
|
||||||
|
|
||||||
|
/// Specifies directives for caching mechanisms in both requests and
|
||||||
|
/// responses.
|
||||||
|
///
|
||||||
|
/// Caching directives are unidirectional, meaning that a given directive in
|
||||||
|
/// a request is not implying that the same directive is to be given in the
|
||||||
|
/// response.
|
||||||
|
CACHE_CONTROL;
|
||||||
|
|
||||||
|
/// Controls whether or not the network connection stays open after the
|
||||||
|
/// current transaction finishes.
|
||||||
|
///
|
||||||
|
/// If the value sent is keep-alive, the connection is persistent and not
|
||||||
|
/// closed, allowing for subsequent requests to the same server to be done.
|
||||||
|
///
|
||||||
|
/// Except for the standard hop-by-hop headers (Keep-Alive,
|
||||||
|
/// Transfer-Encoding, TE, Connection, Trailer, Upgrade, Proxy-Authorization
|
||||||
|
/// and Proxy-Authenticate), any hop-by-hop headers used by the message must
|
||||||
|
/// be listed in the Connection header, so that the first proxy knows he has
|
||||||
|
/// to consume them and not to forward them further. Standard hop-by-hop
|
||||||
|
/// headers can be listed too (it is often the case of Keep-Alive, but this
|
||||||
|
/// is not mandatory.
|
||||||
|
CONNECTION;
|
||||||
|
|
||||||
|
/// Indicates if the content is expected to be displayed inline.
|
||||||
|
///
|
||||||
|
/// In a regular HTTP response, the Content-Disposition response header is a
|
||||||
|
/// header indicating if the content is expected to be displayed inline in
|
||||||
|
/// the browser, that is, as a Web page or as part of a Web page, or as an
|
||||||
|
/// attachment, that is downloaded and saved locally.
|
||||||
|
///
|
||||||
|
/// In a multipart/form-data body, the HTTP Content-Disposition general
|
||||||
|
/// header is a header that can be used on the subpart of a multipart body
|
||||||
|
/// to give information about the field it applies to. The subpart is
|
||||||
|
/// delimited by the boundary defined in the Content-Type header. Used on
|
||||||
|
/// the body itself, Content-Disposition has no effect.
|
||||||
|
///
|
||||||
|
/// The Content-Disposition header is defined in the larger context of MIME
|
||||||
|
/// messages for e-mail, but only a subset of the possible parameters apply
|
||||||
|
/// to HTTP forms and POST requests. Only the value form-data, as well as
|
||||||
|
/// the optional directive name and filename, can be used in the HTTP
|
||||||
|
/// context.
|
||||||
|
CONTENT_DISPOSITION;
|
||||||
|
|
||||||
|
/// Used to compress the media-type.
|
||||||
|
///
|
||||||
|
/// When present, its value indicates what additional content encoding has
|
||||||
|
/// been applied to the entity-body. It lets the client know, how to decode
|
||||||
|
/// in order to obtain the media-type referenced by the Content-Type header.
|
||||||
|
///
|
||||||
|
/// It is recommended to compress data as much as possible and therefore to
|
||||||
|
/// use this field, but some types of resources, like jpeg images, are
|
||||||
|
/// already compressed. Sometimes using additional compression doesn't
|
||||||
|
/// reduce payload size and can even make the payload longer.
|
||||||
|
CONTENT_ENCODING;
|
||||||
|
|
||||||
|
/// Used to describe the languages intended for the audience.
|
||||||
|
///
|
||||||
|
/// This header allows a user to differentiate according to the users' own
|
||||||
|
/// preferred language. For example, if "Content-Language: de-DE" is set, it
|
||||||
|
/// says that the document is intended for German language speakers
|
||||||
|
/// (however, it doesn't indicate the document is written in German. For
|
||||||
|
/// example, it might be written in English as part of a language course for
|
||||||
|
/// German speakers).
|
||||||
|
///
|
||||||
|
/// If no Content-Language is specified, the default is that the content is
|
||||||
|
/// intended for all language audiences. Multiple language tags are also
|
||||||
|
/// possible, as well as applying the Content-Language header to various
|
||||||
|
/// media types and not only to textual documents.
|
||||||
|
CONTENT_LANGUAGE;
|
||||||
|
|
||||||
|
/// Indicates the size fo the entity-body.
|
||||||
|
///
|
||||||
|
/// The header value must be a decimal indicating the number of octets sent
|
||||||
|
/// to the recipient.
|
||||||
|
CONTENT_LENGTH;
|
||||||
|
|
||||||
|
/// Indicates an alternate location for the returned data.
|
||||||
|
///
|
||||||
|
/// The principal use case is to indicate the URL of the resource
|
||||||
|
/// transmitted as the result of content negotiation.
|
||||||
|
///
|
||||||
|
/// Location and Content-Location are different: Location indicates the
|
||||||
|
/// target of a redirection (or the URL of a newly created document), while
|
||||||
|
/// Content-Location indicates the direct URL to use to access the resource,
|
||||||
|
/// without the need of further content negotiation. Location is a header
|
||||||
|
/// associated with the response, while Content-Location is associated with
|
||||||
|
/// the entity returned.
|
||||||
|
CONTENT_LOCATION;
|
||||||
|
|
||||||
|
/// Indicates where in a full body message a partial message belongs.
|
||||||
|
CONTENT_RANGE;
|
||||||
|
|
||||||
|
/// Allows controlling resources the user agent is allowed to load for a
|
||||||
|
/// given page.
|
||||||
|
///
|
||||||
|
/// With a few exceptions, policies mostly involve specifying server origins
|
||||||
|
/// and script endpoints. This helps guard against cross-site scripting
|
||||||
|
/// attacks (XSS).
|
||||||
|
CONTENT_SECURITY_POLICY;
|
||||||
|
|
||||||
|
/// Allows experimenting with policies by monitoring their effects.
|
||||||
|
///
|
||||||
|
/// The HTTP Content-Security-Policy-Report-Only response header allows web
|
||||||
|
/// developers to experiment with policies by monitoring (but not enforcing)
|
||||||
|
/// their effects. These violation reports consist of JSON documents sent
|
||||||
|
/// via an HTTP POST request to the specified URI.
|
||||||
|
CONTENT_SECURITY_POLICY_REPORT_ONLY;
|
||||||
|
|
||||||
|
/// Used to indicate the media type of the resource.
|
||||||
|
///
|
||||||
|
/// In responses, a Content-Type header tells the client what the content
|
||||||
|
/// type of the returned content actually is. Browsers will do MIME sniffing
|
||||||
|
/// in some cases and will not necessarily follow the value of this header;
|
||||||
|
/// to prevent this behavior, the header X-Content-Type-Options can be set
|
||||||
|
/// to nosniff.
|
||||||
|
///
|
||||||
|
/// In requests, (such as POST or PUT), the client tells the server what
|
||||||
|
/// type of data is actually sent.
|
||||||
|
CONTENT_TYPE;
|
||||||
|
|
||||||
|
/// Contains stored HTTP cookies previously sent by the server with the
|
||||||
|
/// Set-Cookie header.
|
||||||
|
///
|
||||||
|
/// The Cookie header might be omitted entirely, if the privacy setting of
|
||||||
|
/// the browser are set to block them, for example.
|
||||||
|
COOKIE;
|
||||||
|
|
||||||
|
/// Indicates the client's tracking preference.
|
||||||
|
///
|
||||||
|
/// This header lets users indicate whether they would prefer privacy rather
|
||||||
|
/// than personalized content.
|
||||||
|
DNT;
|
||||||
|
|
||||||
|
/// Contains the date and time at which the message was originated.
|
||||||
|
DATE;
|
||||||
|
|
||||||
|
/// Identifier for a specific version of a resource.
|
||||||
|
///
|
||||||
|
/// This header allows caches to be more efficient, and saves bandwidth, as
|
||||||
|
/// a web server does not need to send a full response if the content has
|
||||||
|
/// not changed. On the other side, if the content has changed, etags are
|
||||||
|
/// useful to help prevent simultaneous updates of a resource from
|
||||||
|
/// overwriting each other ("mid-air collisions").
|
||||||
|
///
|
||||||
|
/// If the resource at a given URL changes, a new Etag value must be
|
||||||
|
/// generated. Etags are therefore similar to fingerprints and might also be
|
||||||
|
/// used for tracking purposes by some servers. A comparison of them allows
|
||||||
|
/// to quickly determine whether two representations of a resource are the
|
||||||
|
/// same, but they might also be set to persist indefinitely by a tracking
|
||||||
|
/// server.
|
||||||
|
ETAG;
|
||||||
|
|
||||||
|
/// Indicates expectations that need to be fulfilled by the server in order
|
||||||
|
/// to properly handle the request.
|
||||||
|
///
|
||||||
|
/// The only expectation defined in the specification is Expect:
|
||||||
|
/// 100-continue, to which the server shall respond with:
|
||||||
|
///
|
||||||
|
/// * 100 if the information contained in the header is sufficient to cause
|
||||||
|
/// an immediate success,
|
||||||
|
///
|
||||||
|
/// * 417 (Expectation Failed) if it cannot meet the expectation; or any
|
||||||
|
/// other 4xx status otherwise.
|
||||||
|
///
|
||||||
|
/// For example, the server may reject a request if its Content-Length is
|
||||||
|
/// too large.
|
||||||
|
///
|
||||||
|
/// No common browsers send the Expect header, but some other clients such
|
||||||
|
/// as cURL do so by default.
|
||||||
|
EXPECT;
|
||||||
|
|
||||||
|
/// Contains the date/time after which the response is considered stale.
|
||||||
|
///
|
||||||
|
/// Invalid dates, like the value 0, represent a date in the past and mean
|
||||||
|
/// that the resource is already expired.
|
||||||
|
///
|
||||||
|
/// If there is a Cache-Control header with the "max-age" or "s-max-age"
|
||||||
|
/// directive in the response, the Expires header is ignored.
|
||||||
|
EXPIRES;
|
||||||
|
|
||||||
|
/// Contains information from the client-facing side of proxy servers that
|
||||||
|
/// is altered or lost when a proxy is involved in the path of the request.
|
||||||
|
///
|
||||||
|
/// The alternative and de-facto standard versions of this header are the
|
||||||
|
/// X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto headers.
|
||||||
|
///
|
||||||
|
/// This header is used for debugging, statistics, and generating
|
||||||
|
/// location-dependent content and by design it exposes privacy sensitive
|
||||||
|
/// information, such as the IP address of the client. Therefore the user's
|
||||||
|
/// privacy must be kept in mind when deploying this header.
|
||||||
|
FORWARDED;
|
||||||
|
|
||||||
|
/// Contains an Internet email address for a human user who controls the
|
||||||
|
/// requesting user agent.
|
||||||
|
///
|
||||||
|
/// If you are running a robotic user agent (e.g. a crawler), the From
|
||||||
|
/// header should be sent, so you can be contacted if problems occur on
|
||||||
|
/// servers, such as if the robot is sending excessive, unwanted, or invalid
|
||||||
|
/// requests.
|
||||||
|
FROM;
|
||||||
|
|
||||||
|
/// Specifies the domain name of the server and (optionally) the TCP port
|
||||||
|
/// number on which the server is listening.
|
||||||
|
///
|
||||||
|
/// If no port is given, the default port for the service requested (e.g.,
|
||||||
|
/// "80" for an HTTP URL) is implied.
|
||||||
|
///
|
||||||
|
/// A Host header field must be sent in all HTTP/1.1 request messages. A 400
|
||||||
|
/// (Bad Request) status code will be sent to any HTTP/1.1 request message
|
||||||
|
/// that lacks a Host header field or contains more than one.
|
||||||
|
HOST;
|
||||||
|
|
||||||
|
/// Makes a request conditional based on the E-Tag.
|
||||||
|
///
|
||||||
|
/// For GET and HEAD methods, the server will send back the requested
|
||||||
|
/// resource only if it matches one of the listed ETags. For PUT and other
|
||||||
|
/// non-safe methods, it will only upload the resource in this case.
|
||||||
|
///
|
||||||
|
/// The comparison with the stored ETag uses the strong comparison
|
||||||
|
/// algorithm, meaning two files are considered identical byte to byte only.
|
||||||
|
/// This is weakened when the W/ prefix is used in front of the ETag.
|
||||||
|
///
|
||||||
|
/// There are two common use cases:
|
||||||
|
///
|
||||||
|
/// * For GET and HEAD methods, used in combination with an Range header, it
|
||||||
|
/// can guarantee that the new ranges requested comes from the same resource
|
||||||
|
/// than the previous one. If it doesn't match, then a 416 (Range Not
|
||||||
|
/// Satisfiable) response is returned.
|
||||||
|
///
|
||||||
|
/// * For other methods, and in particular for PUT, If-Match can be used to
|
||||||
|
/// prevent the lost update problem. It can check if the modification of a
|
||||||
|
/// resource that the user wants to upload will not override another change
|
||||||
|
/// that has been done since the original resource was fetched. If the
|
||||||
|
/// request cannot be fulfilled, the 412 (Precondition Failed) response is
|
||||||
|
/// returned.
|
||||||
|
IF_MATCH;
|
||||||
|
|
||||||
|
/// Makes a request conditional based on the modification date.
|
||||||
|
///
|
||||||
|
/// The If-Modified-Since request HTTP header makes the request conditional:
|
||||||
|
/// the server will send back the requested resource, with a 200 status,
|
||||||
|
/// only if it has been last modified after the given date. If the request
|
||||||
|
/// has not been modified since, the response will be a 304 without any
|
||||||
|
/// body; the Last-Modified header will contain the date of last
|
||||||
|
/// modification. Unlike If-Unmodified-Since, If-Modified-Since can only be
|
||||||
|
/// used with a GET or HEAD.
|
||||||
|
///
|
||||||
|
/// When used in combination with If-None-Match, it is ignored, unless the
|
||||||
|
/// server doesn't support If-None-Match.
|
||||||
|
///
|
||||||
|
/// The most common use case is to update a cached entity that has no
|
||||||
|
/// associated ETag.
|
||||||
|
IF_MODIFIED_SINCE;
|
||||||
|
|
||||||
|
/// Makes a request conditional based on the E-Tag.
|
||||||
|
///
|
||||||
|
/// The If-None-Match HTTP request header makes the request conditional. For
|
||||||
|
/// GET and HEAD methods, the server will send back the requested resource,
|
||||||
|
/// with a 200 status, only if it doesn't have an ETag matching the given
|
||||||
|
/// ones. For other methods, the request will be processed only if the
|
||||||
|
/// eventually existing resource's ETag doesn't match any of the values
|
||||||
|
/// listed.
|
||||||
|
///
|
||||||
|
/// When the condition fails for GET and HEAD methods, then the server must
|
||||||
|
/// return HTTP status code 304 (Not Modified). For methods that apply
|
||||||
|
/// server-side changes, the status code 412 (Precondition Failed) is used.
|
||||||
|
/// Note that the server generating a 304 response MUST generate any of the
|
||||||
|
/// following header fields that would have been sent in a 200 (OK) response
|
||||||
|
/// to the same request: Cache-Control, Content-Location, Date, ETag,
|
||||||
|
/// Expires, and Vary.
|
||||||
|
///
|
||||||
|
/// The comparison with the stored ETag uses the weak comparison algorithm,
|
||||||
|
/// meaning two files are considered identical not only if they are
|
||||||
|
/// identical byte to byte, but if the content is equivalent. For example,
|
||||||
|
/// two pages that would differ only by the date of generation in the footer
|
||||||
|
/// would be considered as identical.
|
||||||
|
///
|
||||||
|
/// When used in combination with If-Modified-Since, it has precedence (if
|
||||||
|
/// the server supports it).
|
||||||
|
///
|
||||||
|
/// There are two common use cases:
|
||||||
|
///
|
||||||
|
/// * For `GET` and `HEAD` methods, to update a cached entity that has an associated ETag.
|
||||||
|
/// * For other methods, and in particular for `PUT`, `If-None-Match` used with
|
||||||
|
/// the `*` value can be used to save a file not known to exist,
|
||||||
|
/// guaranteeing that another upload didn't happen before, losing the data
|
||||||
|
/// of the previous put; this problems is the variation of the lost update
|
||||||
|
/// problem.
|
||||||
|
IF_NONE_MATCH;
|
||||||
|
|
||||||
|
/// Makes a request conditional based on range.
|
||||||
|
///
|
||||||
|
/// The If-Range HTTP request header makes a range request conditional: if
|
||||||
|
/// the condition is fulfilled, the range request will be issued and the
|
||||||
|
/// server sends back a 206 Partial Content answer with the appropriate
|
||||||
|
/// body. If the condition is not fulfilled, the full resource is sent back,
|
||||||
|
/// with a 200 OK status.
|
||||||
|
///
|
||||||
|
/// This header can be used either with a Last-Modified validator, or with
|
||||||
|
/// an ETag, but not with both.
|
||||||
|
///
|
||||||
|
/// The most common use case is to resume a download, to guarantee that the
|
||||||
|
/// stored resource has not been modified since the last fragment has been
|
||||||
|
/// received.
|
||||||
|
IF_RANGE;
|
||||||
|
|
||||||
|
/// Makes the request conditional based on the last modification date.
|
||||||
|
///
|
||||||
|
/// The If-Unmodified-Since request HTTP header makes the request
|
||||||
|
/// conditional: the server will send back the requested resource, or accept
|
||||||
|
/// it in the case of a POST or another non-safe method, only if it has not
|
||||||
|
/// been last modified after the given date. If the request has been
|
||||||
|
/// modified after the given date, the response will be a 412 (Precondition
|
||||||
|
/// Failed) error.
|
||||||
|
///
|
||||||
|
/// There are two common use cases:
|
||||||
|
///
|
||||||
|
/// * In conjunction non-safe methods, like POST, it can be used to
|
||||||
|
/// implement an optimistic concurrency control, like done by some wikis:
|
||||||
|
/// editions are rejected if the stored document has been modified since the
|
||||||
|
/// original has been retrieved.
|
||||||
|
///
|
||||||
|
/// * In conjunction with a range request with a If-Range header, it can be
|
||||||
|
/// used to ensure that the new fragment requested comes from an unmodified
|
||||||
|
/// document.
|
||||||
|
IF_UNMODIFIED_SINCE;
|
||||||
|
|
||||||
|
/// Content-Types that are acceptable for the response.
|
||||||
|
LAST_MODIFIED;
|
||||||
|
|
||||||
|
/// Allows the server to point an interested client to another resource
|
||||||
|
/// containing metadata about the requested resource.
|
||||||
|
LINK;
|
||||||
|
|
||||||
|
/// Indicates the URL to redirect a page to.
|
||||||
|
///
|
||||||
|
/// The Location response header indicates the URL to redirect a page to. It
|
||||||
|
/// only provides a meaning when served with a 3xx status response.
|
||||||
|
///
|
||||||
|
/// The HTTP method used to make the new request to fetch the page pointed
|
||||||
|
/// to by Location depends of the original method and of the kind of
|
||||||
|
/// redirection:
|
||||||
|
///
|
||||||
|
/// * If 303 (See Also) responses always lead to the use of a GET method,
|
||||||
|
/// 307 (Temporary Redirect) and 308 (Permanent Redirect) don't change the
|
||||||
|
/// method used in the original request;
|
||||||
|
///
|
||||||
|
/// * 301 (Permanent Redirect) and 302 (Found) doesn't change the method
|
||||||
|
/// most of the time, though older user-agents may (so you basically don't
|
||||||
|
/// know).
|
||||||
|
///
|
||||||
|
/// All responses with one of these status codes send a Location header.
|
||||||
|
///
|
||||||
|
/// Beside redirect response, messages with 201 (Created) status also
|
||||||
|
/// include the Location header. It indicates the URL to the newly created
|
||||||
|
/// resource.
|
||||||
|
///
|
||||||
|
/// Location and Content-Location are different: Location indicates the
|
||||||
|
/// target of a redirection (or the URL of a newly created resource), while
|
||||||
|
/// Content-Location indicates the direct URL to use to access the resource
|
||||||
|
/// when content negotiation happened, without the need of further content
|
||||||
|
/// negotiation. Location is a header associated with the response, while
|
||||||
|
/// Content-Location is associated with the entity returned.
|
||||||
|
LOCATION;
|
||||||
|
|
||||||
|
/// Indicates the max number of intermediaries the request should be sent
|
||||||
|
/// through.
|
||||||
|
MAX_FORWARDS;
|
||||||
|
|
||||||
|
/// Indicates where a fetch originates from.
|
||||||
|
///
|
||||||
|
/// It doesn't include any path information, but only the server name. It is
|
||||||
|
/// sent with CORS requests, as well as with POST requests. It is similar to
|
||||||
|
/// the Referer header, but, unlike this header, it doesn't disclose the
|
||||||
|
/// whole path.
|
||||||
|
ORIGIN;
|
||||||
|
|
||||||
|
/// HTTP/1.0 header usually used for backwards compatibility.
|
||||||
|
///
|
||||||
|
/// The Pragma HTTP/1.0 general header is an implementation-specific header
|
||||||
|
/// that may have various effects along the request-response chain. It is
|
||||||
|
/// used for backwards compatibility with HTTP/1.0 caches where the
|
||||||
|
/// Cache-Control HTTP/1.1 header is not yet present.
|
||||||
|
PRAGMA;
|
||||||
|
|
||||||
|
/// Defines the authentication method that should be used to gain access to
|
||||||
|
/// a proxy.
|
||||||
|
///
|
||||||
|
/// Unlike `www-authenticate`, the `proxy-authenticate` header field applies
|
||||||
|
/// only to the next outbound client on the response chain. This is because
|
||||||
|
/// only the client that chose a given proxy is likely to have the
|
||||||
|
/// credentials necessary for authentication. However, when multiple proxies
|
||||||
|
/// are used within the same administrative domain, such as office and
|
||||||
|
/// regional caching proxies within a large corporate network, it is common
|
||||||
|
/// for credentials to be generated by the user agent and passed through the
|
||||||
|
/// hierarchy until consumed. Hence, in such a configuration, it will appear
|
||||||
|
/// as if Proxy-Authenticate is being forwarded because each proxy will send
|
||||||
|
/// the same challenge set.
|
||||||
|
///
|
||||||
|
/// The `proxy-authenticate` header is sent along with a `407 Proxy
|
||||||
|
/// Authentication Required`.
|
||||||
|
PROXY_AUTHENTICATE;
|
||||||
|
|
||||||
|
/// Contains the credentials to authenticate a user agent to a proxy server.
|
||||||
|
///
|
||||||
|
/// This header is usually included after the server has responded with a
|
||||||
|
/// 407 Proxy Authentication Required status and the Proxy-Authenticate
|
||||||
|
/// header.
|
||||||
|
PROXY_AUTHORIZATION;
|
||||||
|
|
||||||
|
/// Associates a specific cryptographic public key with a certain server.
|
||||||
|
///
|
||||||
|
/// This decreases the risk of MITM attacks with forged certificates. If one
|
||||||
|
/// or several keys are pinned and none of them are used by the server, the
|
||||||
|
/// browser will not accept the response as legitimate, and will not display
|
||||||
|
/// it.
|
||||||
|
PUBLIC_KEY_PINS;
|
||||||
|
|
||||||
|
/// Sends reports of pinning violation to the report-uri specified in the
|
||||||
|
/// header.
|
||||||
|
///
|
||||||
|
/// Unlike `Public-Key-Pins`, this header still allows browsers to connect
|
||||||
|
/// to the server if the pinning is violated.
|
||||||
|
PUBLIC_KEY_PINS_REPORT_ONLY;
|
||||||
|
|
||||||
|
/// Indicates the part of a document that the server should return.
|
||||||
|
///
|
||||||
|
/// Several parts can be requested with one Range header at once, and the
|
||||||
|
/// server may send back these ranges in a multipart document. If the server
|
||||||
|
/// sends back ranges, it uses the 206 Partial Content for the response. If
|
||||||
|
/// the ranges are invalid, the server returns the 416 Range Not Satisfiable
|
||||||
|
/// error. The server can also ignore the Range header and return the whole
|
||||||
|
/// document with a 200 status code.
|
||||||
|
RANGE;
|
||||||
|
|
||||||
|
/// Contains the address of the previous web page from which a link to the
|
||||||
|
/// currently requested page was followed.
|
||||||
|
///
|
||||||
|
/// The Referer header allows servers to identify where people are visiting
|
||||||
|
/// them from and may use that data for analytics, logging, or optimized
|
||||||
|
/// caching, for example.
|
||||||
|
REFERER;
|
||||||
|
|
||||||
|
/// Governs which referrer information should be included with requests
|
||||||
|
/// made.
|
||||||
|
REFERRER_POLICY;
|
||||||
|
|
||||||
|
/// Informs the web browser that the current page or frame should be
|
||||||
|
/// refreshed.
|
||||||
|
REFRESH;
|
||||||
|
|
||||||
|
/// The Retry-After response HTTP header indicates how long the user agent
|
||||||
|
/// should wait before making a follow-up request. There are two main cases
|
||||||
|
/// this header is used:
|
||||||
|
///
|
||||||
|
/// * When sent with a 503 (Service Unavailable) response, it indicates how
|
||||||
|
/// long the service is expected to be unavailable.
|
||||||
|
///
|
||||||
|
/// * When sent with a redirect response, such as 301 (Moved Permanently),
|
||||||
|
/// it indicates the minimum time that the user agent is asked to wait
|
||||||
|
/// before issuing the redirected request.
|
||||||
|
RETRY_AFTER;
|
||||||
|
|
||||||
|
/// The |Sec-WebSocket-Accept| header field is used in the WebSocket
|
||||||
|
/// opening handshake. It is sent from the server to the client to
|
||||||
|
/// confirm that the server is willing to initiate the WebSocket
|
||||||
|
/// connection.
|
||||||
|
SEC_WEBSOCKET_ACCEPT;
|
||||||
|
|
||||||
|
/// The |Sec-WebSocket-Extensions| header field is used in the WebSocket
|
||||||
|
/// opening handshake. It is initially sent from the client to the
|
||||||
|
/// server, and then subsequently sent from the server to the client, to
|
||||||
|
/// agree on a set of protocol-level extensions to use for the duration
|
||||||
|
/// of the connection.
|
||||||
|
SEC_WEBSOCKET_EXTENSIONS;
|
||||||
|
|
||||||
|
/// The |Sec-WebSocket-Key| header field is used in the WebSocket opening
|
||||||
|
/// handshake. It is sent from the client to the server to provide part
|
||||||
|
/// of the information used by the server to prove that it received a
|
||||||
|
/// valid WebSocket opening handshake. This helps ensure that the server
|
||||||
|
/// does not accept connections from non-WebSocket clients (e.g., HTTP
|
||||||
|
/// clients) that are being abused to send data to unsuspecting WebSocket
|
||||||
|
/// servers.
|
||||||
|
SEC_WEBSOCKET_KEY;
|
||||||
|
|
||||||
|
/// The |Sec-WebSocket-Protocol| header field is used in the WebSocket
|
||||||
|
/// opening handshake. It is sent from the client to the server and back
|
||||||
|
/// from the server to the client to confirm the subprotocol of the
|
||||||
|
/// connection. This enables scripts to both select a subprotocol and be
|
||||||
|
/// sure that the server agreed to serve that subprotocol.
|
||||||
|
SEC_WEBSOCKET_PROTOCOL;
|
||||||
|
|
||||||
|
/// The |Sec-WebSocket-Version| header field is used in the WebSocket
|
||||||
|
/// opening handshake. It is sent from the client to the server to
|
||||||
|
/// indicate the protocol version of the connection. This enables
|
||||||
|
/// servers to correctly interpret the opening handshake and subsequent
|
||||||
|
/// data being sent from the data, and close the connection if the server
|
||||||
|
/// cannot interpret that data in a safe manner.
|
||||||
|
SEC_WEBSOCKET_VERSION;
|
||||||
|
|
||||||
|
/// Contains information about the software used by the origin server to
|
||||||
|
/// handle the request.
|
||||||
|
///
|
||||||
|
/// Overly long and detailed Server values should be avoided as they
|
||||||
|
/// potentially reveal internal implementation details that might make it
|
||||||
|
/// (slightly) easier for attackers to find and exploit known security
|
||||||
|
/// holes.
|
||||||
|
SERVER;
|
||||||
|
|
||||||
|
/// Used to send cookies from the server to the user agent.
|
||||||
|
SET_COOKIE;
|
||||||
|
|
||||||
|
/// Tells the client to communicate with HTTPS instead of using HTTP.
|
||||||
|
STRICT_TRANSPORT_SECURITY;
|
||||||
|
|
||||||
|
/// Informs the server of transfer encodings willing to be accepted as part
|
||||||
|
/// of the response.
|
||||||
|
///
|
||||||
|
/// See also the Transfer-Encoding response header for more details on
|
||||||
|
/// transfer encodings. Note that chunked is always acceptable for HTTP/1.1
|
||||||
|
/// recipients and you that don't have to specify "chunked" using the TE
|
||||||
|
/// header. However, it is useful for setting if the client is accepting
|
||||||
|
/// trailer fields in a chunked transfer coding using the "trailers" value.
|
||||||
|
TE;
|
||||||
|
|
||||||
|
/// Allows the sender to include additional fields at the end of chunked
|
||||||
|
/// messages.
|
||||||
|
TRAILER;
|
||||||
|
|
||||||
|
/// Specifies the form of encoding used to safely transfer the entity to the
|
||||||
|
/// client.
|
||||||
|
///
|
||||||
|
/// `transfer-encoding` is a hop-by-hop header, that is applying to a
|
||||||
|
/// message between two nodes, not to a resource itself. Each segment of a
|
||||||
|
/// multi-node connection can use different `transfer-encoding` values. If
|
||||||
|
/// you want to compress data over the whole connection, use the end-to-end
|
||||||
|
/// header `content-encoding` header instead.
|
||||||
|
///
|
||||||
|
/// When present on a response to a `HEAD` request that has no body, it
|
||||||
|
/// indicates the value that would have applied to the corresponding `GET`
|
||||||
|
/// message.
|
||||||
|
TRANSFER_ENCODING;
|
||||||
|
|
||||||
|
/// Contains a string that allows identifying the requesting client's
|
||||||
|
/// software.
|
||||||
|
USER_AGENT;
|
||||||
|
|
||||||
|
/// Used as part of the exchange to upgrade the protocol.
|
||||||
|
UPGRADE;
|
||||||
|
|
||||||
|
/// Sends a signal to the server expressing the client’s preference for an
|
||||||
|
/// encrypted and authenticated response.
|
||||||
|
UPGRADE_INSECURE_REQUESTS;
|
||||||
|
|
||||||
|
/// Determines how to match future requests with cached responses.
|
||||||
|
///
|
||||||
|
/// The `vary` HTTP response header determines how to match future request
|
||||||
|
/// headers to decide whether a cached response can be used rather than
|
||||||
|
/// requesting a fresh one from the origin server. It is used by the server
|
||||||
|
/// to indicate which headers it used when selecting a representation of a
|
||||||
|
/// resource in a content negotiation algorithm.
|
||||||
|
///
|
||||||
|
/// The `vary` header should be set on a 304 Not Modified response exactly
|
||||||
|
/// like it would have been set on an equivalent 200 OK response.
|
||||||
|
VARY;
|
||||||
|
|
||||||
|
/// Added by proxies to track routing.
|
||||||
|
///
|
||||||
|
/// The `via` general header is added by proxies, both forward and reverse
|
||||||
|
/// proxies, and can appear in the request headers and the response headers.
|
||||||
|
/// It is used for tracking message forwards, avoiding request loops, and
|
||||||
|
/// identifying the protocol capabilities of senders along the
|
||||||
|
/// request/response chain.
|
||||||
|
VIA;
|
||||||
|
|
||||||
|
/// General HTTP header contains information about possible problems with
|
||||||
|
/// the status of the message.
|
||||||
|
///
|
||||||
|
/// More than one `warning` header may appear in a response. Warning header
|
||||||
|
/// fields can in general be applied to any message, however some warn-codes
|
||||||
|
/// are specific to caches and can only be applied to response messages.
|
||||||
|
WARNING;
|
||||||
|
|
||||||
|
/// Defines the authentication method that should be used to gain access to
|
||||||
|
/// a resource.
|
||||||
|
WWW_AUTHENTICATE;
|
||||||
|
|
||||||
|
/// Marker used by the server to indicate that the MIME types advertised in
|
||||||
|
/// the `content-type` headers should not be changed and be followed.
|
||||||
|
///
|
||||||
|
/// This allows to opt-out of MIME type sniffing, or, in other words, it is
|
||||||
|
/// a way to say that the webmasters knew what they were doing.
|
||||||
|
///
|
||||||
|
/// This header was introduced by Microsoft in IE 8 as a way for webmasters
|
||||||
|
/// to block content sniffing that was happening and could transform
|
||||||
|
/// non-executable MIME types into executable MIME types. Since then, other
|
||||||
|
/// browsers have introduced it, even if their MIME sniffing algorithms were
|
||||||
|
/// less aggressive.
|
||||||
|
///
|
||||||
|
/// Site security testers usually expect this header to be set.
|
||||||
|
X_CONTENT_TYPE_OPTIONS;
|
||||||
|
|
||||||
|
/// Controls DNS prefetching.
|
||||||
|
///
|
||||||
|
/// The `x-dns-prefetch-control` HTTP response header controls DNS
|
||||||
|
/// prefetching, a feature by which browsers proactively perform domain name
|
||||||
|
/// resolution on both links that the user may choose to follow as well as
|
||||||
|
/// URLs for items referenced by the document, including images, CSS,
|
||||||
|
/// JavaScript, and so forth.
|
||||||
|
///
|
||||||
|
/// This prefetching is performed in the background, so that the DNS is
|
||||||
|
/// likely to have been resolved by the time the referenced items are
|
||||||
|
/// needed. This reduces latency when the user clicks a link.
|
||||||
|
X_DNS_PREFETCH_CONTROL;
|
||||||
|
|
||||||
|
/// Indicates whether or not a browser should be allowed to render a page in
|
||||||
|
/// a frame.
|
||||||
|
///
|
||||||
|
/// Sites can use this to avoid clickjacking attacks, by ensuring that their
|
||||||
|
/// content is not embedded into other sites.
|
||||||
|
///
|
||||||
|
/// The added security is only provided if the user accessing the document
|
||||||
|
/// is using a browser supporting `x-frame-options`.
|
||||||
|
X_FRAME_OPTIONS;
|
||||||
|
|
||||||
|
/// Stop pages from loading when an XSS attack is detected.
|
||||||
|
///
|
||||||
|
/// The HTTP X-XSS-Protection response header is a feature of Internet
|
||||||
|
/// Explorer, Chrome and Safari that stops pages from loading when they
|
||||||
|
/// detect reflected cross-site scripting (XSS) attacks. Although these
|
||||||
|
/// protections are largely unnecessary in modern browsers when sites
|
||||||
|
/// implement a strong Content-Security-Policy that disables the use of
|
||||||
|
/// inline JavaScript ('unsafe-inline'), they can still provide protections
|
||||||
|
/// for users of older web browsers that don't yet support CSP.
|
||||||
|
X_XSS_PROTECTION;
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for HeaderName {
|
||||||
|
#[inline]
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Borrow<str> for HeaderName {
|
||||||
|
#[inline]
|
||||||
|
fn borrow(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for HeaderName {
|
||||||
|
#[inline]
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for HeaderName {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self(s.parse::<http::header::HeaderName>().map_err(
|
||||||
|
|_| Error::internal_server_error(ErrorInvalidHeaderName),
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/header/value.rs
Normal file
83
src/header/value.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::error::{Error, ErrorInvalidHeaderValue, Result};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct HeaderValue(pub(crate) http::header::HeaderValue);
|
||||||
|
|
||||||
|
impl HeaderValue {
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn into_inner(self) -> http::header::HeaderValue {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn to_str(&self) -> Result<&str> {
|
||||||
|
self.0
|
||||||
|
.to_str()
|
||||||
|
.map_err(|_| Error::internal_server_error(ErrorInvalidHeaderValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn from_static(src: &'static str) -> Self {
|
||||||
|
Self(http::header::HeaderValue::from_static(src))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.0.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for HeaderValue {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self(s.parse::<http::header::HeaderValue>().map_err(
|
||||||
|
|_| Error::internal_server_error(ErrorInvalidHeaderValue),
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for HeaderValue {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
|
Self::from_str(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! value_from {
|
||||||
|
($($ty:ty),*) => {
|
||||||
|
$(
|
||||||
|
impl From<$ty> for HeaderValue {
|
||||||
|
fn from(value: $ty) -> Self {
|
||||||
|
Self(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value_from!(i16, i32, i64, u16, u32, u64, isize, usize);
|
||||||
|
|
||||||
|
impl PartialEq<str> for HeaderValue {
|
||||||
|
#[inline]
|
||||||
|
fn eq(&self, other: &str) -> bool {
|
||||||
|
self.0.eq(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<String> for HeaderValue {
|
||||||
|
#[inline]
|
||||||
|
fn eq(&self, other: &String) -> bool {
|
||||||
|
self.0.eq(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/lib.rs
Normal file
36
src/lib.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(private_in_public, unreachable_pub)]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub mod middlewares;
|
||||||
|
pub mod route;
|
||||||
|
pub mod uri;
|
||||||
|
pub mod web;
|
||||||
|
|
||||||
|
mod body;
|
||||||
|
mod endpoint;
|
||||||
|
mod header;
|
||||||
|
mod method;
|
||||||
|
mod middleware;
|
||||||
|
mod request;
|
||||||
|
mod response;
|
||||||
|
mod route_recognizer;
|
||||||
|
mod server;
|
||||||
|
mod status_code;
|
||||||
|
mod version;
|
||||||
|
|
||||||
|
pub use http::Extensions;
|
||||||
|
|
||||||
|
pub use body::Body;
|
||||||
|
pub use endpoint::{Endpoint, EndpointExt, FnHandler};
|
||||||
|
pub use error::{Error, Result};
|
||||||
|
pub use header::{map::HeaderMap, name::HeaderName, value::HeaderValue};
|
||||||
|
pub use method::Method;
|
||||||
|
pub use middleware::Middleware;
|
||||||
|
pub use request::{Request, RequestBuilder};
|
||||||
|
pub use response::{Response, ResponseBuilder};
|
||||||
|
pub use server::Server;
|
||||||
|
pub use status_code::StatusCode;
|
||||||
|
pub use version::Version;
|
||||||
|
pub use web::{FromRequest, IntoResponse};
|
||||||
14
src/method.rs
Normal file
14
src/method.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum Method {
|
||||||
|
Options = 0,
|
||||||
|
Get = 1,
|
||||||
|
Post = 2,
|
||||||
|
Put = 3,
|
||||||
|
Delete = 4,
|
||||||
|
Head = 5,
|
||||||
|
Trace = 6,
|
||||||
|
Connect = 7,
|
||||||
|
Patch = 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const COUNT_METHODS: usize = 9;
|
||||||
5
src/middleware.rs
Normal file
5
src/middleware.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
use crate::Endpoint;
|
||||||
|
|
||||||
|
pub trait Middleware {
|
||||||
|
fn transform<T: Endpoint>(&self, ep: T) -> Box<dyn Endpoint>;
|
||||||
|
}
|
||||||
33
src/middlewares/add_data.rs
Normal file
33
src/middlewares/add_data.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use crate::{Endpoint, Middleware, Request, Response, Result};
|
||||||
|
|
||||||
|
pub struct AddData<D> {
|
||||||
|
value: D,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: Clone + Send + Sync + 'static> AddData<D> {
|
||||||
|
pub fn new(value: D) -> Self {
|
||||||
|
AddData { value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D: Clone + Send + Sync + 'static> Middleware for AddData<D> {
|
||||||
|
fn transform<T: Endpoint>(&self, ep: T) -> Box<dyn Endpoint> {
|
||||||
|
Box::new(AddDataImpl {
|
||||||
|
inner: ep,
|
||||||
|
value: self.value.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AddDataImpl<E, T> {
|
||||||
|
inner: E,
|
||||||
|
value: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<E: Endpoint, T: Clone + Send + Sync + 'static> Endpoint for AddDataImpl<E, T> {
|
||||||
|
async fn call(&self, mut req: Request) -> Result<Response> {
|
||||||
|
req.extensions_mut().insert(self.value.clone());
|
||||||
|
self.inner.call(req).await
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/middlewares/mod.rs
Normal file
5
src/middlewares/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mod add_data;
|
||||||
|
mod strip_prefix;
|
||||||
|
|
||||||
|
pub use add_data::AddData;
|
||||||
|
pub use strip_prefix::StripPrefix;
|
||||||
52
src/middlewares/strip_prefix.rs
Normal file
52
src/middlewares/strip_prefix.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::error::{Error, ErrorNotFound};
|
||||||
|
use crate::uri::Uri;
|
||||||
|
use crate::{Endpoint, Middleware, Request, Response, Result};
|
||||||
|
|
||||||
|
pub struct StripPrefix {
|
||||||
|
prefix: Arc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StripPrefix {
|
||||||
|
pub fn new(prefix: impl AsRef<str>) -> Self {
|
||||||
|
Self {
|
||||||
|
prefix: prefix.as_ref().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Middleware for StripPrefix {
|
||||||
|
fn transform<T: Endpoint>(&self, ep: T) -> Box<dyn Endpoint> {
|
||||||
|
Box::new(StripPrefixImpl {
|
||||||
|
inner: ep,
|
||||||
|
prefix: self.prefix.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StripPrefixImpl<E> {
|
||||||
|
inner: E,
|
||||||
|
prefix: Arc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<E: Endpoint> Endpoint for StripPrefixImpl<E> {
|
||||||
|
async fn call(&self, mut req: Request) -> Result<Response> {
|
||||||
|
let mut parts = req.uri().clone().into_parts();
|
||||||
|
|
||||||
|
if let Some(path) = parts
|
||||||
|
.path_and_query
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.as_str().strip_prefix(&*self.prefix))
|
||||||
|
{
|
||||||
|
parts.path_and_query = Some(path.parse()?);
|
||||||
|
} else {
|
||||||
|
return Err(Error::not_found(ErrorNotFound));
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_uri = Uri::from_parts(parts)?;
|
||||||
|
req.set_uri(new_uri);
|
||||||
|
self.inner.call(req).await
|
||||||
|
}
|
||||||
|
}
|
||||||
195
src/request.rs
Normal file
195
src/request.rs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
use std::any::Any;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
use crate::error::ErrorInvalidMethod;
|
||||||
|
use crate::uri::Uri;
|
||||||
|
use crate::{Body, Error, Extensions, HeaderMap, HeaderName, HeaderValue, Method, Result, Version};
|
||||||
|
|
||||||
|
struct Parts {
|
||||||
|
method: Method,
|
||||||
|
uri: Uri,
|
||||||
|
version: Version,
|
||||||
|
headers: HeaderMap,
|
||||||
|
extensions: Extensions,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Request {
|
||||||
|
method: Method,
|
||||||
|
uri: Uri,
|
||||||
|
version: Version,
|
||||||
|
headers: HeaderMap,
|
||||||
|
extensions: Extensions,
|
||||||
|
body: Body,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
pub(crate) fn from_hyper(req: hyper::Request<hyper::Body>) -> Result<Self> {
|
||||||
|
let (parts, body) = req.into_parts();
|
||||||
|
Ok(Self {
|
||||||
|
method: match parts.method {
|
||||||
|
http::Method::GET => Method::Get,
|
||||||
|
_ => return Err(Error::internal_server_error(ErrorInvalidMethod)),
|
||||||
|
},
|
||||||
|
uri: Uri(parts.uri),
|
||||||
|
version: Version(parts.version),
|
||||||
|
headers: HeaderMap(parts.headers),
|
||||||
|
extensions: parts.extensions,
|
||||||
|
body: Body(body),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn builder() -> RequestBuilder {
|
||||||
|
RequestBuilder(Ok(Parts {
|
||||||
|
method: Method::Get,
|
||||||
|
uri: Default::default(),
|
||||||
|
version: Default::default(),
|
||||||
|
headers: Default::default(),
|
||||||
|
extensions: Default::default(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn method(&self) -> Method {
|
||||||
|
self.method
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_method(&mut self, method: Method) {
|
||||||
|
self.method = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn uri(&self) -> &Uri {
|
||||||
|
&self.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_uri(&mut self, uri: Uri) {
|
||||||
|
self.uri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn version(&self) -> Version {
|
||||||
|
self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_version(&mut self, version: Version) {
|
||||||
|
self.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn headers(&self) -> &HeaderMap {
|
||||||
|
&self.headers
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||||
|
&mut self.headers
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the associated extensions.
|
||||||
|
#[inline]
|
||||||
|
pub fn extensions(&self) -> &Extensions {
|
||||||
|
&self.extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the associated extensions.
|
||||||
|
#[inline]
|
||||||
|
pub fn extensions_mut(&mut self) -> &mut Extensions {
|
||||||
|
&mut self.extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn take_body(&mut self) -> Body {
|
||||||
|
std::mem::take(&mut self.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RequestBuilder(Result<Parts>);
|
||||||
|
|
||||||
|
impl RequestBuilder {
|
||||||
|
/// Sets the HTTP method for this request.
|
||||||
|
///
|
||||||
|
/// By default this is [Method::Get].
|
||||||
|
pub fn method(self, method: Method) -> RequestBuilder {
|
||||||
|
Self(self.0.map(move |parts| Parts { method, ..parts }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the URI for this request.
|
||||||
|
///
|
||||||
|
/// By default this is `/`.
|
||||||
|
pub fn uri<T>(self, uri: T) -> RequestBuilder
|
||||||
|
where
|
||||||
|
T: TryInto<Uri, Error = Error>,
|
||||||
|
{
|
||||||
|
Self(self.0.and_then(move |parts| {
|
||||||
|
Ok(Parts {
|
||||||
|
uri: uri.try_into()?,
|
||||||
|
..parts
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the HTTP version for this request.
|
||||||
|
pub fn version(self, version: Version) -> RequestBuilder {
|
||||||
|
Self(self.0.map(move |parts| Parts { version, ..parts }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends a header to this request builder.
|
||||||
|
///
|
||||||
|
/// This function will append the provided key/value as a header to the
|
||||||
|
/// internal [HeaderMap] being constructed.
|
||||||
|
pub fn header<K, V>(self, key: K, value: V) -> Self
|
||||||
|
where
|
||||||
|
K: TryInto<HeaderName, Error = Error>,
|
||||||
|
V: TryInto<HeaderValue, Error = Error>,
|
||||||
|
{
|
||||||
|
Self(self.0.and_then(move |mut parts| {
|
||||||
|
let key = key.try_into()?;
|
||||||
|
let value = value.try_into()?;
|
||||||
|
parts.headers.append(key, value);
|
||||||
|
Ok(parts)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `Content-Type` header on the request.
|
||||||
|
pub fn content_type(self, content_type: &str) -> Self {
|
||||||
|
Self(self.0.and_then(move |mut parts| {
|
||||||
|
let value = content_type.parse()?;
|
||||||
|
parts.headers.append(HeaderName::CONTENT_TYPE, value);
|
||||||
|
Ok(parts)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an extension to this request.
|
||||||
|
pub fn extension<T>(self, extension: T) -> Self
|
||||||
|
where
|
||||||
|
T: Any + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
Self(self.0.map(move |mut parts| {
|
||||||
|
parts.extensions.insert(extension);
|
||||||
|
parts
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes this builder, using the provided body to return a constructed [Request].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function may return an error if any previously configured argument
|
||||||
|
/// failed to parse or get converted to the internal representation. For
|
||||||
|
/// example if an invalid `head` was specified via `header("Foo",
|
||||||
|
/// "Bar\r\n")` the error will be returned when this function is called
|
||||||
|
/// rather than when `header` was called.
|
||||||
|
pub fn body(self, body: Body) -> Result<Request> {
|
||||||
|
self.0.map(move |parts| Request {
|
||||||
|
method: parts.method,
|
||||||
|
uri: parts.uri,
|
||||||
|
version: parts.version,
|
||||||
|
headers: parts.headers,
|
||||||
|
extensions: parts.extensions,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
164
src/response.rs
Normal file
164
src/response.rs
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
use std::any::Any;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Body, Error, Extensions, HeaderMap, HeaderName, HeaderValue, Result, StatusCode, Version,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Parts {
|
||||||
|
status: StatusCode,
|
||||||
|
version: Version,
|
||||||
|
headers: HeaderMap,
|
||||||
|
extensions: Extensions,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Response {
|
||||||
|
status: StatusCode,
|
||||||
|
version: Version,
|
||||||
|
headers: HeaderMap,
|
||||||
|
extensions: Extensions,
|
||||||
|
body: Body,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
pub(crate) fn into_hyper(self) -> hyper::Response<hyper::Body> {
|
||||||
|
let mut resp = hyper::Response::new(self.body.0);
|
||||||
|
*resp.status_mut() = self.status.0;
|
||||||
|
*resp.version_mut() = self.version.0;
|
||||||
|
*resp.headers_mut() = self.headers.0;
|
||||||
|
*resp.extensions_mut() = self.extensions;
|
||||||
|
resp
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn builder() -> ResponseBuilder {
|
||||||
|
ResponseBuilder(Ok(Parts {
|
||||||
|
status: StatusCode::OK,
|
||||||
|
version: Default::default(),
|
||||||
|
headers: Default::default(),
|
||||||
|
extensions: Default::default(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn status(&self) -> StatusCode {
|
||||||
|
self.status
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_status(&mut self, status: StatusCode) {
|
||||||
|
self.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn headers(&self) -> &HeaderMap {
|
||||||
|
&self.headers
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||||
|
&mut self.headers
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn version(&self) -> Version {
|
||||||
|
self.version
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_version(&mut self, version: Version) {
|
||||||
|
self.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the associated extensions.
|
||||||
|
#[inline]
|
||||||
|
pub fn extensions(&self) -> &Extensions {
|
||||||
|
&self.extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the associated extensions.
|
||||||
|
#[inline]
|
||||||
|
pub fn extensions_mut(&mut self) -> &mut Extensions {
|
||||||
|
&mut self.extensions
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn take_body(mut self) -> Body {
|
||||||
|
std::mem::take(&mut self.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResponseBuilder(Result<Parts>);
|
||||||
|
|
||||||
|
impl ResponseBuilder {
|
||||||
|
/// Sets the HTTP status for this response.
|
||||||
|
///
|
||||||
|
/// By default this is [StatusCode::OK].
|
||||||
|
pub fn status(self, status: StatusCode) -> Self {
|
||||||
|
Self(self.0.map(|parts| Parts { status, ..parts }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the HTTP version for this response.
|
||||||
|
///
|
||||||
|
/// By default this is [Version::HTTP_11]
|
||||||
|
pub fn version(self, version: Version) -> Self {
|
||||||
|
Self(self.0.map(|parts| Parts { version, ..parts }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends a header to this response builder.
|
||||||
|
///
|
||||||
|
/// This function will append the provided key/value as a header to the
|
||||||
|
/// internal [HeaderMap] being constructed.
|
||||||
|
pub fn header<K, V>(self, key: K, value: V) -> Self
|
||||||
|
where
|
||||||
|
K: TryInto<HeaderName>,
|
||||||
|
K::Error: Into<Error>,
|
||||||
|
V: TryInto<HeaderValue>,
|
||||||
|
V::Error: Into<Error>,
|
||||||
|
{
|
||||||
|
Self(self.0.and_then(move |mut parts| {
|
||||||
|
let key = key.try_into().map_err(Into::into)?;
|
||||||
|
let value = value.try_into().map_err(Into::into)?;
|
||||||
|
parts.headers.append(key, value);
|
||||||
|
Ok(parts)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `Content-Type` header on the response.
|
||||||
|
pub fn content_type(self, content_type: &str) -> Self {
|
||||||
|
Self(self.0.and_then(move |mut parts| {
|
||||||
|
let value = content_type.parse()?;
|
||||||
|
parts.headers.append(HeaderName::CONTENT_TYPE, value);
|
||||||
|
Ok(parts)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an extension to this response.
|
||||||
|
pub fn extension<T>(self, extension: T) -> Self
|
||||||
|
where
|
||||||
|
T: Any + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
Self(self.0.map(move |mut parts| {
|
||||||
|
parts.extensions.insert(extension);
|
||||||
|
parts
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes this builder, using the provided body to return a constructed [Response].
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function may return an error if any previously configured argument
|
||||||
|
/// failed to parse or get converted to the internal representation. For
|
||||||
|
/// example if an invalid `head` was specified via `header("Foo",
|
||||||
|
/// "Bar\r\n")` the error will be returned when this function is called
|
||||||
|
/// rather than when `header` was called.
|
||||||
|
pub fn body(self, body: Body) -> Result<Response> {
|
||||||
|
self.0.map(move |parts| Response {
|
||||||
|
status: parts.status,
|
||||||
|
version: parts.version,
|
||||||
|
headers: parts.headers,
|
||||||
|
extensions: parts.extensions,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/route.rs
Normal file
124
src/route.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use crate::endpoint::{FnHandler, FnHandlerWrapper};
|
||||||
|
use crate::error::ErrorNotFound;
|
||||||
|
use crate::method::COUNT_METHODS;
|
||||||
|
use crate::route_recognizer::Router;
|
||||||
|
use crate::{Endpoint, Error, Method, Request, Response};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Route {
|
||||||
|
router: Router<Box<dyn Endpoint>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Route {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
router: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn at(mut self, path: &str, ep: impl Endpoint) -> Self {
|
||||||
|
self.router.add(path, Box::new(ep));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Endpoint for Route {
|
||||||
|
async fn call(&self, mut req: Request) -> crate::Result<Response> {
|
||||||
|
let m = self
|
||||||
|
.router
|
||||||
|
.recognize(req.uri().path())
|
||||||
|
.ok()
|
||||||
|
.ok_or_else(|| Error::not_found(ErrorNotFound))?;
|
||||||
|
req.extensions_mut().insert(m.params);
|
||||||
|
m.handler.call(req).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! define_method_fn {
|
||||||
|
($($(#[$docs:meta])* ($name:ident, $method:ident);)*) => {
|
||||||
|
$(
|
||||||
|
$(#[$docs])*
|
||||||
|
pub fn $name<T, In>(ep: T) -> RouteMethod
|
||||||
|
where
|
||||||
|
T: FnHandler<In> + 'static,
|
||||||
|
In: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let mut router = RouteMethod::default();
|
||||||
|
router.router[Method::$method as usize] = Some(Box::new(FnHandlerWrapper::new(ep)) as Box<dyn Endpoint>);
|
||||||
|
router
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
define_method_fn!(
|
||||||
|
(get, Get);
|
||||||
|
(post, Post);
|
||||||
|
(put, Put);
|
||||||
|
(delete, Delete);
|
||||||
|
(head, Head);
|
||||||
|
(options, Options);
|
||||||
|
(connect, Connect);
|
||||||
|
(patch, Patch);
|
||||||
|
(trace, Trace);
|
||||||
|
);
|
||||||
|
|
||||||
|
macro_rules! define_methods {
|
||||||
|
($($(#[$docs:meta])* ($name:ident, $method:ident);)*) => {
|
||||||
|
$(
|
||||||
|
$(#[$docs])*
|
||||||
|
pub fn $name<T, In>(mut self, ep: T) -> Self
|
||||||
|
where
|
||||||
|
T: FnHandler<In> + 'static,
|
||||||
|
In: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.router[Method::$method as usize] = Some(Box::new(FnHandlerWrapper::new(ep)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct RouteMethod {
|
||||||
|
router: [Option<Box<dyn Endpoint>>; COUNT_METHODS],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RouteMethod {
|
||||||
|
pub fn method<T, In>(mut self, method: Method, ep: T) -> Self
|
||||||
|
where
|
||||||
|
T: FnHandler<In> + 'static,
|
||||||
|
In: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.router[method as usize] = Some(Box::new(FnHandlerWrapper::new(ep)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
define_methods!(
|
||||||
|
(get, Get);
|
||||||
|
(post, Post);
|
||||||
|
(put, Put);
|
||||||
|
(delete, Delete);
|
||||||
|
(head, Head);
|
||||||
|
(options, Options);
|
||||||
|
(connect, Connect);
|
||||||
|
(patch, Patch);
|
||||||
|
(trace, Trace);
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Endpoint for RouteMethod {
|
||||||
|
async fn call(&self, req: Request) -> crate::Result<Response> {
|
||||||
|
if let Some(ep) = self
|
||||||
|
.router
|
||||||
|
.get(req.method() as usize)
|
||||||
|
.and_then(|ep| ep.as_ref())
|
||||||
|
{
|
||||||
|
ep.call(req).await
|
||||||
|
} else {
|
||||||
|
Err(Error::not_found(ErrorNotFound))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
433
src/route_recognizer/mod.rs
Normal file
433
src/route_recognizer/mod.rs
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
mod nfa;
|
||||||
|
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::ops::Index;
|
||||||
|
|
||||||
|
use nfa::{CharacterClass, NFA};
|
||||||
|
|
||||||
|
#[derive(Clone, Eq, Debug)]
|
||||||
|
struct Metadata {
|
||||||
|
statics: u32,
|
||||||
|
dynamics: u32,
|
||||||
|
wildcards: u32,
|
||||||
|
param_names: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metadata {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
statics: 0,
|
||||||
|
dynamics: 0,
|
||||||
|
wildcards: 0,
|
||||||
|
param_names: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for Metadata {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
if self.statics > other.statics {
|
||||||
|
Ordering::Greater
|
||||||
|
} else if self.statics < other.statics {
|
||||||
|
Ordering::Less
|
||||||
|
} else if self.dynamics > other.dynamics {
|
||||||
|
Ordering::Greater
|
||||||
|
} else if self.dynamics < other.dynamics {
|
||||||
|
Ordering::Less
|
||||||
|
} else if self.wildcards > other.wildcards {
|
||||||
|
Ordering::Greater
|
||||||
|
} else if self.wildcards < other.wildcards {
|
||||||
|
Ordering::Less
|
||||||
|
} else {
|
||||||
|
Ordering::Equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Metadata {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Metadata {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.statics == other.statics
|
||||||
|
&& self.dynamics == other.dynamics
|
||||||
|
&& self.wildcards == other.wildcards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Router parameters.
|
||||||
|
#[derive(PartialEq, Clone, Debug, Default)]
|
||||||
|
pub(crate) struct Params(pub(crate) Vec<(String, String)>);
|
||||||
|
|
||||||
|
impl Index<&str> for Params {
|
||||||
|
type Output = str;
|
||||||
|
|
||||||
|
fn index(&self, name: &str) -> &Self::Output {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.find(|(n, _)| n == name)
|
||||||
|
.map(|(_, value)| value.as_str())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of a successful match returned by `Router::recognize`.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct Match<T> {
|
||||||
|
/// Return the endpoint handler.
|
||||||
|
pub(crate) handler: T,
|
||||||
|
/// Return the params.
|
||||||
|
pub(crate) params: Params,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recognizes URL patterns with support for dynamic and wildcard segments.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Router<T> {
|
||||||
|
nfa: NFA<Metadata>,
|
||||||
|
handlers: BTreeMap<usize, T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segments(route: &str) -> Vec<(Option<char>, &str)> {
|
||||||
|
let predicate = |c| c == '.' || c == '/';
|
||||||
|
|
||||||
|
let mut segments = vec![];
|
||||||
|
let mut segment_start = 0;
|
||||||
|
|
||||||
|
while segment_start < route.len() {
|
||||||
|
let segment_end = route[segment_start + 1..]
|
||||||
|
.find(predicate)
|
||||||
|
.map(|i| i + segment_start + 1)
|
||||||
|
.unwrap_or_else(|| route.len());
|
||||||
|
let potential_sep = route.chars().nth(segment_start);
|
||||||
|
let sep_and_segment = match potential_sep {
|
||||||
|
Some(sep) if predicate(sep) => (Some(sep), &route[segment_start + 1..segment_end]),
|
||||||
|
_ => (None, &route[segment_start..segment_end]),
|
||||||
|
};
|
||||||
|
|
||||||
|
segments.push(sep_and_segment);
|
||||||
|
segment_start = segment_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
segments
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Router<T> {
|
||||||
|
/// Create a new instance of `Router`.
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
nfa: NFA::new(),
|
||||||
|
handlers: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a route to the router.
|
||||||
|
pub(crate) fn add(&mut self, mut route: &str, dest: T) {
|
||||||
|
if !route.is_empty() && route.as_bytes()[0] == b'/' {
|
||||||
|
route = &route[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
let nfa = &mut self.nfa;
|
||||||
|
let mut state = 0;
|
||||||
|
let mut metadata = Metadata::new();
|
||||||
|
|
||||||
|
for (separator, segment) in segments(route) {
|
||||||
|
if let Some(separator) = separator {
|
||||||
|
state = nfa.put(state, CharacterClass::valid_char(separator));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !segment.is_empty() && segment.as_bytes()[0] == b':' {
|
||||||
|
state = process_dynamic_segment(nfa, state);
|
||||||
|
metadata.dynamics += 1;
|
||||||
|
metadata.param_names.push(segment[1..].to_string());
|
||||||
|
} else if !segment.is_empty() && segment.as_bytes()[0] == b'*' {
|
||||||
|
state = process_star_state(nfa, state);
|
||||||
|
metadata.wildcards += 1;
|
||||||
|
metadata.param_names.push(segment[1..].to_string());
|
||||||
|
} else {
|
||||||
|
state = process_static_segment(segment, nfa, state);
|
||||||
|
metadata.statics += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nfa.acceptance(state);
|
||||||
|
nfa.metadata(state, metadata);
|
||||||
|
self.handlers.insert(state, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Match a route on the router.
|
||||||
|
pub(crate) fn recognize(&self, mut path: &str) -> Result<Match<&T>, String> {
|
||||||
|
if !path.is_empty() && path.as_bytes()[0] == b'/' {
|
||||||
|
path = &path[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
let nfa = &self.nfa;
|
||||||
|
let result = nfa.process(path, |index| nfa.get(index).metadata.as_ref().unwrap());
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(nfa_match) => {
|
||||||
|
let mut params = Params::default();
|
||||||
|
let state = &nfa.get(nfa_match.state);
|
||||||
|
let metadata = state.metadata.as_ref().unwrap();
|
||||||
|
let param_names = metadata.param_names.clone();
|
||||||
|
|
||||||
|
for (i, capture) in nfa_match.captures.iter().enumerate() {
|
||||||
|
if !param_names[i].is_empty() {
|
||||||
|
params
|
||||||
|
.0
|
||||||
|
.push((param_names[i].to_string(), capture.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let handler = self.handlers.get(&nfa_match.state).unwrap();
|
||||||
|
Ok(Match { handler, params })
|
||||||
|
}
|
||||||
|
Err(str) => Err(str),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for Router<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_static_segment<T>(segment: &str, nfa: &mut NFA<T>, mut state: usize) -> usize {
|
||||||
|
for char in segment.chars() {
|
||||||
|
state = nfa.put(state, CharacterClass::valid_char(char));
|
||||||
|
}
|
||||||
|
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_dynamic_segment<T>(nfa: &mut NFA<T>, mut state: usize) -> usize {
|
||||||
|
state = nfa.put(state, CharacterClass::invalid_char('/'));
|
||||||
|
nfa.put_state(state, state);
|
||||||
|
nfa.start_capture(state);
|
||||||
|
nfa.end_capture(state);
|
||||||
|
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_star_state<T>(nfa: &mut NFA<T>, mut state: usize) -> usize {
|
||||||
|
state = nfa.put(state, CharacterClass::any());
|
||||||
|
nfa.put_state(state, state);
|
||||||
|
nfa.start_capture(state);
|
||||||
|
nfa.end_capture(state);
|
||||||
|
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Params, Router};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_router() {
|
||||||
|
let mut router = Router::new();
|
||||||
|
|
||||||
|
router.add("/thomas", "Thomas".to_string());
|
||||||
|
router.add("/tom", "Tom".to_string());
|
||||||
|
router.add("/wycats", "Yehuda".to_string());
|
||||||
|
|
||||||
|
let m = router.recognize("/thomas").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(*m.handler, "Thomas".to_string());
|
||||||
|
assert_eq!(m.params, Params::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn root_router() {
|
||||||
|
let mut router = Router::new();
|
||||||
|
router.add("/", 10);
|
||||||
|
assert_eq!(*router.recognize("/").unwrap().handler, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_path() {
|
||||||
|
let mut router = Router::new();
|
||||||
|
router.add("/", 12);
|
||||||
|
assert_eq!(*router.recognize("").unwrap().handler, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_route() {
|
||||||
|
let mut router = Router::new();
|
||||||
|
router.add("", 12);
|
||||||
|
assert_eq!(*router.recognize("/").unwrap().handler, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ambiguous_router() {
|
||||||
|
let mut router = Router::new();
|
||||||
|
|
||||||
|
router.add("/posts/new", "new".to_string());
|
||||||
|
router.add("/posts/:id", "id".to_string());
|
||||||
|
|
||||||
|
let id = router.recognize("/posts/1").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(*id.handler, "id".to_string());
|
||||||
|
assert_eq!(id.params, params("id", "1"));
|
||||||
|
|
||||||
|
let new = router.recognize("/posts/new").unwrap();
|
||||||
|
assert_eq!(*new.handler, "new".to_string());
|
||||||
|
assert_eq!(new.params, Params::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ambiguous_router_b() {
|
||||||
|
let mut router = Router::new();
|
||||||
|
|
||||||
|
router.add("/posts/:id", "id".to_string());
|
||||||
|
router.add("/posts/new", "new".to_string());
|
||||||
|
|
||||||
|
let id = router.recognize("/posts/1").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(*id.handler, "id".to_string());
|
||||||
|
assert_eq!(id.params, params("id", "1"));
|
||||||
|
|
||||||
|
let new = router.recognize("/posts/new").unwrap();
|
||||||
|
assert_eq!(*new.handler, "new".to_string());
|
||||||
|
assert_eq!(new.params, Params::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_params() {
|
||||||
|
let mut router = Router::new();
|
||||||
|
|
||||||
|
router.add("/posts/:post_id/comments/:id", "comment".to_string());
|
||||||
|
router.add("/posts/:post_id/comments", "comments".to_string());
|
||||||
|
|
||||||
|
let com = router.recognize("/posts/12/comments/100").unwrap();
|
||||||
|
let coms = router.recognize("/posts/12/comments").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(*com.handler, "comment".to_string());
|
||||||
|
assert_eq!(com.params, two_params("post_id", "12", "id", "100"));
|
||||||
|
|
||||||
|
assert_eq!(*coms.handler, "comments".to_string());
|
||||||
|
assert_eq!(coms.params, params("post_id", "12"));
|
||||||
|
assert_eq!(coms.params["post_id"], "12".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wildcard() {
|
||||||
|
let mut router = Router::new();
|
||||||
|
|
||||||
|
router.add("*foo", "test".to_string());
|
||||||
|
router.add("/bar/*foo", "test2".to_string());
|
||||||
|
|
||||||
|
let m = router.recognize("/test").unwrap();
|
||||||
|
assert_eq!(*m.handler, "test".to_string());
|
||||||
|
assert_eq!(m.params, params("foo", "test"));
|
||||||
|
|
||||||
|
let m = router.recognize("/foo/bar").unwrap();
|
||||||
|
assert_eq!(*m.handler, "test".to_string());
|
||||||
|
assert_eq!(m.params, params("foo", "foo/bar"));
|
||||||
|
|
||||||
|
let m = router.recognize("/bar/foo").unwrap();
|
||||||
|
assert_eq!(*m.handler, "test2".to_string());
|
||||||
|
assert_eq!(m.params, params("foo", "foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wildcard_colon() {
|
||||||
|
let mut router = Router::new();
|
||||||
|
|
||||||
|
router.add("/a/*b", "ab".to_string());
|
||||||
|
router.add("/a/*b/c", "abc".to_string());
|
||||||
|
router.add("/a/*b/c/:d", "abcd".to_string());
|
||||||
|
|
||||||
|
let m = router.recognize("/a/foo").unwrap();
|
||||||
|
assert_eq!(*m.handler, "ab".to_string());
|
||||||
|
assert_eq!(m.params, params("b", "foo"));
|
||||||
|
|
||||||
|
let m = router.recognize("/a/foo/bar").unwrap();
|
||||||
|
assert_eq!(*m.handler, "ab".to_string());
|
||||||
|
assert_eq!(m.params, params("b", "foo/bar"));
|
||||||
|
|
||||||
|
let m = router.recognize("/a/foo/c").unwrap();
|
||||||
|
assert_eq!(*m.handler, "abc".to_string());
|
||||||
|
assert_eq!(m.params, params("b", "foo"));
|
||||||
|
|
||||||
|
let m = router.recognize("/a/foo/bar/c").unwrap();
|
||||||
|
assert_eq!(*m.handler, "abc".to_string());
|
||||||
|
assert_eq!(m.params, params("b", "foo/bar"));
|
||||||
|
|
||||||
|
let m = router.recognize("/a/foo/c/baz").unwrap();
|
||||||
|
assert_eq!(*m.handler, "abcd".to_string());
|
||||||
|
assert_eq!(m.params, two_params("b", "foo", "d", "baz"));
|
||||||
|
|
||||||
|
let m = router.recognize("/a/foo/bar/c/baz").unwrap();
|
||||||
|
assert_eq!(*m.handler, "abcd".to_string());
|
||||||
|
assert_eq!(m.params, two_params("b", "foo/bar", "d", "baz"));
|
||||||
|
|
||||||
|
let m = router.recognize("/a/foo/bar/c/baz/bay").unwrap();
|
||||||
|
assert_eq!(*m.handler, "ab".to_string());
|
||||||
|
assert_eq!(m.params, params("b", "foo/bar/c/baz/bay"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unnamed_parameters() {
|
||||||
|
let mut router = Router::new();
|
||||||
|
|
||||||
|
router.add("/foo/:/bar", "test".to_string());
|
||||||
|
router.add("/foo/:bar/*", "test2".to_string());
|
||||||
|
let m = router.recognize("/foo/test/bar").unwrap();
|
||||||
|
assert_eq!(*m.handler, "test");
|
||||||
|
assert_eq!(m.params, Params::default());
|
||||||
|
|
||||||
|
let m = router.recognize("/foo/test/blah").unwrap();
|
||||||
|
assert_eq!(*m.handler, "test2");
|
||||||
|
assert_eq!(m.params, params("bar", "test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn params(key: &str, val: &str) -> Params {
|
||||||
|
let mut params = Params::default();
|
||||||
|
params.0.push((key.to_string(), val.to_string()));
|
||||||
|
params
|
||||||
|
}
|
||||||
|
|
||||||
|
fn two_params(k1: &str, v1: &str, k2: &str, v2: &str) -> Params {
|
||||||
|
let mut params = Params::default();
|
||||||
|
params.0.push((k1.to_string(), v1.to_string()));
|
||||||
|
params.0.push((k2.to_string(), v2.to_string()));
|
||||||
|
params
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dot() {
|
||||||
|
let mut router = Router::new();
|
||||||
|
router.add("/1/baz.:wibble", ());
|
||||||
|
router.add("/2/:bar.baz", ());
|
||||||
|
router.add("/3/:dynamic.:extension", ());
|
||||||
|
router.add("/4/static.static", ());
|
||||||
|
|
||||||
|
let m = router.recognize("/1/baz.jpg").unwrap();
|
||||||
|
assert_eq!(m.params, params("wibble", "jpg"));
|
||||||
|
|
||||||
|
let m = router.recognize("/2/test.baz").unwrap();
|
||||||
|
assert_eq!(m.params, params("bar", "test"));
|
||||||
|
|
||||||
|
let m = router.recognize("/3/any.thing").unwrap();
|
||||||
|
assert_eq!(m.params, two_params("dynamic", "any", "extension", "thing"));
|
||||||
|
|
||||||
|
let m = router.recognize("/3/this.performs.a.greedy.match").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
m.params,
|
||||||
|
two_params("dynamic", "this.performs.a.greedy", "extension", "match")
|
||||||
|
);
|
||||||
|
|
||||||
|
let m = router.recognize("/4/static.static").unwrap();
|
||||||
|
assert_eq!(m.params, Params::default());
|
||||||
|
|
||||||
|
let m = router.recognize("/4/static/static");
|
||||||
|
assert!(m.is_err());
|
||||||
|
|
||||||
|
let m = router.recognize("/4.static.static");
|
||||||
|
assert!(m.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
606
src/route_recognizer/nfa.rs
Normal file
606
src/route_recognizer/nfa.rs
Normal file
@@ -0,0 +1,606 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use self::CharacterClass::{Ascii, InvalidChars, ValidChars};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Default, Debug)]
|
||||||
|
pub(crate) struct CharSet {
|
||||||
|
low_mask: u64,
|
||||||
|
high_mask: u64,
|
||||||
|
non_ascii: HashSet<char>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CharSet {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
low_mask: 0,
|
||||||
|
high_mask: 0,
|
||||||
|
non_ascii: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn insert(&mut self, char: char) {
|
||||||
|
let val = char as u32 - 1;
|
||||||
|
|
||||||
|
if val > 127 {
|
||||||
|
self.non_ascii.insert(char);
|
||||||
|
} else if val > 63 {
|
||||||
|
let bit = 1 << (val - 64);
|
||||||
|
self.high_mask |= bit;
|
||||||
|
} else {
|
||||||
|
let bit = 1 << val;
|
||||||
|
self.low_mask |= bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn contains(&self, char: char) -> bool {
|
||||||
|
let val = char as u32 - 1;
|
||||||
|
|
||||||
|
if val > 127 {
|
||||||
|
self.non_ascii.contains(&char)
|
||||||
|
} else if val > 63 {
|
||||||
|
let bit = 1 << (val - 64);
|
||||||
|
self.high_mask & bit != 0
|
||||||
|
} else {
|
||||||
|
let bit = 1 << val;
|
||||||
|
self.low_mask & bit != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
|
pub(crate) enum CharacterClass {
|
||||||
|
Ascii(u64, u64, bool),
|
||||||
|
ValidChars(CharSet),
|
||||||
|
InvalidChars(CharSet),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CharacterClass {
|
||||||
|
pub(crate) fn any() -> Self {
|
||||||
|
Ascii(u64::max_value(), u64::max_value(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn valid_char(char: char) -> Self {
|
||||||
|
let val = char as u32 - 1;
|
||||||
|
|
||||||
|
if val > 127 {
|
||||||
|
ValidChars(Self::char_to_set(char))
|
||||||
|
} else if val > 63 {
|
||||||
|
Ascii(1 << (val - 64), 0, false)
|
||||||
|
} else {
|
||||||
|
Ascii(0, 1 << val, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn valid(string: &str) -> Self {
|
||||||
|
ValidChars(Self::str_to_set(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn invalid(string: &str) -> Self {
|
||||||
|
InvalidChars(Self::str_to_set(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn invalid_char(char: char) -> Self {
|
||||||
|
let val = char as u32 - 1;
|
||||||
|
|
||||||
|
if val > 127 {
|
||||||
|
InvalidChars(Self::char_to_set(char))
|
||||||
|
} else if val > 63 {
|
||||||
|
Ascii(u64::max_value() ^ (1 << (val - 64)), u64::max_value(), true)
|
||||||
|
} else {
|
||||||
|
Ascii(u64::max_value(), u64::max_value() ^ (1 << val), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn matches(&self, char: char) -> bool {
|
||||||
|
match *self {
|
||||||
|
ValidChars(ref valid) => valid.contains(char),
|
||||||
|
InvalidChars(ref invalid) => !invalid.contains(char),
|
||||||
|
Ascii(high, low, unicode) => {
|
||||||
|
let val = char as u32 - 1;
|
||||||
|
if val > 127 {
|
||||||
|
unicode
|
||||||
|
} else if val > 63 {
|
||||||
|
high & (1 << (val - 64)) != 0
|
||||||
|
} else {
|
||||||
|
low & (1 << val) != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn char_to_set(char: char) -> CharSet {
|
||||||
|
let mut set = CharSet::new();
|
||||||
|
set.insert(char);
|
||||||
|
set
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn str_to_set(string: &str) -> CharSet {
|
||||||
|
let mut set = CharSet::new();
|
||||||
|
for char in string.chars() {
|
||||||
|
set.insert(char);
|
||||||
|
}
|
||||||
|
set
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Thread {
|
||||||
|
state: usize,
|
||||||
|
captures: Vec<(usize, usize)>,
|
||||||
|
capture_begin: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Thread {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
state: 0,
|
||||||
|
captures: Vec::new(),
|
||||||
|
capture_begin: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn start_capture(&mut self, start: usize) {
|
||||||
|
self.capture_begin = Some(start);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn end_capture(&mut self, end: usize) {
|
||||||
|
self.captures.push((self.capture_begin.unwrap(), end));
|
||||||
|
self.capture_begin = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn extract<'a>(&self, source: &'a str) -> Vec<&'a str> {
|
||||||
|
self.captures
|
||||||
|
.iter()
|
||||||
|
.map(|&(begin, end)| &source[begin..end])
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct State<T> {
|
||||||
|
pub(crate) index: usize,
|
||||||
|
pub(crate) chars: CharacterClass,
|
||||||
|
pub(crate) next_states: Vec<usize>,
|
||||||
|
pub(crate) acceptance: bool,
|
||||||
|
pub(crate) start_capture: bool,
|
||||||
|
pub(crate) end_capture: bool,
|
||||||
|
pub(crate) metadata: Option<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialEq for State<T> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.index == other.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> State<T> {
|
||||||
|
pub(crate) fn new(index: usize, chars: CharacterClass) -> Self {
|
||||||
|
Self {
|
||||||
|
index,
|
||||||
|
chars,
|
||||||
|
next_states: Vec::new(),
|
||||||
|
acceptance: false,
|
||||||
|
start_capture: false,
|
||||||
|
end_capture: false,
|
||||||
|
metadata: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct Match<'a> {
|
||||||
|
pub(crate) state: usize,
|
||||||
|
pub(crate) captures: Vec<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Match<'a> {
|
||||||
|
pub(crate) fn new(state: usize, captures: Vec<&'_ str>) -> Match<'_> {
|
||||||
|
Match { state, captures }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
|
#[derive(Clone, Default, Debug)]
|
||||||
|
pub(crate) struct NFA<T> {
|
||||||
|
states: Vec<State<T>>,
|
||||||
|
start_capture: Vec<bool>,
|
||||||
|
end_capture: Vec<bool>,
|
||||||
|
acceptance: Vec<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> NFA<T> {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
let root = State::new(0, CharacterClass::any());
|
||||||
|
Self {
|
||||||
|
states: vec![root],
|
||||||
|
start_capture: vec![false],
|
||||||
|
end_capture: vec![false],
|
||||||
|
acceptance: vec![false],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn process<'a, I, F>(&self, string: &'a str, mut ord: F) -> Result<Match<'a>, String>
|
||||||
|
where
|
||||||
|
I: Ord,
|
||||||
|
F: FnMut(usize) -> I,
|
||||||
|
{
|
||||||
|
let mut threads = vec![Thread::new()];
|
||||||
|
|
||||||
|
for (i, char) in string.chars().enumerate() {
|
||||||
|
let next_threads = self.process_char(threads, char, i);
|
||||||
|
|
||||||
|
if next_threads.is_empty() {
|
||||||
|
return Err(format!("Couldn't process {}", string));
|
||||||
|
}
|
||||||
|
|
||||||
|
threads = next_threads;
|
||||||
|
}
|
||||||
|
|
||||||
|
let returned = threads
|
||||||
|
.into_iter()
|
||||||
|
.filter(|thread| self.get(thread.state).acceptance);
|
||||||
|
|
||||||
|
let thread = returned
|
||||||
|
.fold(None, |prev, y| {
|
||||||
|
let y_v = ord(y.state);
|
||||||
|
match prev {
|
||||||
|
None => Some((y_v, y)),
|
||||||
|
Some((x_v, x)) => {
|
||||||
|
if x_v < y_v {
|
||||||
|
Some((y_v, y))
|
||||||
|
} else {
|
||||||
|
Some((x_v, x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|p| p.1);
|
||||||
|
|
||||||
|
match thread {
|
||||||
|
None => Err("The string was exhausted before reaching an \
|
||||||
|
acceptance state"
|
||||||
|
.to_string()),
|
||||||
|
Some(mut thread) => {
|
||||||
|
if thread.capture_begin.is_some() {
|
||||||
|
thread.end_capture(string.len());
|
||||||
|
}
|
||||||
|
let state = self.get(thread.state);
|
||||||
|
Ok(Match::new(state.index, thread.extract(string)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn process_char(&self, threads: Vec<Thread>, char: char, pos: usize) -> Vec<Thread> {
|
||||||
|
let mut returned = Vec::with_capacity(threads.len());
|
||||||
|
|
||||||
|
for mut thread in threads {
|
||||||
|
let current_state = self.get(thread.state);
|
||||||
|
|
||||||
|
let mut count = 0;
|
||||||
|
let mut found_state = 0;
|
||||||
|
|
||||||
|
for &index in ¤t_state.next_states {
|
||||||
|
let state = &self.states[index];
|
||||||
|
|
||||||
|
if state.chars.matches(char) {
|
||||||
|
count += 1;
|
||||||
|
found_state = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 1 {
|
||||||
|
thread.state = found_state;
|
||||||
|
capture(self, &mut thread, current_state.index, found_state, pos);
|
||||||
|
returned.push(thread);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for &index in ¤t_state.next_states {
|
||||||
|
let state = &self.states[index];
|
||||||
|
if state.chars.matches(char) {
|
||||||
|
let mut thread = fork_thread(&thread, state);
|
||||||
|
capture(self, &mut thread, current_state.index, index, pos);
|
||||||
|
returned.push(thread);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
returned
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn get(&self, state: usize) -> &State<T> {
|
||||||
|
&self.states[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_mut(&mut self, state: usize) -> &mut State<T> {
|
||||||
|
&mut self.states[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn put(&mut self, index: usize, chars: CharacterClass) -> usize {
|
||||||
|
{
|
||||||
|
let state = self.get(index);
|
||||||
|
|
||||||
|
for &index in &state.next_states {
|
||||||
|
let state = self.get(index);
|
||||||
|
if state.chars == chars {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = self.new_state(chars);
|
||||||
|
self.get_mut(index).next_states.push(state);
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn put_state(&mut self, index: usize, child: usize) {
|
||||||
|
if !self.states[index].next_states.contains(&child) {
|
||||||
|
self.get_mut(index).next_states.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn acceptance(&mut self, index: usize) {
|
||||||
|
self.get_mut(index).acceptance = true;
|
||||||
|
self.acceptance[index] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn start_capture(&mut self, index: usize) {
|
||||||
|
self.get_mut(index).start_capture = true;
|
||||||
|
self.start_capture[index] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn end_capture(&mut self, index: usize) {
|
||||||
|
self.get_mut(index).end_capture = true;
|
||||||
|
self.end_capture[index] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn metadata(&mut self, index: usize, metadata: T) {
|
||||||
|
self.get_mut(index).metadata = Some(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_state(&mut self, chars: CharacterClass) -> usize {
|
||||||
|
let index = self.states.len();
|
||||||
|
let state = State::new(index, chars);
|
||||||
|
self.states.push(state);
|
||||||
|
|
||||||
|
self.acceptance.push(false);
|
||||||
|
self.start_capture.push(false);
|
||||||
|
self.end_capture.push(false);
|
||||||
|
|
||||||
|
index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn fork_thread<T>(thread: &Thread, state: &State<T>) -> Thread {
|
||||||
|
let mut new_trace = thread.clone();
|
||||||
|
new_trace.state = state.index;
|
||||||
|
new_trace
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn capture<T>(
|
||||||
|
nfa: &NFA<T>,
|
||||||
|
thread: &mut Thread,
|
||||||
|
current_state: usize,
|
||||||
|
next_state: usize,
|
||||||
|
pos: usize,
|
||||||
|
) {
|
||||||
|
if thread.capture_begin == None && nfa.start_capture[next_state] {
|
||||||
|
thread.start_capture(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if thread.capture_begin != None && nfa.end_capture[current_state] && next_state > current_state
|
||||||
|
{
|
||||||
|
thread.end_capture(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::many_single_char_names)]
|
||||||
|
mod tests {
|
||||||
|
use super::{CharSet, CharacterClass, NFA};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_test() {
|
||||||
|
let mut nfa = NFA::<()>::new();
|
||||||
|
let a = nfa.put(0, CharacterClass::valid("h"));
|
||||||
|
let b = nfa.put(a, CharacterClass::valid("e"));
|
||||||
|
let c = nfa.put(b, CharacterClass::valid("l"));
|
||||||
|
let d = nfa.put(c, CharacterClass::valid("l"));
|
||||||
|
let e = nfa.put(d, CharacterClass::valid("o"));
|
||||||
|
nfa.acceptance(e);
|
||||||
|
|
||||||
|
let m = nfa.process("hello", |a| a);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
m.unwrap().state == e,
|
||||||
|
"You didn't get the right final state"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_solutions() {
|
||||||
|
let mut nfa = NFA::<()>::new();
|
||||||
|
let a1 = nfa.put(0, CharacterClass::valid("n"));
|
||||||
|
let b1 = nfa.put(a1, CharacterClass::valid("e"));
|
||||||
|
let c1 = nfa.put(b1, CharacterClass::valid("w"));
|
||||||
|
nfa.acceptance(c1);
|
||||||
|
|
||||||
|
let a2 = nfa.put(0, CharacterClass::invalid(""));
|
||||||
|
let b2 = nfa.put(a2, CharacterClass::invalid(""));
|
||||||
|
let c2 = nfa.put(b2, CharacterClass::invalid(""));
|
||||||
|
nfa.acceptance(c2);
|
||||||
|
|
||||||
|
let m = nfa.process("new", |a| a);
|
||||||
|
|
||||||
|
assert!(m.unwrap().state == c2, "The two states were not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_paths() {
|
||||||
|
let mut nfa = NFA::<()>::new();
|
||||||
|
let a = nfa.put(0, CharacterClass::valid("t")); // t
|
||||||
|
let b1 = nfa.put(a, CharacterClass::valid("h")); // th
|
||||||
|
let c1 = nfa.put(b1, CharacterClass::valid("o")); // tho
|
||||||
|
let d1 = nfa.put(c1, CharacterClass::valid("m")); // thom
|
||||||
|
let e1 = nfa.put(d1, CharacterClass::valid("a")); // thoma
|
||||||
|
let f1 = nfa.put(e1, CharacterClass::valid("s")); // thomas
|
||||||
|
|
||||||
|
let b2 = nfa.put(a, CharacterClass::valid("o")); // to
|
||||||
|
let c2 = nfa.put(b2, CharacterClass::valid("m")); // tom
|
||||||
|
|
||||||
|
nfa.acceptance(f1);
|
||||||
|
nfa.acceptance(c2);
|
||||||
|
|
||||||
|
let thomas = nfa.process("thomas", |a| a);
|
||||||
|
let tom = nfa.process("tom", |a| a);
|
||||||
|
let thom = nfa.process("thom", |a| a);
|
||||||
|
let nope = nfa.process("nope", |a| a);
|
||||||
|
|
||||||
|
assert!(thomas.unwrap().state == f1, "thomas was parsed correctly");
|
||||||
|
assert!(tom.unwrap().state == c2, "tom was parsed correctly");
|
||||||
|
assert!(thom.is_err(), "thom didn't reach an acceptance state");
|
||||||
|
assert!(nope.is_err(), "nope wasn't parsed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repetitions() {
|
||||||
|
let mut nfa = NFA::<()>::new();
|
||||||
|
let a = nfa.put(0, CharacterClass::valid("p")); // p
|
||||||
|
let b = nfa.put(a, CharacterClass::valid("o")); // po
|
||||||
|
let c = nfa.put(b, CharacterClass::valid("s")); // pos
|
||||||
|
let d = nfa.put(c, CharacterClass::valid("t")); // post
|
||||||
|
let e = nfa.put(d, CharacterClass::valid("s")); // posts
|
||||||
|
let f = nfa.put(e, CharacterClass::valid("/")); // posts/
|
||||||
|
let g = nfa.put(f, CharacterClass::invalid("/")); // posts/[^/]
|
||||||
|
nfa.put_state(g, g);
|
||||||
|
|
||||||
|
nfa.acceptance(g);
|
||||||
|
|
||||||
|
let post = nfa.process("posts/1", |a| a);
|
||||||
|
let new_post = nfa.process("posts/new", |a| a);
|
||||||
|
let invalid = nfa.process("posts/", |a| a);
|
||||||
|
|
||||||
|
assert!(post.unwrap().state == g, "posts/1 was parsed");
|
||||||
|
assert!(new_post.unwrap().state == g, "posts/new was parsed");
|
||||||
|
assert!(invalid.is_err(), "posts/ was invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repetitions_with_ambiguous() {
|
||||||
|
let mut nfa = NFA::<()>::new();
|
||||||
|
let a = nfa.put(0, CharacterClass::valid("p")); // p
|
||||||
|
let b = nfa.put(a, CharacterClass::valid("o")); // po
|
||||||
|
let c = nfa.put(b, CharacterClass::valid("s")); // pos
|
||||||
|
let d = nfa.put(c, CharacterClass::valid("t")); // post
|
||||||
|
let e = nfa.put(d, CharacterClass::valid("s")); // posts
|
||||||
|
let f = nfa.put(e, CharacterClass::valid("/")); // posts/
|
||||||
|
let g1 = nfa.put(f, CharacterClass::invalid("/")); // posts/[^/]
|
||||||
|
let g2 = nfa.put(f, CharacterClass::valid("n")); // posts/n
|
||||||
|
let h2 = nfa.put(g2, CharacterClass::valid("e")); // posts/ne
|
||||||
|
let i2 = nfa.put(h2, CharacterClass::valid("w")); // posts/new
|
||||||
|
|
||||||
|
nfa.put_state(g1, g1);
|
||||||
|
|
||||||
|
nfa.acceptance(g1);
|
||||||
|
nfa.acceptance(i2);
|
||||||
|
|
||||||
|
let post = nfa.process("posts/1", |a| a);
|
||||||
|
let ambiguous = nfa.process("posts/new", |a| a);
|
||||||
|
let invalid = nfa.process("posts/", |a| a);
|
||||||
|
|
||||||
|
assert!(post.unwrap().state == g1, "posts/1 was parsed");
|
||||||
|
assert!(ambiguous.unwrap().state == i2, "posts/new was ambiguous");
|
||||||
|
assert!(invalid.is_err(), "posts/ was invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn captures() {
|
||||||
|
let mut nfa = NFA::<()>::new();
|
||||||
|
let a = nfa.put(0, CharacterClass::valid("n"));
|
||||||
|
let b = nfa.put(a, CharacterClass::valid("e"));
|
||||||
|
let c = nfa.put(b, CharacterClass::valid("w"));
|
||||||
|
|
||||||
|
nfa.acceptance(c);
|
||||||
|
nfa.start_capture(a);
|
||||||
|
nfa.end_capture(c);
|
||||||
|
|
||||||
|
let post = nfa.process("new", |a| a);
|
||||||
|
|
||||||
|
assert_eq!(post.unwrap().captures, vec!["new"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capture_mid_match() {
|
||||||
|
let mut nfa = NFA::<()>::new();
|
||||||
|
let a = nfa.put(0, valid('p'));
|
||||||
|
let b = nfa.put(a, valid('/'));
|
||||||
|
let c = nfa.put(b, invalid('/'));
|
||||||
|
let d = nfa.put(c, valid('/'));
|
||||||
|
let e = nfa.put(d, valid('c'));
|
||||||
|
|
||||||
|
nfa.put_state(c, c);
|
||||||
|
nfa.acceptance(e);
|
||||||
|
nfa.start_capture(c);
|
||||||
|
nfa.end_capture(c);
|
||||||
|
|
||||||
|
let post = nfa.process("p/123/c", |a| a);
|
||||||
|
|
||||||
|
assert_eq!(post.unwrap().captures, vec!["123"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn capture_multiple_captures() {
|
||||||
|
let mut nfa = NFA::<()>::new();
|
||||||
|
let a = nfa.put(0, valid('p'));
|
||||||
|
let b = nfa.put(a, valid('/'));
|
||||||
|
let c = nfa.put(b, invalid('/'));
|
||||||
|
let d = nfa.put(c, valid('/'));
|
||||||
|
let e = nfa.put(d, valid('c'));
|
||||||
|
let f = nfa.put(e, valid('/'));
|
||||||
|
let g = nfa.put(f, invalid('/'));
|
||||||
|
|
||||||
|
nfa.put_state(c, c);
|
||||||
|
nfa.put_state(g, g);
|
||||||
|
nfa.acceptance(g);
|
||||||
|
|
||||||
|
nfa.start_capture(c);
|
||||||
|
nfa.end_capture(c);
|
||||||
|
|
||||||
|
nfa.start_capture(g);
|
||||||
|
nfa.end_capture(g);
|
||||||
|
|
||||||
|
let post = nfa.process("p/123/c/456", |a| a);
|
||||||
|
assert_eq!(post.unwrap().captures, vec!["123", "456"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ascii_set() {
|
||||||
|
let mut set = CharSet::new();
|
||||||
|
set.insert('?');
|
||||||
|
set.insert('a');
|
||||||
|
set.insert('é');
|
||||||
|
|
||||||
|
assert!(set.contains('?'), "The set contains char 63");
|
||||||
|
assert!(set.contains('a'), "The set contains char 97");
|
||||||
|
assert!(set.contains('é'), "The set contains char 233");
|
||||||
|
assert!(!set.contains('q'), "The set does not contain q");
|
||||||
|
assert!(!set.contains('ü'), "The set does not contain ü");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid(char: char) -> CharacterClass {
|
||||||
|
CharacterClass::valid_char(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid(char: char) -> CharacterClass {
|
||||||
|
CharacterClass::invalid_char(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/server.rs
Normal file
42
src/server.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
use std::convert::Infallible;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{Endpoint, Request};
|
||||||
|
|
||||||
|
pub struct Server {
|
||||||
|
ep: Arc<dyn Endpoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Server {
|
||||||
|
pub fn new(root: impl Endpoint) -> Self {
|
||||||
|
Server { ep: Arc::new(root) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve(self, addr: &SocketAddr) -> Result<(), hyper::Error> {
|
||||||
|
let service = hyper::service::make_service_fn(move |_| {
|
||||||
|
let ep = self.ep.clone();
|
||||||
|
async move {
|
||||||
|
Ok::<_, Infallible>(hyper::service::service_fn({
|
||||||
|
move |req: hyper::Request<hyper::Body>| {
|
||||||
|
let ep = ep.clone();
|
||||||
|
async move {
|
||||||
|
let req = match Request::from_hyper(req) {
|
||||||
|
Ok(req) => req,
|
||||||
|
Err(err) => return Ok(err.as_response().into_hyper()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resp = match ep.call(req).await {
|
||||||
|
Ok(resp) => resp.into_hyper(),
|
||||||
|
Err(err) => err.as_response().into_hyper(),
|
||||||
|
};
|
||||||
|
Ok::<_, Infallible>(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
hyper::Server::bind(addr).serve(service).await
|
||||||
|
}
|
||||||
|
}
|
||||||
240
src/status_code.rs
Normal file
240
src/status_code.rs
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
|
use crate::error::{Error, ErrorInvalidStatusCode};
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub struct StatusCode(pub(crate) http::StatusCode);
|
||||||
|
|
||||||
|
impl TryFrom<u16> for StatusCode {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self(http::StatusCode::try_from(value).map_err(|_| {
|
||||||
|
Error::internal_server_error(ErrorInvalidStatusCode)
|
||||||
|
})?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for StatusCode {
|
||||||
|
#[inline]
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! define_status_codes {
|
||||||
|
($($(#[$docs:meta])* $name:ident;)*) => {
|
||||||
|
$(
|
||||||
|
$(#[$docs])*
|
||||||
|
pub const $name: StatusCode = StatusCode(http::StatusCode::$name);
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusCode {
|
||||||
|
#[inline]
|
||||||
|
pub fn as_u16(&self) -> u16 {
|
||||||
|
self.0.as_u16()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_server_error(&self) -> bool {
|
||||||
|
self.0.is_server_error()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_client_error(&self) -> bool {
|
||||||
|
self.0.is_client_error()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_success(&self) -> bool {
|
||||||
|
self.0.is_success()
|
||||||
|
}
|
||||||
|
|
||||||
|
define_status_codes!(
|
||||||
|
/// 100 Continue
|
||||||
|
/// [[RFC7231, Section 6.2.1](https://tools.ietf.org/html/rfc7231#section-6.2.1)]
|
||||||
|
CONTINUE;
|
||||||
|
/// 101 Switching Protocols
|
||||||
|
/// [[RFC7231, Section 6.2.2](https://tools.ietf.org/html/rfc7231#section-6.2.2)]
|
||||||
|
SWITCHING_PROTOCOLS;
|
||||||
|
/// 102 Processing
|
||||||
|
/// [[RFC2518](https://tools.ietf.org/html/rfc2518)]
|
||||||
|
PROCESSING;
|
||||||
|
|
||||||
|
/// 200 OK
|
||||||
|
/// [[RFC7231, Section 6.3.1](https://tools.ietf.org/html/rfc7231#section-6.3.1)]
|
||||||
|
OK;
|
||||||
|
/// 201 Created
|
||||||
|
/// [[RFC7231, Section 6.3.2](https://tools.ietf.org/html/rfc7231#section-6.3.2)]
|
||||||
|
CREATED;
|
||||||
|
/// 202 Accepted
|
||||||
|
/// [[RFC7231, Section 6.3.3](https://tools.ietf.org/html/rfc7231#section-6.3.3)]
|
||||||
|
ACCEPTED;
|
||||||
|
/// 203 Non-Authoritative Information
|
||||||
|
/// [[RFC7231, Section 6.3.4](https://tools.ietf.org/html/rfc7231#section-6.3.4)]
|
||||||
|
NON_AUTHORITATIVE_INFORMATION;
|
||||||
|
/// 204 No Content
|
||||||
|
/// [[RFC7231, Section 6.3.5](https://tools.ietf.org/html/rfc7231#section-6.3.5)]
|
||||||
|
NO_CONTENT;
|
||||||
|
/// 205 Reset Content
|
||||||
|
/// [[RFC7231, Section 6.3.6](https://tools.ietf.org/html/rfc7231#section-6.3.6)]
|
||||||
|
RESET_CONTENT;
|
||||||
|
/// 206 Partial Content
|
||||||
|
/// [[RFC7233, Section 4.1](https://tools.ietf.org/html/rfc7233#section-4.1)]
|
||||||
|
PARTIAL_CONTENT;
|
||||||
|
/// 207 Multi-Status
|
||||||
|
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
|
||||||
|
MULTI_STATUS;
|
||||||
|
/// 208 Already Reported
|
||||||
|
/// [[RFC5842](https://tools.ietf.org/html/rfc5842)]
|
||||||
|
ALREADY_REPORTED;
|
||||||
|
/// 226 IM Used
|
||||||
|
/// [[RFC3229](https://tools.ietf.org/html/rfc3229)]
|
||||||
|
IM_USED;
|
||||||
|
/// 300 Multiple Choices
|
||||||
|
/// [[RFC7231, Section 6.4.1](https://tools.ietf.org/html/rfc7231#section-6.4.1)]
|
||||||
|
MULTIPLE_CHOICES;
|
||||||
|
/// 301 Moved Permanently
|
||||||
|
/// [[RFC7231, Section 6.4.2](https://tools.ietf.org/html/rfc7231#section-6.4.2)]
|
||||||
|
MOVED_PERMANENTLY;
|
||||||
|
/// [[RFC7231, Section 6.4.3](https://tools.ietf.org/html/rfc7231#section-6.4.3)]
|
||||||
|
FOUND;
|
||||||
|
/// 303 See Other
|
||||||
|
/// [[RFC7231, Section 6.4.4](https://tools.ietf.org/html/rfc7231#section-6.4.4)]
|
||||||
|
SEE_OTHER;
|
||||||
|
/// 304 Not Modified
|
||||||
|
/// [[RFC7232, Section 4.1](https://tools.ietf.org/html/rfc7232#section-4.1)]
|
||||||
|
NOT_MODIFIED;
|
||||||
|
/// 305 Use Proxy
|
||||||
|
/// [[RFC7231, Section 6.4.5](https://tools.ietf.org/html/rfc7231#section-6.4.5)]
|
||||||
|
USE_PROXY;
|
||||||
|
/// 307 Temporary Redirect
|
||||||
|
/// [[RFC7231, Section 6.4.7](https://tools.ietf.org/html/rfc7231#section-6.4.7)]
|
||||||
|
TEMPORARY_REDIRECT;
|
||||||
|
/// 308 Permanent Redirect
|
||||||
|
/// [[RFC7238](https://tools.ietf.org/html/rfc7238)]
|
||||||
|
PERMANENT_REDIRECT;
|
||||||
|
|
||||||
|
/// 400 Bad Request
|
||||||
|
/// [[RFC7231, Section 6.5.1](https://tools.ietf.org/html/rfc7231#section-6.5.1)]
|
||||||
|
BAD_REQUEST;
|
||||||
|
/// 401 Unauthorized
|
||||||
|
/// [[RFC7235, Section 3.1](https://tools.ietf.org/html/rfc7235#section-3.1)]
|
||||||
|
UNAUTHORIZED;
|
||||||
|
/// 402 Payment Required
|
||||||
|
/// [[RFC7231, Section 6.5.2](https://tools.ietf.org/html/rfc7231#section-6.5.2)]
|
||||||
|
PAYMENT_REQUIRED;
|
||||||
|
/// 403 Forbidden
|
||||||
|
/// [[RFC7231, Section 6.5.3](https://tools.ietf.org/html/rfc7231#section-6.5.3)]
|
||||||
|
FORBIDDEN;
|
||||||
|
/// 404 Not Found
|
||||||
|
/// [[RFC7231, Section 6.5.4](https://tools.ietf.org/html/rfc7231#section-6.5.4)]
|
||||||
|
NOT_FOUND;
|
||||||
|
/// 405 Method Not Allowed
|
||||||
|
/// [[RFC7231, Section 6.5.5](https://tools.ietf.org/html/rfc7231#section-6.5.5)]
|
||||||
|
METHOD_NOT_ALLOWED;
|
||||||
|
/// 406 Not Acceptable
|
||||||
|
/// [[RFC7231, Section 6.5.6](https://tools.ietf.org/html/rfc7231#section-6.5.6)]
|
||||||
|
NOT_ACCEPTABLE;
|
||||||
|
/// 407 Proxy Authentication Required
|
||||||
|
/// [[RFC7235, Section 3.2](https://tools.ietf.org/html/rfc7235#section-3.2)]
|
||||||
|
PROXY_AUTHENTICATION_REQUIRED;
|
||||||
|
/// 408 Request Timeout
|
||||||
|
/// [[RFC7231, Section 6.5.7](https://tools.ietf.org/html/rfc7231#section-6.5.7)]
|
||||||
|
REQUEST_TIMEOUT;
|
||||||
|
/// 409 Conflict
|
||||||
|
/// [[RFC7231, Section 6.5.8](https://tools.ietf.org/html/rfc7231#section-6.5.8)]
|
||||||
|
CONFLICT;
|
||||||
|
/// 410 Gone
|
||||||
|
/// [[RFC7231, Section 6.5.9](https://tools.ietf.org/html/rfc7231#section-6.5.9)]
|
||||||
|
GONE;
|
||||||
|
/// 411 Length Required
|
||||||
|
/// [[RFC7231, Section 6.5.10](https://tools.ietf.org/html/rfc7231#section-6.5.10)]
|
||||||
|
LENGTH_REQUIRED;
|
||||||
|
/// 412 Precondition Failed
|
||||||
|
/// [[RFC7232, Section 4.2](https://tools.ietf.org/html/rfc7232#section-4.2)]
|
||||||
|
PRECONDITION_FAILED;
|
||||||
|
/// 413 Payload Too Large
|
||||||
|
/// [[RFC7231, Section 6.5.11](https://tools.ietf.org/html/rfc7231#section-6.5.11)]
|
||||||
|
PAYLOAD_TOO_LARGE;
|
||||||
|
/// 414 URI Too Long
|
||||||
|
/// [[RFC7231, Section 6.5.12](https://tools.ietf.org/html/rfc7231#section-6.5.12)]
|
||||||
|
URI_TOO_LONG;
|
||||||
|
/// 415 Unsupported Media Type
|
||||||
|
/// [[RFC7231, Section 6.5.13](https://tools.ietf.org/html/rfc7231#section-6.5.13)]
|
||||||
|
UNSUPPORTED_MEDIA_TYPE;
|
||||||
|
/// 416 Range Not Satisfiable
|
||||||
|
/// [[RFC7233, Section 4.4](https://tools.ietf.org/html/rfc7233#section-4.4)]
|
||||||
|
RANGE_NOT_SATISFIABLE;
|
||||||
|
/// 417 Expectation Failed
|
||||||
|
/// [[RFC7231, Section 6.5.14](https://tools.ietf.org/html/rfc7231#section-6.5.14)]
|
||||||
|
EXPECTATION_FAILED;
|
||||||
|
/// 418 I'm a teapot
|
||||||
|
/// [curiously not registered by IANA but [RFC2324](https://tools.ietf.org/html/rfc2324)]
|
||||||
|
IM_A_TEAPOT;
|
||||||
|
|
||||||
|
/// 421 Misdirected Request
|
||||||
|
/// [RFC7540, Section 9.1.2](http://tools.ietf.org/html/rfc7540#section-9.1.2)
|
||||||
|
MISDIRECTED_REQUEST;
|
||||||
|
/// 422 Unprocessable Entity
|
||||||
|
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
|
||||||
|
UNPROCESSABLE_ENTITY;
|
||||||
|
/// 423 Locked
|
||||||
|
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
|
||||||
|
LOCKED;
|
||||||
|
/// 424 Failed Dependency
|
||||||
|
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
|
||||||
|
FAILED_DEPENDENCY;
|
||||||
|
/// 426 Upgrade Required
|
||||||
|
/// [[RFC7231, Section 6.5.15](https://tools.ietf.org/html/rfc7231#section-6.5.15)]
|
||||||
|
UPGRADE_REQUIRED;
|
||||||
|
/// 428 Precondition Required
|
||||||
|
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
|
||||||
|
PRECONDITION_REQUIRED;
|
||||||
|
/// 429 Too Many Requests
|
||||||
|
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
|
||||||
|
TOO_MANY_REQUESTS;
|
||||||
|
/// 431 Request Header Fields Too Large
|
||||||
|
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
|
||||||
|
REQUEST_HEADER_FIELDS_TOO_LARGE;
|
||||||
|
/// 451 Unavailable For Legal Reasons
|
||||||
|
/// [[RFC7725](http://tools.ietf.org/html/rfc7725)]
|
||||||
|
UNAVAILABLE_FOR_LEGAL_REASONS;
|
||||||
|
|
||||||
|
/// 500 Internal Server Error
|
||||||
|
/// [[RFC7231, Section 6.6.1](https://tools.ietf.org/html/rfc7231#section-6.6.1)]
|
||||||
|
INTERNAL_SERVER_ERROR;
|
||||||
|
/// 501 Not Implemented
|
||||||
|
/// [[RFC7231, Section 6.6.2](https://tools.ietf.org/html/rfc7231#section-6.6.2)]
|
||||||
|
NOT_IMPLEMENTED;
|
||||||
|
/// 502 Bad Gateway
|
||||||
|
/// [[RFC7231, Section 6.6.3](https://tools.ietf.org/html/rfc7231#section-6.6.3)]
|
||||||
|
BAD_GATEWAY;
|
||||||
|
/// 503 Service Unavailable
|
||||||
|
/// [[RFC7231, Section 6.6.4](https://tools.ietf.org/html/rfc7231#section-6.6.4)]
|
||||||
|
SERVICE_UNAVAILABLE;
|
||||||
|
/// 504 Gateway Timeout
|
||||||
|
/// [[RFC7231, Section 6.6.5](https://tools.ietf.org/html/rfc7231#section-6.6.5)]
|
||||||
|
GATEWAY_TIMEOUT;
|
||||||
|
/// 505 HTTP Version Not Supported
|
||||||
|
/// [[RFC7231, Section 6.6.6](https://tools.ietf.org/html/rfc7231#section-6.6.6)]
|
||||||
|
HTTP_VERSION_NOT_SUPPORTED;
|
||||||
|
/// 506 Variant Also Negotiates
|
||||||
|
/// [[RFC2295](https://tools.ietf.org/html/rfc2295)]
|
||||||
|
VARIANT_ALSO_NEGOTIATES;
|
||||||
|
/// 507 Insufficient Storage
|
||||||
|
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
|
||||||
|
INSUFFICIENT_STORAGE;
|
||||||
|
/// 508 Loop Detected
|
||||||
|
/// [[RFC5842](https://tools.ietf.org/html/rfc5842)]
|
||||||
|
LOOP_DETECTED;
|
||||||
|
/// 510 Not Extended
|
||||||
|
/// [[RFC2774](https://tools.ietf.org/html/rfc2774)]
|
||||||
|
NOT_EXTENDED;
|
||||||
|
/// 511 Network Authentication Required
|
||||||
|
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
|
||||||
|
NETWORK_AUTHENTICATION_REQUIRED;
|
||||||
|
);
|
||||||
|
}
|
||||||
55
src/uri/authority.rs
Normal file
55
src/uri/authority.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::error::{Error, ErrorInvalidUri};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||||
|
pub struct Authority(pub(crate) http::uri::Authority);
|
||||||
|
|
||||||
|
impl Authority {
|
||||||
|
/// Attempt to convert an [Authority] from a static string.
|
||||||
|
///
|
||||||
|
/// This function will not perform any copying, and the string will be checked if it is empty or contains an invalid character.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if the argument contains invalid characters or is empty.
|
||||||
|
#[inline]
|
||||||
|
pub fn from_static(src: &'static str) -> Self {
|
||||||
|
Authority(http::uri::Authority::from_static(src))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a str representation of the authority.
|
||||||
|
#[inline]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the host of this [Authority].
|
||||||
|
#[inline]
|
||||||
|
pub fn host(&self) -> &str {
|
||||||
|
self.0.host()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the port of this [Authority].
|
||||||
|
#[inline]
|
||||||
|
pub fn port(&self) -> Option<u16> {
|
||||||
|
self.0.port_u16()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Authority {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Authority(s.parse().map_err(|_| {
|
||||||
|
Error::internal_server_error(ErrorInvalidUri)
|
||||||
|
})?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Authority {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/uri/mod.rs
Normal file
99
src/uri/mod.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
mod authority;
|
||||||
|
mod parts;
|
||||||
|
mod path_and_query;
|
||||||
|
mod scheme;
|
||||||
|
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
pub use authority::Authority;
|
||||||
|
pub use parts::Parts;
|
||||||
|
pub use path_and_query::PathAndQuery;
|
||||||
|
pub use scheme::Scheme;
|
||||||
|
|
||||||
|
use crate::error::{Error, ErrorInvalidUri};
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
/// The URI component of a request.
|
||||||
|
#[derive(Debug, Clone, Hash, Default)]
|
||||||
|
pub struct Uri(pub(crate) http::Uri);
|
||||||
|
|
||||||
|
impl Display for Uri {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Uri {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self(s.parse::<http::Uri>().map_err(|_| {
|
||||||
|
Error::internal_server_error(ErrorInvalidUri)
|
||||||
|
})?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Uri {
|
||||||
|
/// Attempt to convert a `Uri` from `Parts`
|
||||||
|
pub fn from_parts(parts: Parts) -> Result<Self> {
|
||||||
|
let mut builder = http::uri::Builder::new();
|
||||||
|
|
||||||
|
if let Some(scheme) = parts.scheme {
|
||||||
|
builder = builder.scheme(scheme.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(authority) = parts.authority {
|
||||||
|
builder = builder.authority(authority.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(path_and_query) = parts.path_and_query {
|
||||||
|
builder = builder.path_and_query(path_and_query.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.build()
|
||||||
|
.map_err(|_| Error::internal_server_error(ErrorInvalidUri))
|
||||||
|
.map(Self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a [Uri] into [Parts].
|
||||||
|
pub fn into_parts(self) -> Parts {
|
||||||
|
let parts = self.0.into_parts();
|
||||||
|
Parts {
|
||||||
|
scheme: parts.scheme.map(Scheme),
|
||||||
|
authority: parts.authority.map(Authority),
|
||||||
|
path_and_query: parts.path_and_query.map(PathAndQuery),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the scheme of this [Uri].
|
||||||
|
pub fn schema(&self) -> Option<Scheme> {
|
||||||
|
self.0.scheme().cloned().map(Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the scheme of this [Uri] as a `&str`.
|
||||||
|
pub fn schema_str(&self) -> Option<&str> {
|
||||||
|
self.0.scheme_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the host of this [Uri].
|
||||||
|
pub fn host(&self) -> Option<&str> {
|
||||||
|
self.0.host()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the path of this [Uri].
|
||||||
|
pub fn path(&self) -> &str {
|
||||||
|
self.0.path()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the query string of this [Uri], starting after the `?`.
|
||||||
|
pub fn query(&self) -> Option<&str> {
|
||||||
|
self.0.query()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the port of this [Uri] as a `u16`.
|
||||||
|
pub fn port(&self) -> Option<u16> {
|
||||||
|
self.0.port_u16()
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/uri/parts.rs
Normal file
8
src/uri/parts.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use crate::uri::{Authority, PathAndQuery, Scheme};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Parts {
|
||||||
|
pub scheme: Option<Scheme>,
|
||||||
|
pub authority: Option<Authority>,
|
||||||
|
pub path_and_query: Option<PathAndQuery>,
|
||||||
|
}
|
||||||
55
src/uri/path_and_query.rs
Normal file
55
src/uri/path_and_query.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::error::{Error, ErrorInvalidUri};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct PathAndQuery(pub(crate) http::uri::PathAndQuery);
|
||||||
|
|
||||||
|
impl PathAndQuery {
|
||||||
|
/// Convert a [PathAndQuery] from a static string.
|
||||||
|
///
|
||||||
|
/// This function will not perform any copying, however the string is checked to ensure that it is valid.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics if the argument is an invalid path and query.
|
||||||
|
#[inline]
|
||||||
|
pub fn from_static(src: &'static str) -> Self {
|
||||||
|
Self(http::uri::PathAndQuery::from_static(src))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path and query as a string component.
|
||||||
|
#[inline]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
self.0.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path component.
|
||||||
|
#[inline]
|
||||||
|
pub fn path(&self) -> &str {
|
||||||
|
self.0.path()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the query string component.
|
||||||
|
#[inline]
|
||||||
|
pub fn query(&self) -> Option<&str> {
|
||||||
|
self.0.query()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for PathAndQuery {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(PathAndQuery(s.parse().map_err(|_| {
|
||||||
|
Error::internal_server_error(ErrorInvalidUri)
|
||||||
|
})?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for PathAndQuery {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/uri/scheme.rs
Normal file
19
src/uri/scheme.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
|
/// Represents the scheme component of a URI
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Scheme(pub(crate) http::uri::Scheme);
|
||||||
|
|
||||||
|
impl Scheme {
|
||||||
|
/// HTTP protocol scheme
|
||||||
|
pub const HTTP: Scheme = Scheme(http::uri::Scheme::HTTP);
|
||||||
|
|
||||||
|
/// HTTP protocol over TLS.
|
||||||
|
pub const HTTPS: Scheme = Scheme(http::uri::Scheme::HTTPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Scheme {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/version.rs
Normal file
19
src/version.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||||
|
pub struct Version(pub(crate) http::Version);
|
||||||
|
|
||||||
|
impl Version {
|
||||||
|
/// `HTTP/0.9`
|
||||||
|
pub const HTTP_09: Version = Version(http::Version::HTTP_09);
|
||||||
|
|
||||||
|
/// `HTTP/1.0`
|
||||||
|
pub const HTTP_10: Version = Version(http::Version::HTTP_10);
|
||||||
|
|
||||||
|
/// `HTTP/1.1`
|
||||||
|
pub const HTTP_11: Version = Version(http::Version::HTTP_11);
|
||||||
|
|
||||||
|
/// `HTTP/2.0`
|
||||||
|
pub const HTTP_2: Version = Version(http::Version::HTTP_2);
|
||||||
|
|
||||||
|
/// `HTTP/3.0`
|
||||||
|
pub const HTTP_3: Version = Version(http::Version::HTTP_3);
|
||||||
|
}
|
||||||
35
src/web/data.rs
Normal file
35
src/web/data.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use crate::{Error, FromRequest, Request};
|
||||||
|
|
||||||
|
pub struct Data<T>(pub T);
|
||||||
|
|
||||||
|
impl<T> Deref for Data<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DerefMut for Data<T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T: Clone + Send + Sync + 'static> FromRequest for Data<T> {
|
||||||
|
async fn from_request(req: &mut Request) -> crate::Result<Self> {
|
||||||
|
req.extensions()
|
||||||
|
.get::<T>()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::internal_server_error(anyhow::anyhow!(
|
||||||
|
"Data of type `{}` was not found.",
|
||||||
|
std::any::type_name::<T>()
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.map(Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/web/json.rs
Normal file
40
src/web/json.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use crate::{Error, FromRequest, HeaderName, IntoResponse, Request, Response, Result};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub struct Json<T>(pub T);
|
||||||
|
|
||||||
|
impl<T> Deref for Json<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DerefMut for Json<T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T: DeserializeOwned> FromRequest for Json<T> {
|
||||||
|
async fn from_request(req: &mut Request) -> Result<Self> {
|
||||||
|
let data = req.take_body().into_bytes().await?;
|
||||||
|
Ok(Self(
|
||||||
|
serde_json::from_slice(&data).map_err(Error::bad_request)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize> IntoResponse for Json<T> {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
let data = serde_json::to_vec(&self.0).map_err(Error::internal_server_error)?;
|
||||||
|
Response::builder()
|
||||||
|
.header(HeaderName::CONTENT_TYPE, "application/json")
|
||||||
|
.body(data.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/web/mod.rs
Normal file
99
src/web/mod.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
mod data;
|
||||||
|
mod json;
|
||||||
|
mod path;
|
||||||
|
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
|
pub use data::Data;
|
||||||
|
pub use json::Json;
|
||||||
|
pub use path::Path;
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
|
||||||
|
use crate::{Body, Error, HeaderMap, Request, Response, Result, StatusCode};
|
||||||
|
|
||||||
|
/// Types that can be created from requests.
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait FromRequest: Sized {
|
||||||
|
/// Perform the extraction.
|
||||||
|
async fn from_request(req: &mut Request) -> Result<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for generating responses.
|
||||||
|
///
|
||||||
|
/// Types that implement [IntoResponse] can be returned from handlers.
|
||||||
|
pub trait IntoResponse {
|
||||||
|
fn into_response(self) -> Result<Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for String {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
Response::builder().body(self.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for &'static str {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
Response::builder().body(self.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for &'static [u8] {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
Response::builder().body(self.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Bytes {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
Response::builder().body(self.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Vec<u8> {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
Response::builder().body(self.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for () {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
Response::builder().body(Body::empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Infallible {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
Response::builder().body(Body::empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for StatusCode {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
Response::builder().status(self).body(Body::empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: IntoResponse> IntoResponse for (StatusCode, T) {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
let mut resp = self.1.into_response()?;
|
||||||
|
resp.set_status(self.0);
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: IntoResponse> IntoResponse for (StatusCode, HeaderMap, T) {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
let mut resp = self.2.into_response()?;
|
||||||
|
resp.set_status(self.0);
|
||||||
|
resp.headers_mut().extend(self.1.into_iter());
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: IntoResponse, E: Into<Error>> IntoResponse for Result<T, E> {
|
||||||
|
fn into_response(self) -> Result<Response> {
|
||||||
|
self.map_err(Into::into)
|
||||||
|
.and_then(IntoResponse::into_response)
|
||||||
|
}
|
||||||
|
}
|
||||||
672
src/web/path/de.rs
Normal file
672
src/web/path/de.rs
Normal file
@@ -0,0 +1,672 @@
|
|||||||
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
|
use serde::{
|
||||||
|
de::{self, DeserializeSeed, EnumAccess, Error, MapAccess, SeqAccess, VariantAccess, Visitor},
|
||||||
|
forward_to_deserialize_any, Deserializer,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::route_recognizer::Params;
|
||||||
|
|
||||||
|
/// This type represents errors that can occur when deserializing.
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct PathDeserializerError(pub(crate) String);
|
||||||
|
|
||||||
|
impl de::Error for PathDeserializerError {
|
||||||
|
#[inline]
|
||||||
|
fn custom<T: Display>(msg: T) -> Self {
|
||||||
|
PathDeserializerError(msg.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for PathDeserializerError {
|
||||||
|
#[inline]
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
"path deserializer error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PathDeserializerError {
|
||||||
|
#[inline]
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
PathDeserializerError(msg) => write!(f, "{}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! unsupported_type {
|
||||||
|
($trait_fn:ident, $name:literal) => {
|
||||||
|
fn $trait_fn<V>(self, _: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
Err(PathDeserializerError::custom(concat!(
|
||||||
|
"unsupported type: ",
|
||||||
|
$name
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! parse_single_value {
|
||||||
|
($trait_fn:ident, $visit_fn:ident, $tp:literal) => {
|
||||||
|
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
if self.url_params.0.len() != 1 {
|
||||||
|
return Err(PathDeserializerError::custom(
|
||||||
|
format!(
|
||||||
|
"wrong number of parameters: {} expected 1",
|
||||||
|
self.url_params.0.len()
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = self.url_params.0[0].1.parse().map_err(|_| {
|
||||||
|
PathDeserializerError::custom(format!(
|
||||||
|
"can not parse `{:?}` to a `{}`",
|
||||||
|
self.url_params.0[0].1.as_str(),
|
||||||
|
$tp
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
visitor.$visit_fn(value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct PathDeserializer<'de> {
|
||||||
|
url_params: &'de Params,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> PathDeserializer<'de> {
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn new(url_params: &'de Params) -> Self {
|
||||||
|
PathDeserializer { url_params }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserializer<'de> for PathDeserializer<'de> {
|
||||||
|
type Error = PathDeserializerError;
|
||||||
|
|
||||||
|
unsupported_type!(deserialize_any, "'any'");
|
||||||
|
unsupported_type!(deserialize_bytes, "bytes");
|
||||||
|
unsupported_type!(deserialize_option, "Option<T>");
|
||||||
|
unsupported_type!(deserialize_identifier, "identifier");
|
||||||
|
unsupported_type!(deserialize_ignored_any, "ignored_any");
|
||||||
|
|
||||||
|
parse_single_value!(deserialize_bool, visit_bool, "bool");
|
||||||
|
parse_single_value!(deserialize_i8, visit_i8, "i8");
|
||||||
|
parse_single_value!(deserialize_i16, visit_i16, "i16");
|
||||||
|
parse_single_value!(deserialize_i32, visit_i32, "i32");
|
||||||
|
parse_single_value!(deserialize_i64, visit_i64, "i64");
|
||||||
|
parse_single_value!(deserialize_u8, visit_u8, "u8");
|
||||||
|
parse_single_value!(deserialize_u16, visit_u16, "u16");
|
||||||
|
parse_single_value!(deserialize_u32, visit_u32, "u32");
|
||||||
|
parse_single_value!(deserialize_u64, visit_u64, "u64");
|
||||||
|
parse_single_value!(deserialize_f32, visit_f32, "f32");
|
||||||
|
parse_single_value!(deserialize_f64, visit_f64, "f64");
|
||||||
|
parse_single_value!(deserialize_string, visit_string, "String");
|
||||||
|
parse_single_value!(deserialize_byte_buf, visit_string, "String");
|
||||||
|
parse_single_value!(deserialize_char, visit_char, "char");
|
||||||
|
|
||||||
|
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
if self.url_params.0.len() != 1 {
|
||||||
|
return Err(PathDeserializerError::custom(format!(
|
||||||
|
"wrong number of parameters: {} expected 1",
|
||||||
|
self.url_params.0.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
visitor.visit_str(&self.url_params.0[0].1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_unit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_unit_struct<V>(
|
||||||
|
self,
|
||||||
|
_name: &'static str,
|
||||||
|
visitor: V,
|
||||||
|
) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_unit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_newtype_struct<V>(
|
||||||
|
self,
|
||||||
|
_name: &'static str,
|
||||||
|
visitor: V,
|
||||||
|
) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_newtype_struct(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_seq(SeqDeserializer {
|
||||||
|
params: &self.url_params.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_tuple<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
if self.url_params.0.len() < len {
|
||||||
|
return Err(PathDeserializerError::custom(
|
||||||
|
format!(
|
||||||
|
"wrong number of parameters: {} expected {}",
|
||||||
|
self.url_params.0.len(),
|
||||||
|
len
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
visitor.visit_seq(SeqDeserializer {
|
||||||
|
params: &self.url_params.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_tuple_struct<V>(
|
||||||
|
self,
|
||||||
|
_name: &'static str,
|
||||||
|
len: usize,
|
||||||
|
visitor: V,
|
||||||
|
) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
if self.url_params.0.len() < len {
|
||||||
|
return Err(PathDeserializerError::custom(
|
||||||
|
format!(
|
||||||
|
"wrong number of parameters: {} expected {}",
|
||||||
|
self.url_params.0.len(),
|
||||||
|
len
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
visitor.visit_seq(SeqDeserializer {
|
||||||
|
params: &self.url_params.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_map(MapDeserializer {
|
||||||
|
params: &self.url_params.0,
|
||||||
|
value: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_struct<V>(
|
||||||
|
self,
|
||||||
|
_name: &'static str,
|
||||||
|
_fields: &'static [&'static str],
|
||||||
|
visitor: V,
|
||||||
|
) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
self.deserialize_map(visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_enum<V>(
|
||||||
|
self,
|
||||||
|
_name: &'static str,
|
||||||
|
_variants: &'static [&'static str],
|
||||||
|
visitor: V,
|
||||||
|
) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
if self.url_params.0.len() != 1 {
|
||||||
|
return Err(PathDeserializerError::custom(format!(
|
||||||
|
"wrong number of parameters: {} expected 1",
|
||||||
|
self.url_params.0.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitor.visit_enum(EnumDeserializer {
|
||||||
|
value: &self.url_params.0[0].1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MapDeserializer<'de> {
|
||||||
|
params: &'de [(String, String)],
|
||||||
|
value: Option<&'de str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> MapAccess<'de> for MapDeserializer<'de> {
|
||||||
|
type Error = PathDeserializerError;
|
||||||
|
|
||||||
|
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
|
||||||
|
where
|
||||||
|
K: DeserializeSeed<'de>,
|
||||||
|
{
|
||||||
|
match self.params.split_first() {
|
||||||
|
Some(((key, value), tail)) => {
|
||||||
|
self.value = Some(value);
|
||||||
|
self.params = tail;
|
||||||
|
seed.deserialize(KeyDeserializer { key }).map(Some)
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: DeserializeSeed<'de>,
|
||||||
|
{
|
||||||
|
match self.value.take() {
|
||||||
|
Some(value) => seed.deserialize(ValueDeserializer { value }),
|
||||||
|
None => Err(serde::de::Error::custom("value is missing")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct KeyDeserializer<'de> {
|
||||||
|
key: &'de str,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! parse_key {
|
||||||
|
($trait_fn:ident) => {
|
||||||
|
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_str(self.key)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserializer<'de> for KeyDeserializer<'de> {
|
||||||
|
type Error = PathDeserializerError;
|
||||||
|
|
||||||
|
parse_key!(deserialize_identifier);
|
||||||
|
parse_key!(deserialize_str);
|
||||||
|
parse_key!(deserialize_string);
|
||||||
|
|
||||||
|
fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
Err(PathDeserializerError::custom("Unexpected"))
|
||||||
|
}
|
||||||
|
|
||||||
|
forward_to_deserialize_any! {
|
||||||
|
bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char bytes
|
||||||
|
byte_buf option unit unit_struct seq tuple
|
||||||
|
tuple_struct map newtype_struct struct enum ignored_any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! parse_value {
|
||||||
|
($trait_fn:ident, $visit_fn:ident, $ty:literal) => {
|
||||||
|
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
let v = self.value.parse().map_err(|_| {
|
||||||
|
PathDeserializerError::custom(format!(
|
||||||
|
"can not parse `{:?}` to a `{}`",
|
||||||
|
self.value, $ty
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
visitor.$visit_fn(v)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ValueDeserializer<'de> {
|
||||||
|
value: &'de str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserializer<'de> for ValueDeserializer<'de> {
|
||||||
|
type Error = PathDeserializerError;
|
||||||
|
|
||||||
|
unsupported_type!(deserialize_any, "any");
|
||||||
|
unsupported_type!(deserialize_seq, "seq");
|
||||||
|
unsupported_type!(deserialize_map, "map");
|
||||||
|
unsupported_type!(deserialize_identifier, "identifier");
|
||||||
|
|
||||||
|
parse_value!(deserialize_bool, visit_bool, "bool");
|
||||||
|
parse_value!(deserialize_i8, visit_i8, "i8");
|
||||||
|
parse_value!(deserialize_i16, visit_i16, "i16");
|
||||||
|
parse_value!(deserialize_i32, visit_i32, "i16");
|
||||||
|
parse_value!(deserialize_i64, visit_i64, "i64");
|
||||||
|
parse_value!(deserialize_u8, visit_u8, "u8");
|
||||||
|
parse_value!(deserialize_u16, visit_u16, "u16");
|
||||||
|
parse_value!(deserialize_u32, visit_u32, "u32");
|
||||||
|
parse_value!(deserialize_u64, visit_u64, "u64");
|
||||||
|
parse_value!(deserialize_f32, visit_f32, "f32");
|
||||||
|
parse_value!(deserialize_f64, visit_f64, "f64");
|
||||||
|
parse_value!(deserialize_string, visit_string, "String");
|
||||||
|
parse_value!(deserialize_byte_buf, visit_string, "String");
|
||||||
|
parse_value!(deserialize_char, visit_char, "char");
|
||||||
|
|
||||||
|
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_borrowed_str(self.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_borrowed_bytes(self.value.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_some(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_unit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_unit_struct<V>(
|
||||||
|
self,
|
||||||
|
_name: &'static str,
|
||||||
|
visitor: V,
|
||||||
|
) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_unit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_newtype_struct<V>(
|
||||||
|
self,
|
||||||
|
_name: &'static str,
|
||||||
|
visitor: V,
|
||||||
|
) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_newtype_struct(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_tuple<V>(self, _len: usize, _visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
Err(PathDeserializerError::custom("unsupported type: tuple"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_tuple_struct<V>(
|
||||||
|
self,
|
||||||
|
_name: &'static str,
|
||||||
|
_len: usize,
|
||||||
|
_visitor: V,
|
||||||
|
) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
Err(PathDeserializerError::custom(
|
||||||
|
"unsupported type: tuple struct",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_struct<V>(
|
||||||
|
self,
|
||||||
|
_name: &'static str,
|
||||||
|
_fields: &'static [&'static str],
|
||||||
|
_visitor: V,
|
||||||
|
) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
Err(PathDeserializerError::custom("unsupported type: struct"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_enum<V>(
|
||||||
|
self,
|
||||||
|
_name: &'static str,
|
||||||
|
_variants: &'static [&'static str],
|
||||||
|
visitor: V,
|
||||||
|
) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_enum(EnumDeserializer { value: self.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_unit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EnumDeserializer<'de> {
|
||||||
|
value: &'de str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> EnumAccess<'de> for EnumDeserializer<'de> {
|
||||||
|
type Error = PathDeserializerError;
|
||||||
|
type Variant = UnitVariant;
|
||||||
|
|
||||||
|
fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error>
|
||||||
|
where
|
||||||
|
V: de::DeserializeSeed<'de>,
|
||||||
|
{
|
||||||
|
Ok((
|
||||||
|
seed.deserialize(KeyDeserializer { key: self.value })?,
|
||||||
|
UnitVariant,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UnitVariant;
|
||||||
|
|
||||||
|
impl<'de> VariantAccess<'de> for UnitVariant {
|
||||||
|
type Error = PathDeserializerError;
|
||||||
|
|
||||||
|
fn unit_variant(self) -> Result<(), Self::Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn newtype_variant_seed<T>(self, _seed: T) -> Result<T::Value, Self::Error>
|
||||||
|
where
|
||||||
|
T: DeserializeSeed<'de>,
|
||||||
|
{
|
||||||
|
Err(PathDeserializerError::custom("not supported"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tuple_variant<V>(self, _len: usize, _visitor: V) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
Err(PathDeserializerError::custom("not supported"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn struct_variant<V>(
|
||||||
|
self,
|
||||||
|
_fields: &'static [&'static str],
|
||||||
|
_visitor: V,
|
||||||
|
) -> Result<V::Value, Self::Error>
|
||||||
|
where
|
||||||
|
V: Visitor<'de>,
|
||||||
|
{
|
||||||
|
Err(PathDeserializerError::custom("not supported"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SeqDeserializer<'de> {
|
||||||
|
params: &'de [(String, String)],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> SeqAccess<'de> for SeqDeserializer<'de> {
|
||||||
|
type Error = PathDeserializerError;
|
||||||
|
|
||||||
|
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
|
||||||
|
where
|
||||||
|
T: DeserializeSeed<'de>,
|
||||||
|
{
|
||||||
|
match self.params.split_first() {
|
||||||
|
Some(((_, value), tail)) => {
|
||||||
|
self.params = tail;
|
||||||
|
Ok(Some(seed.deserialize(ValueDeserializer { value })?))
|
||||||
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::float_cmp)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Eq, PartialEq)]
|
||||||
|
enum MyEnum {
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
#[serde(rename = "c")]
|
||||||
|
C,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Eq, PartialEq)]
|
||||||
|
struct Struct {
|
||||||
|
c: String,
|
||||||
|
b: bool,
|
||||||
|
a: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_url_params<I, K, V>(values: I) -> Params
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (K, V)>,
|
||||||
|
K: Into<String>,
|
||||||
|
V: Into<String>,
|
||||||
|
{
|
||||||
|
Params(
|
||||||
|
values
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k.into(), v.into()))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! check_single_value {
|
||||||
|
($ty:ty, $value_str:literal, $value:expr) => {
|
||||||
|
#[allow(clippy::bool_assert_comparison)]
|
||||||
|
{
|
||||||
|
let url_params = create_url_params(vec![("value", $value_str)]);
|
||||||
|
let deserializer = PathDeserializer::new(&url_params);
|
||||||
|
assert_eq!(<$ty>::deserialize(deserializer).unwrap(), $value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_single_value() {
|
||||||
|
check_single_value!(bool, "true", true);
|
||||||
|
check_single_value!(bool, "false", false);
|
||||||
|
check_single_value!(i8, "-123", -123);
|
||||||
|
check_single_value!(i16, "-123", -123);
|
||||||
|
check_single_value!(i32, "-123", -123);
|
||||||
|
check_single_value!(i64, "-123", -123);
|
||||||
|
check_single_value!(u8, "123", 123);
|
||||||
|
check_single_value!(u16, "123", 123);
|
||||||
|
check_single_value!(u32, "123", 123);
|
||||||
|
check_single_value!(u64, "123", 123);
|
||||||
|
check_single_value!(f32, "123", 123.0);
|
||||||
|
check_single_value!(f64, "123", 123.0);
|
||||||
|
check_single_value!(String, "abc", "abc");
|
||||||
|
check_single_value!(char, "a", 'a');
|
||||||
|
|
||||||
|
let url_params = create_url_params(vec![("a", "B")]);
|
||||||
|
assert_eq!(
|
||||||
|
MyEnum::deserialize(PathDeserializer::new(&url_params)).unwrap(),
|
||||||
|
MyEnum::B
|
||||||
|
);
|
||||||
|
|
||||||
|
let url_params = create_url_params(vec![("a", "1"), ("b", "2")]);
|
||||||
|
assert_eq!(
|
||||||
|
i32::deserialize(PathDeserializer::new(&url_params)).unwrap_err(),
|
||||||
|
PathDeserializerError::custom("wrong number of parameters: 2 expected 1".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_seq() {
|
||||||
|
let url_params = create_url_params(vec![("a", "1"), ("b", "true"), ("c", "abc")]);
|
||||||
|
assert_eq!(
|
||||||
|
<(i32, bool, String)>::deserialize(PathDeserializer::new(&url_params)).unwrap(),
|
||||||
|
(1, true, "abc".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Eq, PartialEq)]
|
||||||
|
struct TupleStruct(i32, bool, String);
|
||||||
|
assert_eq!(
|
||||||
|
TupleStruct::deserialize(PathDeserializer::new(&url_params)).unwrap(),
|
||||||
|
TupleStruct(1, true, "abc".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
let url_params = create_url_params(vec![("a", "1"), ("b", "2"), ("c", "3")]);
|
||||||
|
assert_eq!(
|
||||||
|
<Vec<i32>>::deserialize(PathDeserializer::new(&url_params)).unwrap(),
|
||||||
|
vec![1, 2, 3]
|
||||||
|
);
|
||||||
|
|
||||||
|
let url_params = create_url_params(vec![("a", "c"), ("a", "B")]);
|
||||||
|
assert_eq!(
|
||||||
|
<Vec<MyEnum>>::deserialize(PathDeserializer::new(&url_params)).unwrap(),
|
||||||
|
vec![MyEnum::C, MyEnum::B]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_struct() {
|
||||||
|
let url_params = create_url_params(vec![("a", "1"), ("b", "true"), ("c", "abc")]);
|
||||||
|
assert_eq!(
|
||||||
|
Struct::deserialize(PathDeserializer::new(&url_params)).unwrap(),
|
||||||
|
Struct {
|
||||||
|
c: "abc".to_string(),
|
||||||
|
b: true,
|
||||||
|
a: 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_map() {
|
||||||
|
let url_params = create_url_params(vec![("a", "1"), ("b", "true"), ("c", "abc")]);
|
||||||
|
assert_eq!(
|
||||||
|
<HashMap<String, String>>::deserialize(PathDeserializer::new(&url_params)).unwrap(),
|
||||||
|
[("a", "1"), ("b", "true"), ("c", "abc")]
|
||||||
|
.iter()
|
||||||
|
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
|
||||||
|
.collect()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/web/path/mod.rs
Normal file
42
src/web/path/mod.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
mod de;
|
||||||
|
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
|
use crate::error::{ErrorInvalidPathParams, ErrorMissingRouteParams};
|
||||||
|
use crate::route_recognizer::Params;
|
||||||
|
use crate::{Error, FromRequest, Request, Result};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Path<T>(pub T);
|
||||||
|
|
||||||
|
impl<T> Deref for Path<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DerefMut for Path<T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T> FromRequest for Path<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + Send,
|
||||||
|
{
|
||||||
|
async fn from_request(req: &mut Request) -> Result<Self> {
|
||||||
|
let params = req
|
||||||
|
.extensions_mut()
|
||||||
|
.get::<Params>()
|
||||||
|
.ok_or_else(|| Error::internal_server_error(ErrorMissingRouteParams))?;
|
||||||
|
T::deserialize(de::PathDeserializer::new(params))
|
||||||
|
.map_err(|_| Error::internal_server_error(ErrorInvalidPathParams))
|
||||||
|
.map(Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user