Initial commit

This commit is contained in:
Sunli
2021-08-11 17:08:51 +08:00
commit eda29e6219
43 changed files with 4987 additions and 0 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: sunli829

24
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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:

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
/target
Cargo.lock

2
.rustfmt.toml Normal file
View File

@@ -0,0 +1,2 @@
edition = "2018"
newline_style = "unix"

34
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,3 @@
pub(crate) mod map;
pub(crate) mod name;
pub(crate) mod value;

888
src/header/name.rs Normal file
View 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 clients 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
use crate::Endpoint;
pub trait Middleware {
fn transform<T: Endpoint>(&self, ep: T) -> Box<dyn Endpoint>;
}

View 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
View File

@@ -0,0 +1,5 @@
mod add_data;
mod strip_prefix;
pub use add_data::AddData;
pub use strip_prefix::StripPrefix;

View 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
View 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
View 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
View 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
View 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
View 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 &current_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 &current_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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}
}