├── .cargo-husky └── hooks │ ├── pre-commit │ └── pre-push ├── .ci └── run_test.sh ├── .gitignore ├── .rustfmt.toml ├── .travis.yml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── changelog ├── 0.12.x.md ├── 0.13.x.md └── changelog-old.md ├── doctest ├── Cargo.toml └── src │ └── lib.rs ├── examples ├── custom-logging │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── diesel │ ├── .env.sample │ ├── .gitignore │ ├── Cargo.toml │ ├── diesel.toml │ ├── migrations │ │ ├── 00000000000000_diesel_initial_setup │ │ │ ├── down.sql │ │ │ └── up.sql │ │ └── 2018-08-15-093259_create_posts │ │ │ ├── down.sql │ │ │ └── up.sql │ └── src │ │ ├── main.rs │ │ ├── model.rs │ │ └── schema.rs ├── juniper │ ├── Cargo.toml │ └── src │ │ ├── main.rs │ │ └── schema.rs ├── jwt-auth │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── middlewares │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── session-redis │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── staticfiles │ ├── Cargo.toml │ ├── src │ │ └── main.rs │ └── static │ │ └── index.html ├── template-askama │ ├── Cargo.toml │ ├── build.rs │ ├── src │ │ └── main.rs │ └── templates │ │ └── index.html ├── template-handlebars │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── template-horrorshow │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── template-tera │ ├── Cargo.toml │ └── src │ │ └── main.rs └── websocket │ ├── Cargo.toml │ └── src │ └── main.rs ├── finchers-juniper ├── Cargo.toml ├── README.md ├── changelog.md ├── examples │ ├── executors.rs │ └── todos.rs ├── release.toml ├── src │ ├── execute │ │ ├── current_thread.rs │ │ ├── mod.rs │ │ ├── nonblocking.rs │ │ └── with_spawner.rs │ ├── graphiql.rs │ ├── lib.rs │ └── request.rs └── tests │ └── integration_test.rs ├── finchers-macros ├── Cargo.toml └── src │ └── lib.rs ├── finchers-session ├── Cargo.toml ├── README.md ├── examples │ └── simple.rs ├── release.toml └── src │ ├── cookie.rs │ ├── in_memory.rs │ ├── lib.rs │ ├── redis.rs │ ├── session.rs │ ├── tests.rs │ └── util.rs ├── finchers-template ├── Cargo.toml ├── README.md ├── build.rs ├── changelog │ └── v0.1.x.md ├── release.toml └── src │ ├── backend │ ├── askama.rs │ ├── engine.rs │ ├── handlebars.rs │ ├── horrorshow.rs │ ├── mod.rs │ └── tera.rs │ ├── lib.rs │ └── renderer.rs ├── finchers-tungstenite ├── Cargo.toml ├── README.md ├── examples │ └── server.rs ├── release.toml ├── src │ ├── handshake.rs │ └── lib.rs └── tests │ └── test_tungstenite.rs ├── release.toml ├── scripts └── update_local_registry.sh ├── src ├── action.rs ├── common.rs ├── common │ ├── combine.rs │ ├── func.rs │ └── hlist.rs ├── endpoint.rs ├── endpoint │ ├── boxed.rs │ ├── ext.rs │ ├── ext │ │ ├── and.rs │ │ ├── and_then.rs │ │ ├── map.rs │ │ ├── map_err.rs │ │ ├── or.rs │ │ ├── or_strict.rs │ │ └── recover.rs │ ├── syntax.rs │ └── syntax │ │ ├── encoded.rs │ │ └── verb.rs ├── endpoints.rs ├── endpoints │ ├── body.rs │ ├── fs.rs │ ├── header.rs │ └── query.rs ├── error.rs ├── lib.rs ├── output.rs ├── output │ ├── binary.rs │ ├── debug.rs │ ├── fs.rs │ ├── json.rs │ ├── redirect.rs │ ├── status.rs │ └── text.rs ├── server │ ├── builder.rs │ ├── error.rs │ └── http_server.rs ├── service.rs ├── test.rs └── util.rs └── tests ├── endpoint ├── and.rs ├── and_then.rs ├── boxed.rs ├── macros.rs ├── map.rs ├── mod.rs ├── or.rs ├── or_strict.rs ├── recover.rs └── syntax.rs ├── endpoints ├── body.rs ├── cookie.rs ├── header.rs ├── mod.rs ├── query.rs └── upgrade.rs └── tests.rs /.cargo-husky/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | [[ -n "${DISABLE_GIT_HOOKS}" ]] && { 6 | echo "[warn] Git hooks are disabled by user." 7 | exit 0 8 | } 9 | 10 | BRANCH="$(git symbolic-ref --short HEAD)" 11 | [[ "${BRANCH:-}" = wip-* ]] && { 12 | echo "[info] The current branch is working in progress." 13 | exit 0 14 | } 15 | 16 | if cargo fmt --version >/dev/null 2>&1; then 17 | (set -x; cargo fmt -- --check) 18 | fi 19 | -------------------------------------------------------------------------------- /.cargo-husky/hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | [[ -n "${DISABLE_GIT_HOOKS}" ]] && { 6 | echo "[warn] Git hooks are disabled by user." 7 | exit 0 8 | } 9 | 10 | BRANCH="$(git symbolic-ref --short HEAD)" 11 | [[ "${BRANCH:-}" = wip-* ]] && { 12 | echo "[info] The current branch is working in progress." 13 | exit 0 14 | } 15 | 16 | [[ -z $(git status --porcelain) ]] || { 17 | echo '[error] The repository is dirty.' 18 | exit 1 19 | } 20 | 21 | set -x 22 | exec "$(git rev-parse --show-toplevel)/.ci/run_test.sh" 23 | -------------------------------------------------------------------------------- /.ci/run_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | rustc --version 6 | cargo --version 7 | 8 | if cargo fmt --version >/dev/null 2>&1; then 9 | cargo fmt -- --check 10 | fi 11 | 12 | if cargo clippy --version >/dev/null 2>&1; then 13 | cargo clippy --all --all-targets 14 | fi 15 | 16 | cargo test --all 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | **/*.rs.bk 4 | 5 | /kcov/ 6 | /master.zip 7 | 8 | .sass-cache/ 9 | Gemfile.lock 10 | doc-upload/ 11 | **/*.rmeta 12 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | #edition = "Edition2018" 2 | max_width = 100 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | language: rust 4 | 5 | cache: 6 | cargo: true 7 | 8 | branches: 9 | only: 10 | - master 11 | 12 | script: .ci/run_test.sh 13 | 14 | matrix: 15 | allow_failures: 16 | - rust: nightly 17 | fast_finish: true 18 | 19 | include: 20 | - rust: stable 21 | before_script: 22 | - rustup component add rustfmt clippy 23 | - rust: beta 24 | - rust: nightly 25 | - rust: 1.31.1 26 | 27 | - rust: stable 28 | env: DEPLOY_API_DOC 29 | script: >- 30 | rm -rf target/doc && 31 | cargo update && 32 | cargo doc --no-deps -p izanami-service && 33 | cargo doc --no-deps -p finchers --all-features && 34 | rm -f target/doc/.lock && 35 | (echo '' > target/doc/index.html) 36 | deploy: 37 | provider: pages 38 | skip_cleanup: true 39 | github_token: $GH_TOKEN 40 | repo: finchers-rs/finchers 41 | target_branch: gh-pages 42 | local_dir: target/doc 43 | on: 44 | branch: master 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "finchers" 3 | version = "0.14.0-dev" 4 | edition = "2018" 5 | description = "A combinator library for builidng asynchronous HTTP services" 6 | authors = ["Yusuke Sasaki "] 7 | license = "MIT OR Apache-2.0" 8 | readme = "README.md" 9 | homepage = "https://finchers-rs.github.io" 10 | repository = "https://github.com/finchers-rs/finchers.git" 11 | keywords = ["finchers", "web", "framework", "server"] 12 | categories = ["web-programming::http-server"] 13 | 14 | include = [ 15 | "/Cargo.toml", 16 | "/build.rs", 17 | "/src/**/*", 18 | "/tests/**/*", 19 | "/examples/**/*", 20 | "/benches/**/*", 21 | "/LICENSE-MIT", 22 | "/LICENSE-APACHE", 23 | "/README.md" 24 | ] 25 | 26 | [badges] 27 | maintenance = { status = "actively-developed" } 28 | 29 | [features] 30 | default = [] 31 | secure = ["cookie/secure"] 32 | 33 | [dependencies] 34 | finchers-macros = { version = "0.14.0-dev", path = "finchers-macros" } 35 | 36 | bitflags = "1.0.4" 37 | bytes = { version = "0.4.9", features = ["either"] } 38 | cookie = { version = "0.11.0", features = ["percent-encode"] } 39 | either = "1.5.0" 40 | failure = "0.1.2" 41 | futures = "0.1.23" 42 | http = "0.1.10" 43 | izanami-service = "0.1.0-preview.1" 44 | izanami-util = "0.1.0-preview.1" 45 | log = "0.4.3" 46 | mime = "0.3.8" 47 | mime_guess = "2.0.0-alpha.6" 48 | percent-encoding = "1.0.1" 49 | serde = { version = "1.0.71", features = ["derive"] } 50 | serde_json = "1.0.24" 51 | serde_qs = "0.4.1" 52 | tokio = "0.1.8" 53 | url = "1.7.1" 54 | 55 | [dev-dependencies] 56 | matches = "0.1.8" 57 | izanami = "0.1.0-preview.1" 58 | version-sync = "0.7" 59 | 60 | [dev-dependencies.cargo-husky] 61 | version = "1" 62 | default-features = false 63 | features = ["user-hooks"] 64 | 65 | [workspace] 66 | members = [ 67 | "finchers-macros", 68 | #"finchers-juniper", 69 | #"finchers-session", 70 | #"finchers-template", 71 | "finchers-tungstenite", 72 | 73 | "doctest", 74 | #"examples/custom-logging", 75 | #"examples/diesel", 76 | #"examples/juniper", 77 | #"examples/jwt-auth", 78 | #"examples/middlewares", 79 | #"examples/session-redis", 80 | #"examples/staticfiles", 81 | #"examples/template-askama", 82 | #"examples/template-handlebars", 83 | #"examples/template-horrorshow", 84 | #"examples/template-tera", 85 | #"examples/websocket", 86 | ] 87 | 88 | [patch.crates-io] 89 | #izanami = { git = "https://github.com/ubnt-intrepid/izanami.git" } 90 | #izanami-util = { git = "https://github.com/ubnt-intrepid/izanami.git" } 91 | #izanami-service = { git = "https://github.com/ubnt-intrepid/izanami.git" } 92 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Yusuke Sasaki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `finchers` 2 | 3 | [![Crates.io][crates-io-badge]][crates-io] 4 | [![Crates.io (Downloads)][downloads-badge]][crates-io] 5 | [![Docs.rs][docs-rs-badge]][docs-rs] 6 | [![Master doc][master-doc-badge]][master-doc] 7 | [![Rustc Version][rustc-version-badge]][rustc-version] 8 | [![dependency status][dependencies-badge]][dependencies] 9 | [![Gitter][gitter-badge]][gitter] 10 | 11 | `finchers` is a combinator library for building asynchronous HTTP services. 12 | 13 | The concept and design was highly inspired by [`finch`]. 14 | 15 | # Features 16 | 17 | * Asynchronous handling powerd by futures and Tokio 18 | * Building an HTTP service by *combining* the primitive components 19 | * Type-safe routing without (unstable) procedural macros 20 | 21 | # Usage 22 | 23 | Add this item to `Cargo.toml` in your project: 24 | 25 | ```toml 26 | [dependencies] 27 | finchers = "0.14.0-dev" 28 | ``` 29 | 30 | # Example 31 | 32 | ```rust,no_run 33 | use finchers::{ 34 | prelude::*, 35 | endpoint::syntax::path, 36 | }; 37 | 38 | fn main() -> izanami::Result<()> { 39 | let endpoint = path!(@get "/greeting/") 40 | .map(|name: String| { 41 | format!("Hello, {}!\n", name) 42 | }); 43 | 44 | izanami::Server::build() 45 | .start(endpoint.into_service()) 46 | } 47 | ``` 48 | 49 | # Resources 50 | 51 | * [API documentation (docs.rs)][docs-rs] 52 | * [API documentation (master)][master-doc] 53 | * [Examples][examples] 54 | * [Gitter chat][gitter] 55 | 56 | # Contributed Features 57 | 58 | * [`finchers-juniper`] - GraphQL integration support, based on [`juniper`] 59 | * [`finchers-tungstenite`] - WebSocket support, based on [`tungstenite`] 60 | * [`finchers-session`]: Session support 61 | * [`finchers-template`]: Template engine support 62 | 63 | # Status 64 | 65 | | Travis CI | Codecov | 66 | |:---------:|:-------:| 67 | | [![Travis CI][travis-badge]][travis] | [![Codecov][codecov-badge]][codecov] | 68 | 69 | # License 70 | This project is licensed under either of 71 | 72 | * MIT license, ([LICENSE-MIT](./LICENSE-MIT) or http://opensource.org/licenses/MIT) 73 | * Apache License, Version 2.0 ([LICENSE-APACHE](./LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 74 | 75 | at your option. 76 | 77 | 78 | 79 | [crates-io]: https://crates.io/crates/finchers 80 | [docs-rs]: https://docs.rs/finchers 81 | [master-doc]: https://finchers-rs.github.io/finchers 82 | [examples]: https://github.com/finchers-rs/examples 83 | [user-guide]: https://finchers-rs.github.io/finchers/guide/index.html 84 | [gitter]: https://gitter.im/finchers-rs/finchers 85 | [travis]: https://travis-ci.org/finchers-rs/finchers 86 | [circleci]: https://circleci.com/gh/finchers-rs/finchers/tree/master 87 | [codecov]: https://codecov.io/gh/finchers-rs/finchers 88 | [dependencies]: https://deps.rs/crate/finchers/0.13.5 89 | [rustc-version]: https://rust-lang.org 90 | 91 | [crates-io-badge]: https://img.shields.io/crates/v/finchers.svg 92 | [downloads-badge]: https://img.shields.io/crates/d/finchers.svg 93 | [docs-rs-badge]: https://docs.rs/finchers/badge.svg 94 | [master-doc-badge]: https://img.shields.io/badge/docs-master-blue.svg 95 | [gitter-badge]: https://badges.gitter.im/finchers-rs/finchers.svg 96 | [travis-badge]: https://travis-ci.org/finchers-rs/finchers.svg?branch=master 97 | [circleci-badge]: https://circleci.com/gh/finchers-rs/finchers/tree/master.svg?style=svg 98 | [codecov-badge]: https://codecov.io/gh/finchers-rs/finchers/branch/master/graph/badge.svg 99 | [dependencies-badge]: https://deps.rs/crate/finchers/0.13.5/status.svg 100 | [rustc-version-badge]: https://img.shields.io/badge/rustc-1.29+-yellow.svg 101 | 102 | [`finchers-juniper`]: https://github.com/finchers-rs/finchers-juniper 103 | [`finchers-tungstenite`]: https://github.com/finchers-rs/finchers-tungstenite 104 | [`finchers-session`]: https://github.com/finchers-rs/finchers-session 105 | [`finchers-template`]: https://github.com/finchers-rs/finchers-template 106 | 107 | [`finch`]: https://github.com/finagle/finch 108 | [`juniper`]: https://github.com/graphql-rust/juniper.git 109 | [`tungstenite`]: https://github.com/snapview/tungstenite-rs 110 | -------------------------------------------------------------------------------- /changelog/0.12.x.md: -------------------------------------------------------------------------------- 1 | 2 | ## 0.12.2 (2018-10-02) 3 | * fix the value of [package.metadata.docs.rs] 4 | 5 | 6 | ## 0.12.1 (2018-10-02) (yanked) 7 | 8 | * add flag to `output::body::Empty` to set the end of stream ([#340](https://github.com/finchers-rs/finchers/pull/340)) 9 | * remove unneeded feature flags ([#342](https://github.com/finchers-rs/finchers/pull/342)) 10 | * disable the feature flag 'secure' by default 11 | 12 | 13 | # 0.12.0 (2018-09-30) (yanked) 14 | 15 | The first release on this iteration. 16 | -------------------------------------------------------------------------------- /changelog/0.13.x.md: -------------------------------------------------------------------------------- 1 | 2 | ## 0.13.5 (2018-10-17) 3 | 4 | * add `Redirect` ([#367](https://github.com/finchers-rs/finchers/pull/367)) 5 | * reform Cookie endpoints ([#368](https://github.com/finchers-rs/finchers/pull/368)) 6 | 7 | 8 | ## 0.13.4 (2018-10-11) 9 | 10 | * add missing `IntoEndpoint::into_endpoint()` in `path!()` ([#363](https://github.com/finchers-rs/finchers/pull/363)) 11 | * introduce middleware-level error handler ([#365](https://github.com/finchers-rs/finchers/pull/365)) 12 | 13 | 14 | ## 0.13.3 (2018-10-10) 15 | 16 | * fix type error in the definition of `middleware::log::stdlog()` ([#360](https://github.com/finchers-rs/finchers/pull/360)) 17 | 18 | 19 | ## 0.13.2 (2018-10-09) 20 | 21 | * fallback to call tokio's blocking() if the runtime mode is not set ([#357](https://github.com/finchers-rs/finchers/pull/357)) 22 | * add `skeptic` and `cargo-husky` to dev-dependencies 23 | 24 | 25 | ## 0.13.1 (2018-10-08) 26 | 27 | * remove `unwrap()` from `AppPayload::poll_data()` ([#354](https://github.com/finchers-rs/finchers/pull/354)) 28 | 29 | 30 | # 0.13.0 (2018-10-08) 31 | 32 | The first release on this iteration. 33 | 34 | New features: 35 | 36 | * Introduce the new server implementation and test runner 37 | - lower level middleware support (compatible with tower-service) 38 | - improve the UI of testing facility 39 | * Add built-in support for HTTP/1.1 protocol upgrade 40 | * add a primitive endpoint `endpoint::Lazy` 41 | * add a trait `OutputEndpoint` for representing an endpoint with `Self::Output: Output` 42 | 43 | Breaking changes: 44 | 45 | * remove the old `launcher` and `local` 46 | * remove the endpoint-level logging support 47 | - use the middlware on the new server instead 48 | * remove some methods and trait implementations from `Input` and `ReqBody` 49 | * remove `ApplyFn` and redefine as `Apply` and `ApplyRaw` 50 | * remove the wrapper struct `SendEndpoint` and `impl_endpoint!()` 51 | * rename `IsSendEndpoint` to `SendEndpoint` 52 | * remove constructors and `From` impls from payload in `output::body` 53 | -------------------------------------------------------------------------------- /doctest/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "doctest" 3 | version = "0.1.0" 4 | authors = ["Yusuke Sasaki "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | 9 | [dependencies.doubter] 10 | version = "0.1" 11 | default-features = false 12 | 13 | [dev-dependencies] 14 | finchers = { path = ".." } 15 | izanami = "0.1.0-preview.1" 16 | -------------------------------------------------------------------------------- /doctest/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | #![doc(test(attr(deny(warnings))))] 3 | 4 | #[macro_use] 5 | extern crate doubter; 6 | 7 | generate_doc_tests! { 8 | include = "README.md", 9 | } 10 | -------------------------------------------------------------------------------- /examples/custom-logging/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-custom-logging" 3 | version = "0.0.0" 4 | edition = "2018" 5 | authors = ["Yusuke Sasaki "] 6 | publish = false 7 | 8 | [[bin]] 9 | name = "example_custom_logging" 10 | path = "src/main.rs" 11 | doc = false 12 | 13 | [dependencies] 14 | finchers = "0.13" 15 | slog = "2.4" 16 | sloggers = "0.2" 17 | http = "0.1.10" 18 | futures = "0.1.24" 19 | -------------------------------------------------------------------------------- /examples/custom-logging/src/main.rs: -------------------------------------------------------------------------------- 1 | use finchers::output::status::Created; 2 | use finchers::prelude::*; 3 | use finchers::server::middleware::log::log; 4 | use finchers::{path, routes}; 5 | 6 | use slog::Logger; 7 | 8 | fn main() { 9 | let endpoint = routes![ 10 | path!(@get / "index").map(|| "Index page"), 11 | path!(@get / "created").map(|| Created("created")), 12 | ]; 13 | 14 | let logger = build_logger(); 15 | let log_middleware = log(logging::Slog { 16 | logger: logger.new(slog::o!("local_addr" => "http://127.0.0.1:4000")), 17 | }); 18 | 19 | finchers::server::start(endpoint) 20 | .with_middleware(log_middleware) 21 | .serve("127.0.0.1:4000") 22 | .unwrap_or_else(|e| slog::error!(logger, "{}", e)); 23 | } 24 | 25 | fn build_logger() -> Logger { 26 | use sloggers::terminal::{Destination, TerminalLoggerBuilder}; 27 | use sloggers::types::{Format, Severity}; 28 | use sloggers::Build; 29 | 30 | let mut builder = TerminalLoggerBuilder::new(); 31 | builder.level(Severity::Debug); 32 | builder.destination(Destination::Stdout); 33 | builder.format(Format::Full); 34 | builder.build().expect("failed to construct a Logger") 35 | } 36 | 37 | mod logging { 38 | use finchers::server::middleware::log::{Logger, Logging}; 39 | use http::{Request, Response}; 40 | use std::time::Instant; 41 | 42 | #[derive(Clone)] 43 | pub struct Slog { 44 | pub logger: slog::Logger, 45 | } 46 | 47 | impl Logger for Slog { 48 | type Instance = SlogInstance; 49 | 50 | fn start(&self, request: &Request) -> Self::Instance { 51 | let start = Instant::now(); 52 | SlogInstance { 53 | logger: self.logger.new(slog::o! { 54 | "request_method" => request.method().to_string(), 55 | "request_uri" => request.uri().to_string(), 56 | }), 57 | start, 58 | } 59 | } 60 | } 61 | 62 | pub struct SlogInstance { 63 | logger: slog::Logger, 64 | start: Instant, 65 | } 66 | 67 | impl Logging for SlogInstance { 68 | fn finish(self, response: &Response) { 69 | slog::info!(self.logger, "response"; 70 | "response_status" => response.status().to_string(), 71 | "response_time" => format!("{:?}", self.start.elapsed()), 72 | ); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/diesel/.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://username:password@localhost/diesel 2 | -------------------------------------------------------------------------------- /examples/diesel/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /examples/diesel/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-diesel" 3 | version = "0.0.0" 4 | edition = "2018" 5 | authors = ["Yusuke Sasaki "] 6 | publish = false 7 | 8 | [[bin]] 9 | name = "example_diesel" 10 | path = "src/main.rs" 11 | doc = false 12 | 13 | [dependencies] 14 | finchers = "0.13" 15 | 16 | diesel = { version = "1.3.2", features = ["postgres", "r2d2"] } 17 | dotenv = "0.13.0" 18 | failure = "0.1.2" 19 | futures = "0.1.24" 20 | http = "0.1.10" 21 | r2d2 = "0.8.2" 22 | serde = "1.0" 23 | tokio = "0.1" 24 | -------------------------------------------------------------------------------- /examples/diesel/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | -------------------------------------------------------------------------------- /examples/diesel/migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /examples/diesel/migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /examples/diesel/migrations/2018-08-15-093259_create_posts/down.sql: -------------------------------------------------------------------------------- 1 | drop table posts 2 | -------------------------------------------------------------------------------- /examples/diesel/migrations/2018-08-15-093259_create_posts/up.sql: -------------------------------------------------------------------------------- 1 | create table posts ( 2 | id serial primary key, 3 | title varchar not null, 4 | body text not null, 5 | published boolean not null default false 6 | ) 7 | -------------------------------------------------------------------------------- /examples/diesel/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(proc_macro_derive_resolution_fallback)] 2 | 3 | #[macro_use] 4 | extern crate diesel; 5 | 6 | mod model; 7 | mod schema; 8 | 9 | use failure::Fallible; 10 | use http::StatusCode; 11 | 12 | use diesel::pg::PgConnection; 13 | use diesel::r2d2::ConnectionManager; 14 | 15 | use finchers::prelude::*; 16 | use finchers::rt::blocking_section; 17 | use finchers::{output, path, routes}; 18 | 19 | use crate::model::{NewPost, Post}; 20 | 21 | type Conn = r2d2::PooledConnection>; 22 | 23 | fn main() -> Fallible<()> { 24 | dotenv::dotenv()?; 25 | 26 | let acquire_conn = { 27 | use std::env; 28 | use std::sync::Arc; 29 | 30 | let manager = ConnectionManager::::new(env::var("DATABASE_URL")?); 31 | let pool = r2d2::Pool::builder().build(manager)?; 32 | Arc::new(endpoint::unit().and_then(move || { 33 | let pool = pool.clone(); 34 | blocking_section(move || pool.get().map_err(finchers::error::fail)) 35 | })) 36 | }; 37 | 38 | let get_posts = path!(@get /) 39 | .and(endpoints::query::optional()) 40 | .and(acquire_conn.clone()) 41 | .and_then({ 42 | #[derive(Debug, serde::Deserialize)] 43 | pub struct Query { 44 | count: i64, 45 | } 46 | 47 | |query: Option, conn: Conn| { 48 | let query = query.unwrap_or_else(|| Query { count: 20 }); 49 | blocking_section(move || { 50 | use crate::schema::posts::dsl::*; 51 | use diesel::prelude::*; 52 | posts 53 | .limit(query.count) 54 | .load::(&*conn) 55 | .map(output::Json) 56 | .map_err(finchers::error::fail) 57 | }) 58 | } 59 | }); 60 | 61 | let create_post = path!(@post /) 62 | .and(endpoints::body::json()) 63 | .and(acquire_conn.clone()) 64 | .and_then(|new_post: NewPost, conn: Conn| { 65 | blocking_section(move || { 66 | use diesel::prelude::*; 67 | diesel::insert_into(crate::schema::posts::table) 68 | .values(&new_post) 69 | .get_result::(&*conn) 70 | .map(output::Json) 71 | .map(output::status::Created) 72 | .map_err(finchers::error::fail) 73 | }) 74 | }); 75 | 76 | let find_post = 77 | path!(@get / i32 /) 78 | .and(acquire_conn.clone()) 79 | .and_then(|id: i32, conn: Conn| { 80 | blocking_section(move || { 81 | use crate::schema::posts::dsl; 82 | use diesel::prelude::*; 83 | dsl::posts 84 | .filter(dsl::id.eq(id)) 85 | .get_result::(&*conn) 86 | .optional() 87 | .map_err(finchers::error::fail) 88 | .and_then(|conn_opt| { 89 | conn_opt.ok_or_else(|| { 90 | finchers::error::err_msg(StatusCode::NOT_FOUND, "not found") 91 | }) 92 | }) 93 | .map(output::Json) 94 | }) 95 | }); 96 | 97 | let endpoint = path!(/"api"/"v1"/"posts").and(routes! { 98 | get_posts, 99 | create_post, 100 | find_post, 101 | }); 102 | 103 | finchers::server::start(endpoint).serve("127.0.0.1:4000")?; 104 | Ok(()) 105 | } 106 | -------------------------------------------------------------------------------- /examples/diesel/src/model.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::posts; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Queryable, Serialize)] 5 | pub struct Post { 6 | pub id: i32, 7 | pub title: String, 8 | pub body: String, 9 | pub published: bool, 10 | } 11 | 12 | #[derive(Debug, Insertable, Deserialize)] 13 | #[table_name = "posts"] 14 | pub struct NewPost { 15 | pub title: String, 16 | pub body: String, 17 | } 18 | -------------------------------------------------------------------------------- /examples/diesel/src/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | posts (id) { 3 | id -> Int4, 4 | title -> Varchar, 5 | body -> Text, 6 | published -> Bool, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/juniper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-juniper" 3 | version = "0.0.0" 4 | edition = "2018" 5 | authors = ["Yusuke Sasaki "] 6 | publish = false 7 | 8 | [[bin]] 9 | name = "example_juniper" 10 | path = "src/main.rs" 11 | doc = false 12 | 13 | [dependencies] 14 | finchers = "0.13" 15 | finchers-juniper = "0.2" 16 | juniper = "0.10" 17 | log = "0.4.5" 18 | pretty_env_logger = "0.2.4" 19 | -------------------------------------------------------------------------------- /examples/juniper/src/main.rs: -------------------------------------------------------------------------------- 1 | mod schema; 2 | 3 | use finchers::prelude::*; 4 | use finchers::server::middleware::log::stdlog; 5 | 6 | use crate::schema::{create_schema, Context}; 7 | 8 | fn main() { 9 | let schema = create_schema(); 10 | 11 | let acquire_context = endpoint::unit().map(|| Context::default()); 12 | 13 | let graphql_endpoint = finchers::path!(/ "graphql") 14 | .and(acquire_context) 15 | .wrap(finchers_juniper::execute::nonblocking(schema)); 16 | 17 | let index_page = finchers::path!(@get /).and(finchers_juniper::graphiql_source("/graphql")); 18 | 19 | let endpoint = index_page.or(graphql_endpoint); 20 | 21 | std::env::set_var("RUST_LOG", "info"); 22 | pretty_env_logger::init(); 23 | 24 | log::info!("Listening on http://127.0.0.1:4000"); 25 | finchers::server::start(endpoint) 26 | .with_middleware(stdlog(log::Level::Info, module_path!())) 27 | .serve("127.0.0.1:4000") 28 | .unwrap_or_else(|e| log::error!("{}", e)); 29 | } 30 | -------------------------------------------------------------------------------- /examples/juniper/src/schema.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default)] 2 | pub struct Context { 3 | _priv: (), 4 | } 5 | 6 | impl juniper::Context for Context {} 7 | 8 | pub struct Query { 9 | _priv: (), 10 | } 11 | 12 | juniper::graphql_object!(Query: Context |&self| { 13 | field apiVersion() -> &str { 14 | "1.0" 15 | } 16 | }); 17 | 18 | pub type Schema = juniper::RootNode<'static, Query, juniper::EmptyMutation>; 19 | 20 | pub fn create_schema() -> Schema { 21 | Schema::new(Query { _priv: () }, juniper::EmptyMutation::new()) 22 | } 23 | -------------------------------------------------------------------------------- /examples/jwt-auth/.gitignore: -------------------------------------------------------------------------------- 1 | .secret-key 2 | -------------------------------------------------------------------------------- /examples/jwt-auth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-jwt-auth" 3 | version = "0.0.0" 4 | authors = ["Yusuke Sasaki "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [[bin]] 9 | name = "example_jwt_auth" 10 | path = "src/main.rs" 11 | doc = false 12 | 13 | [dependencies] 14 | finchers = "0.13" 15 | http = "0.1.13" 16 | jsonwebtoken = "5" 17 | log = "0.4" 18 | pretty_env_logger = "0.2" 19 | serde = { version = "1", features = ["derive"] } 20 | time = "0.1" 21 | cookie = "0.11.0" 22 | either = "1.5.0" 23 | -------------------------------------------------------------------------------- /examples/jwt-auth/src/main.rs: -------------------------------------------------------------------------------- 1 | use finchers::error; 2 | use finchers::input::Cookies; 3 | use finchers::output::Redirect; 4 | use finchers::prelude::*; 5 | use finchers::server::middleware::log::stdlog; 6 | 7 | use cookie::Cookie; 8 | use either::Either; 9 | use http::{Response, StatusCode}; 10 | use jsonwebtoken::TokenData; 11 | use serde::{Deserialize, Serialize}; 12 | use std::net::SocketAddr; 13 | 14 | const SECRET_KEY: &[u8] = b"this-is-a-very-very-secret-key"; 15 | 16 | #[derive(Debug, Deserialize, Serialize)] 17 | struct Claims { 18 | user_id: i32, 19 | iat: i64, 20 | exp: i64, 21 | nbf: i64, 22 | } 23 | 24 | fn parse_token(token_str: &str) -> Result, jsonwebtoken::errors::Error> { 25 | jsonwebtoken::decode(token_str, SECRET_KEY, &Default::default()) 26 | } 27 | 28 | fn generate_token() -> Result { 29 | let now = time::now_utc().to_timespec(); 30 | let claims = Claims { 31 | user_id: 0, 32 | iat: now.sec, 33 | nbf: now.sec, 34 | exp: (now + time::Duration::days(1)).sec, 35 | }; 36 | jsonwebtoken::encode(&Default::default(), &claims, SECRET_KEY) 37 | } 38 | 39 | fn html(body: T) -> Response { 40 | Response::builder() 41 | .header("content-type", "text/html; charset=utf-8") 42 | .body(body) 43 | .expect("should be a valid response") 44 | } 45 | 46 | fn main() { 47 | let login = { 48 | #[derive(Debug, Deserialize)] 49 | struct FormData { 50 | username: String, 51 | password: String, 52 | } 53 | finchers::path!(@post / "login" /) 54 | .and(endpoints::body::urlencoded()) 55 | .and(endpoints::cookie::cookies()) 56 | .and_then(|form: FormData, mut cookies: Cookies| { 57 | if form.username == "user1" && form.password == "user1" { 58 | let token = generate_token().map_err(error::fail)?; 59 | cookies.add(Cookie::new("token", token)); 60 | Ok(Redirect::found("/")) 61 | } else { 62 | Err(error::err_msg(StatusCode::UNAUTHORIZED, "invalid user")) 63 | } 64 | }) 65 | }; 66 | 67 | let login_page = finchers::path!(@get / "login" /).map(|| { 68 | const FORM_HTML: &str = "
\n 69 | \n 70 | \n 71 | \n 72 |
"; 73 | html(FORM_HTML) 74 | }); 75 | 76 | let logout = finchers::path!(@get / "logout" /) 77 | .and(endpoints::cookie::cookies()) 78 | .map(|mut cookies: Cookies| { 79 | cookies.remove(Cookie::named("token")); 80 | Redirect::see_other("/login") 81 | }); 82 | 83 | let index = finchers::path!(@get /) 84 | .and(endpoints::cookie::cookies()) 85 | .and_then(|cookies: Cookies| match cookies.get("token") { 86 | Some(cookie) => { 87 | let token = parse_token(cookie.value()).map_err(error::bad_request)?; 88 | Ok(Either::Left(html(format!( 89 | "

logged in (used_id = {})

", 90 | token.claims.user_id 91 | )))) 92 | } 93 | None => Ok(Either::Right(Redirect::see_other("/login"))), 94 | }); 95 | 96 | let endpoint = index.or(login_page).or(login).or(logout); 97 | 98 | std::env::set_var("RUST_LOG", "info"); 99 | pretty_env_logger::init(); 100 | 101 | let addr: SocketAddr = ([127, 0, 0, 1], 4000).into(); 102 | 103 | log::info!("Listening on {}", addr); 104 | finchers::server::start(endpoint) 105 | .with_middleware(stdlog(log::Level::Info, module_path!())) 106 | .serve("127.0.0.1:4000") 107 | .unwrap_or_else(|e| log::error!("{}", e)); 108 | } 109 | -------------------------------------------------------------------------------- /examples/middlewares/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-middlewares" 3 | version = "0.0.0" 4 | authors = ["Yusuke Sasaki "] 5 | edition = "2018" 6 | 7 | [[bin]] 8 | name = "example_middlewares" 9 | path = "src/main.rs" 10 | doc = false 11 | 12 | [dependencies] 13 | finchers = { version = "0.13", features = ["tower-web"] } 14 | tower-service = "0.1" 15 | tower-web = { version = "0.3", default-features = false } 16 | http = "0.1.13" 17 | -------------------------------------------------------------------------------- /examples/middlewares/src/main.rs: -------------------------------------------------------------------------------- 1 | use finchers::output::body::optional; 2 | use finchers::prelude::*; 3 | use finchers::server::middleware::map_response_body; 4 | 5 | use http::Method; 6 | use tower_web::middleware::cors::{AllowedOrigins, CorsBuilder}; 7 | use tower_web::middleware::log::LogMiddleware; 8 | 9 | fn main() { 10 | let endpoint = endpoint::cloned("Hello, world!"); 11 | 12 | let log_middleware = LogMiddleware::new(module_path!()); 13 | let cors_middleware = CorsBuilder::new() 14 | .allow_origins(AllowedOrigins::Any { allow_null: true }) 15 | .allow_methods(&[Method::GET]) 16 | .build(); 17 | 18 | println!("Listening on http://127.0.0.1:4000"); 19 | finchers::server::start(endpoint) 20 | .with_tower_middleware(log_middleware) 21 | .with_tower_middleware(cors_middleware) 22 | .with_middleware(map_response_body(optional)) 23 | .serve("127.0.0.1:4000") 24 | .expect("failed to start the server"); 25 | } 26 | -------------------------------------------------------------------------------- /examples/session-redis/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-session-redis" 3 | version = "0.0.0" 4 | edition = "2018" 5 | authors = ["Yusuke Sasaki "] 6 | publish = false 7 | 8 | [[bin]] 9 | name = "example_session_redis" 10 | path = "src/main.rs" 11 | doc = false 12 | 13 | [dependencies] 14 | finchers = "0.13" 15 | finchers-session = { version = "0.2", features = ["redis"] } 16 | 17 | failure = "0.1.2" 18 | http = "0.1.13" 19 | log = "0.4.5" 20 | pretty_env_logger = "0.2.4" 21 | redis = "0.9" 22 | serde = { version = "1.0.79", features = ["derive"] } 23 | serde_json = "1.0.31" 24 | -------------------------------------------------------------------------------- /examples/session-redis/src/main.rs: -------------------------------------------------------------------------------- 1 | use finchers::prelude::*; 2 | use finchers::{path, routes}; 3 | 4 | use finchers_session::redis::RedisBackend; 5 | use finchers_session::Session; 6 | 7 | use failure::Fallible; 8 | use http::{Response, StatusCode}; 9 | use redis::Client; 10 | use serde::{Deserialize, Serialize}; 11 | use std::sync::Arc; 12 | use std::time::Duration; 13 | 14 | #[derive(Debug, Deserialize, Serialize)] 15 | struct Login { 16 | username: String, 17 | } 18 | 19 | fn main() -> Fallible<()> { 20 | pretty_env_logger::init(); 21 | 22 | let client = Client::open("redis://127.0.0.1/")?; 23 | let backend = RedisBackend::new(client) 24 | .key_prefix("my-app-name") 25 | .cookie_name("my-session-id") 26 | .timeout(Duration::from_secs(60 * 3)); 27 | let session = Arc::new(backend); 28 | 29 | let greet = path!(@get /) 30 | .and(session.clone()) 31 | .and_then(|session: Session<_>| { 32 | session.with( 33 | |session| match session.get().map(serde_json::from_str::) { 34 | Some(Ok(login)) => Ok(html(format!( 35 | "Hello, {}!
\n\ 36 |
\n\ 37 | \n\ 38 |
\ 39 | ", 40 | login.username 41 | ))), 42 | _ => Ok(Response::builder() 43 | .status(StatusCode::UNAUTHORIZED) 44 | .header("content-type", "text/html; charset=utf-8") 45 | .body("Log in".into()) 46 | .unwrap()), 47 | }, 48 | ) 49 | }); 50 | 51 | let login = path!(@get /"login"/) 52 | .and(session.clone()) 53 | .and_then(|session: Session<_>| { 54 | session.with( 55 | |session| match session.get().map(serde_json::from_str::) { 56 | Some(Ok(_login)) => Ok(redirect_to("/").map(|_| "")), 57 | _ => Ok(html( 58 | "login form\n\ 59 |
\n\ 60 | \n\ 61 | \n\ 62 |
", 63 | )), 64 | }, 65 | ) 66 | }); 67 | 68 | let login_post = { 69 | #[derive(Debug, Deserialize)] 70 | struct Form { 71 | username: String, 72 | } 73 | 74 | path!(@post /"login"/) 75 | .and(session.clone()) 76 | .and(endpoints::body::urlencoded()) 77 | .and_then(|session: Session<_>, form: Form| { 78 | session.with(|session| { 79 | let value = serde_json::to_string(&Login { 80 | username: form.username, 81 | }) 82 | .map_err(finchers::error::fail)?; 83 | session.set(value); 84 | Ok(redirect_to("/")) 85 | }) 86 | }) 87 | }; 88 | 89 | let logout = path!(@post /"logout"/) 90 | .and(session.clone()) 91 | .and_then(|session: Session<_>| { 92 | session.with(|session| { 93 | session.remove(); 94 | Ok(redirect_to("/")) 95 | }) 96 | }); 97 | 98 | let endpoint = endpoint::EndpointObj::new(routes![greet, login, login_post, logout,]); 99 | 100 | log::info!("Listening on http://127.0.0.1:4000"); 101 | finchers::server::start(endpoint).serve("127.0.0.1:4000")?; 102 | 103 | Ok(()) 104 | } 105 | 106 | fn redirect_to(location: &str) -> Response<()> { 107 | Response::builder() 108 | .status(StatusCode::FOUND) 109 | .header("location", location) 110 | .body(()) 111 | .unwrap() 112 | } 113 | 114 | fn html(body: T) -> Response { 115 | Response::builder() 116 | .header("content-type", "text/html; charset=utf-8") 117 | .body(body) 118 | .unwrap() 119 | } 120 | -------------------------------------------------------------------------------- /examples/staticfiles/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-staticfiles" 3 | version = "0.0.0" 4 | edition = "2018" 5 | authors = ["Yusuke Sasaki "] 6 | publish = false 7 | 8 | [[bin]] 9 | name = "example_staticfiles" 10 | path = "src/main.rs" 11 | doc = false 12 | 13 | [dependencies] 14 | finchers = "0.13" 15 | -------------------------------------------------------------------------------- /examples/staticfiles/src/main.rs: -------------------------------------------------------------------------------- 1 | use finchers::prelude::*; 2 | use finchers::{path, routes}; 3 | 4 | fn main() { 5 | let endpoint = routes![ 6 | path!(@get /).and(endpoints::fs::file("./Cargo.toml")), 7 | path!(@get / "public").and(endpoints::fs::dir("./static")), 8 | endpoint::syntax::verb::get().map(|| "Not found"), 9 | ]; 10 | 11 | finchers::server::start(endpoint) 12 | .serve("127.0.0.1:5000") 13 | .unwrap_or_else(|e| eprintln!("{}", e)); 14 | } 15 | -------------------------------------------------------------------------------- /examples/staticfiles/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Index 7 | 8 | 9 | 10 |

Finchers

11 |

12 | Finchers is a Rust Web framework focuses on asynchronous and type-safe handling. 13 |

14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/template-askama/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-template-askama" 3 | version = "0.0.0" 4 | authors = ["Yusuke Sasaki "] 5 | publish = false 6 | 7 | build = "build.rs" 8 | 9 | [[bin]] 10 | name = "example_template_askama" 11 | path = "src/main.rs" 12 | doc = false 13 | 14 | [dependencies] 15 | finchers = "0.13" 16 | finchers-template = { version = "0.2.0-dev", features = ["use-askama"] } 17 | askama = "0.7" 18 | pretty_env_logger = "0.2.4" 19 | log = "0.4.5" 20 | 21 | [build-dependencies] 22 | askama = "0.7" 23 | -------------------------------------------------------------------------------- /examples/template-askama/build.rs: -------------------------------------------------------------------------------- 1 | extern crate askama; 2 | 3 | fn main() { 4 | askama::rerun_if_templates_changed(); 5 | } 6 | -------------------------------------------------------------------------------- /examples/template-askama/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate askama; 3 | #[macro_use] 4 | extern crate finchers; 5 | extern crate finchers_template; 6 | #[macro_use] 7 | extern crate log; 8 | extern crate pretty_env_logger; 9 | 10 | use finchers::prelude::*; 11 | 12 | use askama::Template; 13 | 14 | #[derive(Debug, Template)] 15 | #[template(path = "index.html")] 16 | struct UserInfo { 17 | name: String, 18 | } 19 | 20 | fn main() { 21 | std::env::set_var("RUST_LOG", "example_askama=info"); 22 | pretty_env_logger::init(); 23 | 24 | let endpoint = path!(@get /) 25 | .map(|| UserInfo { 26 | name: "Alice".into(), 27 | }) 28 | .wrap(finchers_template::askama()); 29 | 30 | info!("Listening on http://127.0.0.1:4000"); 31 | finchers::server::start(endpoint) 32 | .serve("127.0.0.1:4000") 33 | .unwrap_or_else(|e| error!("{}", e)); 34 | } 35 | -------------------------------------------------------------------------------- /examples/template-askama/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Greeting 6 | 7 | 8 | Hello, {{ name }}. 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/template-handlebars/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-template-handlebars" 3 | version = "0.0.0" 4 | authors = ["Yusuke Sasaki "] 5 | publish = false 6 | 7 | [[bin]] 8 | name = "example_template_handlebars" 9 | path = "src/main.rs" 10 | doc = false 11 | 12 | [dependencies] 13 | finchers = "0.13" 14 | finchers-template = { version = "0.2.0-dev", features = ["use-handlebars"] } 15 | handlebars = "1" 16 | pretty_env_logger = "0.2.4" 17 | log = "0.4.5" 18 | serde = "1" 19 | -------------------------------------------------------------------------------- /examples/template-handlebars/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate finchers; 3 | extern crate finchers_template; 4 | #[macro_use] 5 | extern crate log; 6 | extern crate pretty_env_logger; 7 | #[macro_use] 8 | extern crate serde; 9 | extern crate handlebars; 10 | 11 | use finchers::prelude::*; 12 | 13 | use handlebars::Handlebars; 14 | 15 | #[derive(Debug, Serialize)] 16 | struct UserInfo { 17 | name: String, 18 | } 19 | 20 | impl UserInfo { 21 | const TEMPLATE_NAME: &'static str = "index.html"; 22 | 23 | const TEMPLATE_STR: &'static str = "\ 24 | 25 | 26 | 27 | 28 | Greeting 29 | 30 | 31 | Hello, {{ name }}. 32 | 33 | "; 34 | } 35 | 36 | fn main() { 37 | pretty_env_logger::init(); 38 | 39 | let mut engine = Handlebars::new(); 40 | engine 41 | .register_template_string(UserInfo::TEMPLATE_NAME, UserInfo::TEMPLATE_STR) 42 | .unwrap(); 43 | 44 | let endpoint = path!(@get /) 45 | .map(|| UserInfo { 46 | name: "Alice".into(), 47 | }) 48 | .wrap(finchers_template::handlebars( 49 | engine, 50 | UserInfo::TEMPLATE_NAME, 51 | )); 52 | 53 | info!("Listening on http://127.0.0.1:4000"); 54 | finchers::server::start(endpoint) 55 | .serve("127.0.0.1:4000") 56 | .unwrap_or_else(|e| error!("{}", e)); 57 | } 58 | -------------------------------------------------------------------------------- /examples/template-horrorshow/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-template-horrorshow" 3 | version = "0.0.0" 4 | authors = ["Yusuke Sasaki "] 5 | publish = false 6 | 7 | [[bin]] 8 | name = "example_template_horrorshow" 9 | path = "src/main.rs" 10 | doc = false 11 | 12 | [dependencies] 13 | finchers = "0.13" 14 | finchers-template = { version = "0.2.0-dev", features = ["use-horrorshow"] } 15 | horrorshow = "0.6" 16 | pretty_env_logger = "0.2.4" 17 | log = "0.4.5" 18 | -------------------------------------------------------------------------------- /examples/template-horrorshow/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate finchers; 3 | extern crate finchers_template; 4 | #[macro_use] 5 | extern crate log; 6 | extern crate pretty_env_logger; 7 | #[macro_use] 8 | extern crate horrorshow; 9 | 10 | use finchers::prelude::*; 11 | 12 | use horrorshow::helper::doctype; 13 | 14 | fn main() { 15 | std::env::set_var("RUST_LOG", "horrorshow=info"); 16 | pretty_env_logger::init(); 17 | 18 | let endpoint = path!(@get /) 19 | .map(|| { 20 | html! { 21 | : doctype::HTML; 22 | html { 23 | head { 24 | meta(charset="utf-8"); 25 | title: "Greeting"; 26 | } 27 | body { 28 | p: format!("Hello, {}", "Alice"); 29 | } 30 | } 31 | } 32 | }) 33 | .wrap(finchers_template::horrorshow()); 34 | 35 | info!("Listening on http://127.0.0.1:4000"); 36 | finchers::server::start(endpoint) 37 | .serve("127.0.0.1:4000") 38 | .unwrap_or_else(|e| error!("{}", e)); 39 | } 40 | -------------------------------------------------------------------------------- /examples/template-tera/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-template-tera" 3 | version = "0.0.0" 4 | authors = ["Yusuke Sasaki "] 5 | publish = false 6 | 7 | [[bin]] 8 | name = "example_template_tera" 9 | path = "src/main.rs" 10 | doc = false 11 | 12 | [dependencies] 13 | finchers = "0.13" 14 | finchers-template = { version = "0.2.0-dev", features = ["use-tera"] } 15 | tera = "0.11" 16 | pretty_env_logger = "0.2.4" 17 | log = "0.4.5" 18 | serde = "1" 19 | -------------------------------------------------------------------------------- /examples/template-tera/src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate finchers; 3 | extern crate finchers_template; 4 | #[macro_use] 5 | extern crate log; 6 | extern crate pretty_env_logger; 7 | #[macro_use] 8 | extern crate serde; 9 | extern crate tera; 10 | 11 | use finchers::prelude::*; 12 | 13 | use tera::Tera; 14 | 15 | #[derive(Debug, Serialize)] 16 | struct UserInfo { 17 | name: String, 18 | } 19 | 20 | impl UserInfo { 21 | const TEMPLATE_NAME: &'static str = "index.html"; 22 | 23 | const TEMPLATE_STR: &'static str = "\ 24 | 25 | 26 | 27 | 28 | Greeting 29 | 30 | 31 | Hello, {{ name }}. 32 | 33 | "; 34 | } 35 | 36 | fn main() { 37 | pretty_env_logger::init(); 38 | 39 | let mut engine = Tera::default(); 40 | engine 41 | .add_raw_template(UserInfo::TEMPLATE_NAME, UserInfo::TEMPLATE_STR) 42 | .unwrap(); 43 | 44 | let endpoint = { 45 | path!(@get /) 46 | .map(|| UserInfo { 47 | name: "Alice".into(), 48 | }) 49 | .wrap(finchers_template::tera(engine, UserInfo::TEMPLATE_NAME)) 50 | }; 51 | 52 | info!("Listening on http://127.0.0.1:4000"); 53 | finchers::server::start(endpoint) 54 | .serve("127.0.0.1:4000") 55 | .unwrap_or_else(|e| error!("{}", e)); 56 | } 57 | -------------------------------------------------------------------------------- /examples/websocket/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-websocket" 3 | version = "0.0.0" 4 | authors = ["Yusuke Sasaki "] 5 | edition = "2018" 6 | 7 | [[bin]] 8 | name = "example_websocket" 9 | path = "src/main.rs" 10 | doc = false 11 | 12 | [dependencies] 13 | finchers = "0.13" 14 | finchers-tungstenite = "0.2" 15 | 16 | futures = "0.1.24" 17 | http = "0.1.13" 18 | pretty_env_logger = "0.2.4" 19 | log = "0.4.5" 20 | tungstenite = "0.6.0" 21 | -------------------------------------------------------------------------------- /examples/websocket/src/main.rs: -------------------------------------------------------------------------------- 1 | use finchers::prelude::*; 2 | use futures::prelude::*; 3 | use http::Response; 4 | 5 | use finchers_tungstenite::{ws, Ws, WsTransport}; 6 | use tungstenite::error::Error as WsError; 7 | use tungstenite::Message; 8 | 9 | fn on_upgrade(stream: WsTransport) -> impl Future { 10 | let (tx, rx) = stream.split(); 11 | rx.filter_map(|m| { 12 | log::info!("Message from client: {:?}", m); 13 | match m { 14 | Message::Ping(p) => Some(Message::Pong(p)), 15 | Message::Pong(..) => None, 16 | m => Some(m), 17 | } 18 | }) 19 | .forward(tx) 20 | .map(|_| ()) 21 | .map_err(|e| match e { 22 | WsError::ConnectionClosed(..) => log::info!("connection is closed"), 23 | e => log::error!("error during handling WebSocket connection: {}", e), 24 | }) 25 | } 26 | 27 | fn main() { 28 | pretty_env_logger::init(); 29 | 30 | let index = finchers::path!(/).map(|| { 31 | Response::builder() 32 | .header("content-type", "text/html; charset=utf-8") 33 | .body( 34 | r#" 35 | 36 | 37 | 38 | Index 39 | 40 | 41 | 42 | 43 | "#, 44 | ) 45 | .unwrap() 46 | }); 47 | 48 | let ws_endpoint = finchers::path!(/ "ws" /).and(ws()).map(|ws: Ws| { 49 | log::info!("accepted a WebSocket request"); 50 | ws.on_upgrade(on_upgrade) 51 | }); 52 | 53 | let endpoint = index.or(ws_endpoint); 54 | 55 | log::info!("Listening on http://127.0.0.1:4000"); 56 | finchers::server::start(endpoint) 57 | .serve("127.0.0.1:4000") 58 | .unwrap_or_else(|e| log::error!("{}", e)); 59 | } 60 | -------------------------------------------------------------------------------- /finchers-juniper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "finchers-juniper" 3 | version = "0.2.1" 4 | authors = ["Yusuke Sasaki "] 5 | description = """ 6 | A set of extensions for supporting Juniper integration. 7 | """ 8 | license = "MIT OR Apache-2.0" 9 | readme = "README.md" 10 | repository = "https://github.com/finchers-rs/finchers-juniper.git" 11 | 12 | include = [ 13 | "Cargo.toml", 14 | "build.rs", 15 | "src/**/*", 16 | "tests/**/*", 17 | "examples/**/*", 18 | "benches/**/*", 19 | "LICENSE-MIT", 20 | "LICENSE-APACHE", 21 | "README.md" 22 | ] 23 | 24 | [package.metadata.docs.rs] 25 | # FIXME: remove it as soon as the rustc version used in docs.rs is updated 26 | rustdoc-args = ["--cfg", "finchers_inject_extern_prelude"] 27 | 28 | [dependencies] 29 | finchers = "0.13" 30 | 31 | bytes = "0.4.9" 32 | failure = { version = "0.1.2", features = ["derive"] } 33 | futures = "0.1.24" 34 | http = "0.1.10" 35 | juniper = "0.10.0" 36 | log = "0.4.5" 37 | percent-encoding = "1.0.1" 38 | serde = { version = "1.0.75", features = ["derive"] } 39 | serde_json = "1.0.26" 40 | serde_qs = "0.4.1" 41 | 42 | [dev-dependencies] 43 | pretty_env_logger = "0.2.4" 44 | juniper = { version = "0.10.0", features = ["expose-test-schema", "serde_json"] } 45 | futures-cpupool = "0.1.8" 46 | matches = "0.1.8" 47 | cargo-husky = "1.0.1" 48 | -------------------------------------------------------------------------------- /finchers-juniper/README.md: -------------------------------------------------------------------------------- 1 | # `finchers-juniper` 2 | 3 | [![crates.io](https://img.shields.io/crates/v/finchers-juniper.svg)](https://crates.io/crates/finchers-juniper) 4 | [![Docs.rs](https://docs.rs/finchers-juniper/badge.svg)](https://docs.rs/finchers-juniper) 5 | [![dependency status](https://deps.rs/crate/finchers-juniper/0.2.1/status.svg)](https://deps.rs/crate/finchers-juniper/0.2.1) 6 | [![Build Status](https://travis-ci.org/finchers-rs/finchers-juniper.svg?branch=master)](https://travis-ci.org/finchers-rs/finchers-juniper) 7 | [![Coverage Status](https://coveralls.io/repos/github/finchers-rs/finchers-juniper/badge.svg?branch=master)](https://coveralls.io/github/finchers-rs/finchers-juniper?branch=master) 8 | 9 | A set of extensions for integrating [Juniper] endpoints. 10 | 11 | [Juniper]: https://github.com/graphql-rust/juniper 12 | 13 | ## License 14 | 15 | [MIT license](../LICENSE-MIT) or [Apache License, Version 2.0](../LICENSE-APACHE) at your option. 16 | -------------------------------------------------------------------------------- /finchers-juniper/changelog.md: -------------------------------------------------------------------------------- 1 | 2 | ### 0.2.1 (2018-10-15) 3 | 4 | * fix badge URL in README.md 5 | 6 | 7 | ## 0.2.0 (2018-10-09) 8 | 9 | The initial release on this iteration. 10 | 11 | * bump `finchers` to `0.13` 12 | * introduce the trait `Schema` and `SharedSchema` for abstraction of `RootNode` 13 | - The advantages in here are as follows: 14 | + Simplify the trait bounds in GraphQL executors 15 | + `Box`, `Rc` and `Arc` implements `Schema` 16 | 17 | 18 | ### 0.1.1 (2018-10-02) 19 | * update metadata in Cargo.toml 20 | 21 | 22 | ## 0.1.0 (2018-09-30) 23 | The initial release on this iteration. 24 | -------------------------------------------------------------------------------- /finchers-juniper/examples/executors.rs: -------------------------------------------------------------------------------- 1 | extern crate finchers; 2 | extern crate finchers_juniper; 3 | extern crate futures; // 0.1 4 | extern crate futures_cpupool; 5 | #[macro_use] 6 | extern crate juniper; 7 | #[macro_use] 8 | extern crate log; 9 | extern crate pretty_env_logger; 10 | 11 | use finchers::endpoint::syntax; 12 | use finchers::prelude::*; 13 | use finchers_juniper::execute; 14 | 15 | use futures_cpupool::CpuPool; 16 | use juniper::{EmptyMutation, RootNode}; 17 | use std::sync::Arc; 18 | 19 | struct MyContext { 20 | _priv: (), 21 | } 22 | 23 | impl juniper::Context for MyContext {} 24 | 25 | struct Query; 26 | 27 | graphql_object!(Query: MyContext |&self| { 28 | field apiVersion() -> &str { 29 | "1.0" 30 | } 31 | }); 32 | 33 | fn main() { 34 | pretty_env_logger::init(); 35 | 36 | let schema = Arc::new(RootNode::new(Query, EmptyMutation::::new())); 37 | 38 | let current_thread_endpoint = syntax::segment("current") 39 | .map(|| MyContext { _priv: () }) 40 | .wrap(execute::current_thread(schema.clone())); 41 | 42 | let nonblocking_endpoint = syntax::segment("nonblocking") 43 | .map(|| MyContext { _priv: () }) 44 | .wrap(execute::nonblocking(schema.clone())); 45 | 46 | let cpupool_endpoint = syntax::segment("cpupool") 47 | .map(|| MyContext { _priv: () }) 48 | .wrap(execute::with_spawner( 49 | schema.clone(), 50 | CpuPool::new_num_cpus(), 51 | )); 52 | 53 | let endpoint = current_thread_endpoint 54 | .or(nonblocking_endpoint) 55 | .or(cpupool_endpoint); 56 | 57 | info!("Listening on http://127.0.0.1:4000/"); 58 | finchers::server::start(endpoint) 59 | .serve("127.0.0.1:4000") 60 | .unwrap_or_else(|err| error!("{}", err)); 61 | } 62 | -------------------------------------------------------------------------------- /finchers-juniper/examples/todos.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate failure; 3 | #[macro_use] 4 | extern crate finchers; 5 | extern crate finchers_juniper; 6 | #[macro_use] 7 | extern crate juniper; 8 | #[macro_use] 9 | extern crate log; 10 | extern crate pretty_env_logger; 11 | 12 | use finchers::prelude::*; 13 | 14 | use failure::Fallible; 15 | use std::sync::Arc; 16 | 17 | use business::Repository; 18 | use graphql::{create_schema, Context}; 19 | 20 | fn main() -> Fallible<()> { 21 | pretty_env_logger::try_init()?; 22 | 23 | let repository = Arc::new(Repository::init()); 24 | let context_endpoint = endpoint::cloned(repository).map(|repository| Context { repository }); 25 | 26 | let graphql_endpoint = path!(/ "graphql" /) 27 | .and(context_endpoint) 28 | .wrap(finchers_juniper::execute::nonblocking(create_schema())); 29 | 30 | let graphiql_endpoint = path!(@get /).and(finchers_juniper::graphiql_source("/graphql")); 31 | 32 | let endpoint = graphql_endpoint.or(graphiql_endpoint); 33 | 34 | info!("Listening on http://127.0.0.1:4000"); 35 | finchers::server::start(endpoint) 36 | .serve("127.0.0.1:4000") 37 | .map_err(Into::into) 38 | } 39 | 40 | /// The implelentation of business logic. 41 | mod business { 42 | use failure::Fallible; 43 | use std::collections::HashMap; 44 | use std::sync::RwLock; 45 | 46 | #[derive(Debug, Clone)] 47 | pub struct Todo { 48 | pub id: i32, 49 | pub title: String, 50 | pub text: String, 51 | pub published: bool, 52 | } 53 | 54 | #[derive(Debug)] 55 | pub struct Repository(RwLock); 56 | 57 | #[derive(Debug)] 58 | struct Inner { 59 | todos: HashMap, 60 | counter: i32, 61 | } 62 | 63 | impl Repository { 64 | pub fn init() -> Repository { 65 | Repository(RwLock::new(Inner { 66 | todos: HashMap::new(), 67 | counter: 0, 68 | })) 69 | } 70 | 71 | pub fn all_todos(&self) -> Fallible> { 72 | let inner = self.0.read().map_err(|e| format_err!("{}", e))?; 73 | Ok(inner.todos.values().cloned().collect()) 74 | } 75 | 76 | pub fn find_todo_by_id(&self, id: i32) -> Fallible> { 77 | let inner = self.0.read().map_err(|e| format_err!("{}", e))?; 78 | Ok(inner.todos.get(&id).cloned()) 79 | } 80 | 81 | pub fn create_todo(&self, title: String, text: String) -> Fallible { 82 | let mut inner = self.0.write().map_err(|e| format_err!("{}", e))?; 83 | 84 | let new_todo = Todo { 85 | id: inner.counter, 86 | title, 87 | text, 88 | published: false, 89 | }; 90 | inner.counter = inner 91 | .counter 92 | .checked_add(1) 93 | .ok_or_else(|| format_err!("overflow detected"))?; 94 | inner.todos.insert(new_todo.id, new_todo.clone()); 95 | 96 | Ok(new_todo) 97 | } 98 | } 99 | } 100 | 101 | /// The definition of GraphQL schema and resolvers. 102 | mod graphql { 103 | use juniper; 104 | use juniper::{FieldResult, RootNode}; 105 | use std::sync::Arc; 106 | 107 | use business::Repository; 108 | 109 | #[derive(Debug)] 110 | pub struct Context { 111 | pub repository: Arc, 112 | } 113 | 114 | impl juniper::Context for Context {} 115 | 116 | #[derive(Debug)] 117 | #[repr(transparent)] 118 | pub struct Todo(::business::Todo); 119 | 120 | graphql_object!(Todo: () |&self| { 121 | field id() -> i32 { self.0.id } 122 | field title() -> &String { &self.0.title } 123 | field text() -> &String { &self.0.text } 124 | field published() -> bool { self.0.published } 125 | }); 126 | 127 | pub struct Query; 128 | 129 | graphql_object!(Query: Context |&self| { 130 | field apiVersion() -> &str { 131 | "1.0" 132 | } 133 | 134 | field todos(&executor) -> FieldResult> { 135 | Ok(executor.context() 136 | .repository 137 | .all_todos()? 138 | .into_iter() 139 | .map(Todo) 140 | .collect()) 141 | } 142 | 143 | field todo(&executor, id: i32) -> FieldResult> { 144 | Ok(executor.context() 145 | .repository 146 | .find_todo_by_id(id)? 147 | .map(Todo)) 148 | } 149 | }); 150 | 151 | pub struct Mutation; 152 | 153 | graphql_object!(Mutation: Context |&self| { 154 | field create_todo(&executor, title: String, text: String) -> FieldResult { 155 | executor.context() 156 | .repository 157 | .create_todo(title, text) 158 | .map(Todo) 159 | .map_err(Into::into) 160 | } 161 | }); 162 | 163 | pub type Schema = RootNode<'static, Query, Mutation>; 164 | 165 | pub fn create_schema() -> Schema { 166 | Schema::new(Query, Mutation) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /finchers-juniper/release.toml: -------------------------------------------------------------------------------- 1 | tag-prefix = "v" 2 | disable-push = true 3 | no-dev-version = true 4 | pre-release-commit-message = "(cargo-release) bump version to {{version}}" 5 | tag-message = "(cargo-release) version {{version}}" 6 | 7 | pre-release-hook = ["cargo", "publish", "--dry-run"] 8 | 9 | [[pre-release-replacements]] 10 | file = "README.md" 11 | search = "https://deps.rs/crate/finchers-juniper/[a-z0-9\\.-]+" 12 | replace = "https://deps.rs/crate/finchers-juniper/{{version}}" 13 | 14 | [[pre-release-replacements]] 15 | file = "README.md" 16 | search = "finchers-juniper = \"[a-z0-9\\.-]+\"" 17 | replace = "finchers-juniper = \"{{version}}\"" 18 | 19 | [[pre-release-replacements]] 20 | file = "src/lib.rs" 21 | search = "https://docs.rs/finchers-juniper/[a-z0-9\\.-]+" 22 | replace = "https://docs.rs/finchers-juniper/{{version}}" 23 | -------------------------------------------------------------------------------- /finchers-juniper/src/execute/current_thread.rs: -------------------------------------------------------------------------------- 1 | use finchers::endpoint; 2 | use finchers::endpoint::wrapper::Wrapper; 3 | use finchers::endpoint::{ApplyContext, ApplyResult, Endpoint, IntoEndpoint}; 4 | use finchers::error::Error; 5 | 6 | use futures::future; 7 | use futures::{Future, Poll}; 8 | 9 | use juniper::{GraphQLType, RootNode}; 10 | use std::fmt; 11 | 12 | use super::Schema; 13 | use request::{GraphQLRequestEndpoint, GraphQLResponse, RequestFuture}; 14 | 15 | /// Create a GraphQL executor from the specified `RootNode`. 16 | /// 17 | /// The endpoint created by this executor will execute the GraphQL queries 18 | /// on the current thread. 19 | pub fn current_thread(schema: S) -> CurrentThread 20 | where 21 | S: Schema, 22 | { 23 | CurrentThread { schema } 24 | } 25 | 26 | #[allow(missing_docs)] 27 | #[derive(Debug)] 28 | pub struct CurrentThread { 29 | schema: S, 30 | } 31 | 32 | impl<'a, S> IntoEndpoint<'a> for CurrentThread 33 | where 34 | S: Schema + 'a, 35 | { 36 | type Output = (GraphQLResponse,); 37 | type Endpoint = CurrentThreadEndpoint, S>; 38 | 39 | fn into_endpoint(self) -> Self::Endpoint { 40 | CurrentThreadEndpoint { 41 | context: endpoint::cloned(()), 42 | request: ::request::graphql_request(), 43 | schema: self.schema, 44 | } 45 | } 46 | } 47 | 48 | impl<'a, E, CtxT, S> Wrapper<'a, E> for CurrentThread 49 | where 50 | E: Endpoint<'a, Output = (CtxT,)>, 51 | S: Schema + 'a, 52 | CtxT: 'a, 53 | { 54 | type Output = (GraphQLResponse,); 55 | type Endpoint = CurrentThreadEndpoint; 56 | 57 | fn wrap(self, endpoint: E) -> Self::Endpoint { 58 | CurrentThreadEndpoint { 59 | context: endpoint, 60 | request: ::request::graphql_request(), 61 | schema: self.schema, 62 | } 63 | } 64 | } 65 | 66 | #[derive(Debug)] 67 | pub struct CurrentThreadEndpoint { 68 | context: E, 69 | request: GraphQLRequestEndpoint, 70 | schema: S, 71 | } 72 | 73 | impl<'a, E, S, CtxT> Endpoint<'a> for CurrentThreadEndpoint 74 | where 75 | E: Endpoint<'a, Output = (CtxT,)>, 76 | S: Schema + 'a, 77 | CtxT: 'a, 78 | { 79 | type Output = (GraphQLResponse,); 80 | type Future = CurrentThreadFuture<'a, E, S::Query, S::Mutation, CtxT>; 81 | 82 | fn apply(&'a self, cx: &mut ApplyContext<'_>) -> ApplyResult { 83 | let context = self.context.apply(cx)?; 84 | let request = self.request.apply(cx)?; 85 | Ok(CurrentThreadFuture { 86 | inner: context.join(request), 87 | root_node: self.schema.as_root_node(), 88 | }) 89 | } 90 | } 91 | 92 | pub struct CurrentThreadFuture<'a, E, QueryT, MutationT, CtxT> 93 | where 94 | E: Endpoint<'a, Output = (CtxT,)>, 95 | QueryT: GraphQLType + 'a, 96 | MutationT: GraphQLType + 'a, 97 | CtxT: 'a, 98 | { 99 | inner: future::Join>, 100 | root_node: &'a RootNode<'static, QueryT, MutationT>, 101 | } 102 | 103 | impl<'a, E, QueryT, MutationT, CtxT> fmt::Debug 104 | for CurrentThreadFuture<'a, E, QueryT, MutationT, CtxT> 105 | where 106 | E: Endpoint<'a, Output = (CtxT,)>, 107 | QueryT: GraphQLType + 'a, 108 | MutationT: GraphQLType + 'a, 109 | CtxT: 'a, 110 | { 111 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 112 | f.debug_struct("CurrentThreadFuture").finish() 113 | } 114 | } 115 | 116 | impl<'a, E, QueryT, MutationT, CtxT> Future for CurrentThreadFuture<'a, E, QueryT, MutationT, CtxT> 117 | where 118 | E: Endpoint<'a, Output = (CtxT,)>, 119 | QueryT: GraphQLType, 120 | MutationT: GraphQLType, 121 | CtxT: 'a, 122 | { 123 | type Item = (GraphQLResponse,); 124 | type Error = Error; 125 | 126 | fn poll(&mut self) -> Poll { 127 | let ((context,), (request,)) = try_ready!(self.inner.poll()); 128 | let response = request.execute(self.root_node, &context); 129 | Ok((response,).into()) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /finchers-juniper/src/execute/nonblocking.rs: -------------------------------------------------------------------------------- 1 | use finchers::endpoint; 2 | use finchers::endpoint::wrapper::Wrapper; 3 | use finchers::endpoint::{ApplyContext, ApplyResult, Endpoint, IntoEndpoint}; 4 | use finchers::error::Error; 5 | use finchers::rt; 6 | 7 | use futures::future; 8 | use futures::{Future, Poll}; 9 | use std::sync::Arc; 10 | 11 | use super::shared::SharedSchema; 12 | use request::{GraphQLRequestEndpoint, GraphQLResponse, RequestFuture}; 13 | 14 | /// Create a GraphQL executor from the specified `RootNode`. 15 | /// 16 | /// The endpoint created by this wrapper will spawn a task which executes the GraphQL queries 17 | /// after receiving the request, by using tokio's `DefaultExecutor`, and notify the start of 18 | /// the blocking section by using tokio_threadpool's blocking API. 19 | pub fn nonblocking(schema: S) -> Nonblocking 20 | where 21 | S: SharedSchema, 22 | { 23 | Nonblocking { schema } 24 | } 25 | 26 | #[allow(missing_docs)] 27 | #[derive(Debug)] 28 | pub struct Nonblocking { 29 | schema: S, 30 | } 31 | 32 | impl<'a, S> IntoEndpoint<'a> for Nonblocking 33 | where 34 | S: SharedSchema, 35 | { 36 | type Output = (GraphQLResponse,); 37 | type Endpoint = NonblockingEndpoint, S>; 38 | 39 | fn into_endpoint(self) -> Self::Endpoint { 40 | NonblockingEndpoint { 41 | context: endpoint::cloned(()), 42 | request: ::request::graphql_request(), 43 | schema: Arc::new(self.schema), 44 | } 45 | } 46 | } 47 | 48 | impl<'a, E, S> Wrapper<'a, E> for Nonblocking 49 | where 50 | E: Endpoint<'a, Output = (S::Context,)>, 51 | S: SharedSchema, 52 | { 53 | type Output = (GraphQLResponse,); 54 | type Endpoint = NonblockingEndpoint; 55 | 56 | fn wrap(self, endpoint: E) -> Self::Endpoint { 57 | NonblockingEndpoint { 58 | context: endpoint, 59 | request: ::request::graphql_request(), 60 | schema: Arc::new(self.schema), 61 | } 62 | } 63 | } 64 | 65 | #[derive(Debug)] 66 | pub struct NonblockingEndpoint { 67 | context: E, 68 | request: GraphQLRequestEndpoint, 69 | schema: Arc, 70 | } 71 | 72 | impl<'a, E, S> Endpoint<'a> for NonblockingEndpoint 73 | where 74 | E: Endpoint<'a, Output = (S::Context,)>, 75 | S: SharedSchema, 76 | { 77 | type Output = (GraphQLResponse,); 78 | type Future = NonblockingFuture<'a, E, S>; 79 | 80 | fn apply(&'a self, cx: &mut ApplyContext<'_>) -> ApplyResult { 81 | let context = self.context.apply(cx)?; 82 | let request = self.request.apply(cx)?; 83 | Ok(NonblockingFuture { 84 | inner: context.join(request), 85 | handle: None, 86 | endpoint: self, 87 | }) 88 | } 89 | } 90 | 91 | #[allow(missing_debug_implementations)] 92 | pub struct NonblockingFuture<'a, E: Endpoint<'a>, S: 'a> { 93 | inner: future::Join>, 94 | handle: Option>, 95 | endpoint: &'a NonblockingEndpoint, 96 | } 97 | 98 | impl<'a, E, S> Future for NonblockingFuture<'a, E, S> 99 | where 100 | E: Endpoint<'a, Output = (S::Context,)>, 101 | S: SharedSchema, 102 | { 103 | type Item = (GraphQLResponse,); 104 | type Error = Error; 105 | 106 | fn poll(&mut self) -> Poll { 107 | loop { 108 | match self.handle { 109 | Some(ref mut handle) => return handle.poll().map(|x| x.map(|response| (response,))), 110 | None => { 111 | let ((context,), (request,)) = try_ready!(self.inner.poll()); 112 | 113 | trace!("spawn a GraphQL task using the default executor"); 114 | let schema = self.endpoint.schema.clone(); 115 | let future = rt::blocking_section(move || -> ::finchers::error::Result<_> { 116 | Ok(request.execute(schema.as_root_node(), &context)) 117 | }); 118 | self.handle = Some(rt::spawn_with_handle(future)); 119 | } 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /finchers-juniper/src/graphiql.rs: -------------------------------------------------------------------------------- 1 | //! Endpoint for serving GraphiQL source. 2 | 3 | use finchers::endpoint::{ApplyContext, ApplyResult, Endpoint}; 4 | use finchers::error::Error; 5 | 6 | use futures::{Future, Poll}; 7 | 8 | use bytes::Bytes; 9 | use http::{header, Response}; 10 | use juniper; 11 | 12 | /// Creates an endpoint which returns a generated GraphiQL interface. 13 | pub fn graphiql_source(endpoint_url: impl AsRef) -> GraphiQLSource { 14 | GraphiQLSource { 15 | source: juniper::http::graphiql::graphiql_source(endpoint_url.as_ref()).into(), 16 | } 17 | } 18 | 19 | #[allow(missing_docs)] 20 | #[derive(Debug)] 21 | pub struct GraphiQLSource { 22 | source: Bytes, 23 | } 24 | 25 | impl GraphiQLSource { 26 | /// Regenerate the GraphiQL interface with the specified endpoint URL. 27 | pub fn regenerate(&mut self, endpoint_url: impl AsRef) { 28 | self.source = juniper::http::graphiql::graphiql_source(endpoint_url.as_ref()).into(); 29 | } 30 | } 31 | 32 | impl<'a> Endpoint<'a> for GraphiQLSource { 33 | type Output = (Response,); 34 | type Future = GraphiQLFuture<'a>; 35 | 36 | fn apply(&'a self, _: &mut ApplyContext<'_>) -> ApplyResult { 37 | Ok(GraphiQLFuture(&self.source)) 38 | } 39 | } 40 | 41 | #[doc(hidden)] 42 | #[derive(Debug)] 43 | pub struct GraphiQLFuture<'a>(&'a Bytes); 44 | 45 | impl<'a> Future for GraphiQLFuture<'a> { 46 | type Item = (Response,); 47 | type Error = Error; 48 | 49 | fn poll(&mut self) -> Poll { 50 | Ok((Response::builder() 51 | .header(header::CONTENT_TYPE, "text/html; charset=utf-8") 52 | .body(self.0.clone()) 53 | .expect("should be a valid response"),) 54 | .into()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /finchers-juniper/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A set of extensions for supporting Juniper integration. 2 | //! 3 | //! # Examples 4 | //! 5 | //! ``` 6 | //! #[macro_use] 7 | //! extern crate finchers; 8 | //! # use finchers::prelude::*; 9 | //! extern crate finchers_juniper; 10 | //! #[macro_use] 11 | //! extern crate juniper; 12 | //! 13 | //! use juniper::{EmptyMutation, RootNode}; 14 | //! 15 | //! // The contextual information used when GraphQL query executes. 16 | //! // 17 | //! // Typically it contains a connection retrieved from pool 18 | //! // or credential information extracted from HTTP headers. 19 | //! struct MyContext { 20 | //! // ... 21 | //! } 22 | //! impl juniper::Context for MyContext {} 23 | //! 24 | //! struct Query {} 25 | //! graphql_object!(Query: MyContext |&self| { 26 | //! field apiVersion() -> &str { "1.0" } 27 | //! // ... 28 | //! }); 29 | //! 30 | //! # fn main() { 31 | //! let schema = RootNode::new( 32 | //! Query {}, 33 | //! EmptyMutation::::new(), 34 | //! ); 35 | //! 36 | //! // An endpoint which acquires a GraphQL context from request. 37 | //! let fetch_graphql_context = 38 | //! endpoint::unit().map(|| MyContext { /* ... */ }); 39 | //! 40 | //! // Build an endpoint which handles GraphQL requests. 41 | //! let endpoint = path!(@get / "graphql" /) 42 | //! .and(fetch_graphql_context) 43 | //! .wrap(finchers_juniper::execute::nonblocking(schema)); 44 | //! # drop(move || { 45 | //! # finchers::server::start(endpoint).serve("127.0.0.1:4000") 46 | //! # }); 47 | //! # } 48 | //! ``` 49 | 50 | #![doc(html_root_url = "https://docs.rs/finchers-juniper/0.2.1")] 51 | #![warn( 52 | missing_docs, 53 | missing_debug_implementations, 54 | nonstandard_style, 55 | rust_2018_idioms, 56 | rust_2018_compatibility, 57 | unused 58 | )] 59 | 60 | extern crate bytes; 61 | extern crate failure; 62 | extern crate finchers; 63 | #[macro_use] 64 | extern crate futures; 65 | extern crate juniper; 66 | #[macro_use] 67 | extern crate log; 68 | extern crate percent_encoding; 69 | #[macro_use] 70 | extern crate serde; 71 | extern crate http; 72 | extern crate serde_json; 73 | extern crate serde_qs; 74 | 75 | #[cfg(test)] 76 | #[macro_use] 77 | extern crate matches; 78 | 79 | pub mod execute; 80 | pub mod graphiql; 81 | pub mod request; 82 | 83 | pub use graphiql::graphiql_source; 84 | pub use request::graphql_request; 85 | -------------------------------------------------------------------------------- /finchers-juniper/tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | extern crate finchers; 2 | extern crate finchers_juniper; 3 | extern crate juniper; 4 | #[macro_use] 5 | extern crate percent_encoding; 6 | extern crate http; 7 | 8 | use finchers::endpoint::syntax; 9 | use finchers::prelude::*; 10 | use finchers::test; 11 | use finchers::test::{TestResult, TestRunner}; 12 | use finchers_juniper::request::{GraphQLRequest, GraphQLResponse}; 13 | 14 | use juniper::http::tests as http_tests; 15 | use juniper::tests::model::Database; 16 | use juniper::{EmptyMutation, RootNode}; 17 | 18 | use http::{Request, Response}; 19 | use percent_encoding::{utf8_percent_encode, QUERY_ENCODE_SET}; 20 | use std::cell::RefCell; 21 | 22 | type Schema = RootNode<'static, Database, EmptyMutation>; 23 | 24 | struct TestFinchersIntegration { 25 | runner: RefCell>, 26 | } 27 | 28 | impl http_tests::HTTPIntegration for TestFinchersIntegration 29 | where 30 | for<'e> E: Endpoint<'e, Output = (GraphQLResponse,)>, 31 | { 32 | fn get(&self, url: &str) -> http_tests::TestResponse { 33 | let response = self 34 | .runner 35 | .borrow_mut() 36 | .perform(Request::get(custom_url_encode(url))) 37 | .unwrap(); 38 | make_test_response(response) 39 | } 40 | 41 | fn post(&self, url: &str, body: &str) -> http_tests::TestResponse { 42 | let response = self 43 | .runner 44 | .borrow_mut() 45 | .perform( 46 | Request::post(custom_url_encode(url)) 47 | .header("content-type", "application/json") 48 | .body(body.to_owned()), 49 | ) 50 | .unwrap(); 51 | make_test_response(response) 52 | } 53 | } 54 | 55 | fn custom_url_encode(url: &str) -> String { 56 | define_encode_set! { 57 | pub CUSTOM_ENCODE_SET = [QUERY_ENCODE_SET] | {'{', '}'} 58 | } 59 | utf8_percent_encode(url, CUSTOM_ENCODE_SET).to_string() 60 | } 61 | 62 | fn make_test_response(response: Response) -> http_tests::TestResponse { 63 | let status_code = response.status().as_u16() as i32; 64 | let content_type = response 65 | .headers() 66 | .get("content-type") 67 | .expect("No content type header from endpoint") 68 | .to_str() 69 | .expect("failed to convert the header value to string") 70 | .to_owned(); 71 | let body = response.body().to_utf8().unwrap().into_owned(); 72 | http_tests::TestResponse { 73 | status_code, 74 | content_type, 75 | body: Some(body), 76 | } 77 | } 78 | 79 | #[test] 80 | fn test_finchers_integration() { 81 | let database = Database::new(); 82 | let schema = Schema::new(Database::new(), EmptyMutation::::new()); 83 | let endpoint = syntax::eos() 84 | .and(finchers_juniper::graphql_request()) 85 | .and(endpoint::by_ref(database)) 86 | .and(endpoint::by_ref(schema)) 87 | .and_then( 88 | |req: GraphQLRequest, db: &Database, schema: &Schema| Ok(req.execute(schema, db)), 89 | ); 90 | let integration = TestFinchersIntegration { 91 | runner: RefCell::new(test::runner(endpoint)), 92 | }; 93 | http_tests::run_http_test_suite(&integration); 94 | } 95 | -------------------------------------------------------------------------------- /finchers-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "finchers-macros" 3 | version = "0.14.0-dev" 4 | authors = ["Yusuke Sasaki "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [lib] 9 | proc-macro = true 10 | 11 | [dependencies] 12 | proc-macro2 = "0.4" 13 | syn = { version = "0.15", features = ["full", "extra-traits"] } 14 | quote = "0.6" 15 | -------------------------------------------------------------------------------- /finchers-session/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "finchers-session" 3 | version = "0.2.1-dev" 4 | authors = ["Yusuke Sasaki "] 5 | description = """ 6 | Session support for Finchers. 7 | """ 8 | license = "MIT OR Apache-2.0" 9 | readme = "README.md" 10 | repository = "https://github.com/finchers-rs/finchers-session.git" 11 | 12 | include = [ 13 | "Cargo.toml", 14 | "build.rs", 15 | "src/**/*", 16 | "tests/**/*", 17 | "examples/**/*", 18 | "benches/**/*", 19 | "LICENSE-MIT", 20 | "LICENSE-APACHE", 21 | ] 22 | 23 | [package.metadata.docs.rs] 24 | features = [ 25 | "secure", 26 | "redis", 27 | ] 28 | rustdoc-args = [ 29 | # FIXME: remove it as soon as the rustc version used in docs.rs is updated 30 | "--cfg", "finchers_inject_extern_prelude", 31 | ] 32 | 33 | [features] 34 | default = ["secure"] 35 | secure = ["cookie/secure", "finchers/secure"] 36 | 37 | [dependencies] 38 | finchers = { version = "0.13", default-features = false } 39 | 40 | cookie = "0.11.0" 41 | failure = "0.1.2" 42 | futures = "0.1.24" 43 | http = "0.1.13" 44 | time = "0.1.40" 45 | uuid = { version = "0.7.1", features = ["serde", "v4"] } 46 | 47 | redis = { version = "0.9.1", optional = true } 48 | 49 | [dev-dependencies] 50 | pretty_env_logger = "0.2.4" 51 | log = "0.4.5" 52 | serde = { version = "1.0.79", features = ["derive"] } 53 | serde_json = "1.0.30" 54 | -------------------------------------------------------------------------------- /finchers-session/README.md: -------------------------------------------------------------------------------- 1 | # `finchers-session` 2 | 3 | [![crates.io](https://img.shields.io/crates/v/finchers-session.svg)](https://crates.io/crates/finchers-session) 4 | [![Docs.rs](https://docs.rs/finchers-session/badge.svg)](https://docs.rs/finchers-session) 5 | [![dependency status](https://deps.rs/crate/finchers-session/0.2.0/status.svg)](https://deps.rs/crate/finchers-session/0.2.0) 6 | [![Build Status](https://travis-ci.org/finchers-rs/finchers-session.svg?branch=master)](https://travis-ci.org/finchers-rs/finchers-session) 7 | [![Coverage Status](https://coveralls.io/repos/github/finchers-rs/finchers-session/badge.svg?branch=master)](https://coveralls.io/github/finchers-rs/finchers-session?branch=master) 8 | 9 | Session support for Finchers. 10 | 11 | ## Supported Backends 12 | 13 | * In-memory storage 14 | * Cookie 15 | * Redis (requires the feature flag `feature = "redis"`) 16 | 17 | # License 18 | [MIT license](../LICENSE-MIT) or [Apache License, Version 2.0](../LICENSE-APACHE) at your option. 19 | -------------------------------------------------------------------------------- /finchers-session/examples/simple.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate finchers; 3 | extern crate finchers_session; 4 | extern crate http; 5 | #[macro_use] 6 | extern crate log; 7 | extern crate pretty_env_logger; 8 | #[macro_use] 9 | extern crate serde; 10 | extern crate serde_json; 11 | 12 | use finchers::prelude::*; 13 | use finchers_session::in_memory::{InMemoryBackend, InMemorySession}; 14 | 15 | use http::Response; 16 | 17 | type Session = finchers_session::Session; 18 | 19 | #[derive(Debug, Deserialize, Serialize, Default)] 20 | struct SessionValue { 21 | text: String, 22 | } 23 | 24 | fn main() { 25 | pretty_env_logger::init(); 26 | 27 | let session_endpoint = InMemoryBackend::default(); 28 | 29 | let endpoint = path!(@get /) 30 | .and(session_endpoint) 31 | .and_then(|session: Session| { 32 | session.with(|session| { 33 | // Retrieve the value of session. 34 | // 35 | // Note that the session value are stored as a UTF-8 string, 36 | // which means that the user it is necessary for the user to 37 | // deserialize/serialize the session data. 38 | let mut session_value: SessionValue = { 39 | let s = session.get().unwrap_or(r#"{ "text": "" }"#); 40 | serde_json::from_str(s).map_err(|err| { 41 | finchers::error::bad_request(format!( 42 | "failed to parse session value (input = {:?}): {}", 43 | s, err 44 | )) 45 | })? 46 | }; 47 | 48 | let response = Response::builder() 49 | .header("content-type", "text/html; charset=utf-8") 50 | .body(format!("{:?}", session_value)) 51 | .expect("should be a valid response"); 52 | 53 | session_value.text += "a"; 54 | 55 | // Stores session data to the store. 56 | let s = serde_json::to_string(&session_value).map_err(finchers::error::fail)?; 57 | session.set(s); 58 | 59 | Ok(response) 60 | }) 61 | }); 62 | 63 | info!("Listening on http://127.0.0.1:4000"); 64 | finchers::server::start(endpoint) 65 | .serve("127.0.0.1:4000") 66 | .unwrap_or_else(|err| error!("{}", err)); 67 | } 68 | -------------------------------------------------------------------------------- /finchers-session/release.toml: -------------------------------------------------------------------------------- 1 | tag-prefix = "v" 2 | tag-message = "release {{version}}" 3 | dev-version-ext = "dev" 4 | pre-release-commit-message = "bump version to {{version}}" 5 | 6 | [[pre-release-replacements]] 7 | file = "README.md" 8 | search = "https://deps.rs/crate/finchers-session/[a-z0-9\\.-]+" 9 | replace = "https://deps.rs/crate/finchers-session/{{version}}" 10 | 11 | [[pre-release-replacements]] 12 | file = "src/lib.rs" 13 | search = "https://docs.rs/finchers-session/[a-z0-9\\.-]+" 14 | replace = "https://docs.rs/finchers-session/{{version}}" 15 | -------------------------------------------------------------------------------- /finchers-session/src/in_memory.rs: -------------------------------------------------------------------------------- 1 | //! The session backend using in-memory database. 2 | //! 3 | //! # Example 4 | //! 5 | //! ``` 6 | //! #[macro_use] 7 | //! extern crate finchers; 8 | //! extern crate finchers_session; 9 | //! 10 | //! use finchers::prelude::*; 11 | //! use finchers_session::Session; 12 | //! use finchers_session::in_memory::{ 13 | //! InMemoryBackend, 14 | //! InMemorySession, 15 | //! }; 16 | //! 17 | //! # fn main() { 18 | //! let backend = InMemoryBackend::default(); 19 | //! 20 | //! let endpoint = path!(@get /) 21 | //! .and(backend) 22 | //! .and_then(|session: Session| { 23 | //! session.with(|_session| { 24 | //! // ... 25 | //! # Ok("done") 26 | //! }) 27 | //! }); 28 | //! # drop(move || finchers::server::start(endpoint).serve("127.0.0.1:4000")); 29 | //! # } 30 | //! ``` 31 | 32 | extern crate cookie; 33 | 34 | use std::collections::HashMap; 35 | use std::sync::{Arc, RwLock}; 36 | 37 | use finchers; 38 | use finchers::endpoint::{ApplyContext, ApplyResult, Endpoint}; 39 | use finchers::error::Error; 40 | use finchers::input::Input; 41 | 42 | use self::cookie::Cookie; 43 | use futures::future; 44 | use uuid::Uuid; 45 | 46 | use session::{RawSession, Session}; 47 | 48 | #[derive(Debug, Default)] 49 | struct Storage { 50 | inner: RwLock>, 51 | } 52 | 53 | impl Storage { 54 | fn get(&self, session_id: &Uuid) -> Result, Error> { 55 | let inner = self.inner.read().map_err(|e| format_err!("{}", e))?; 56 | Ok(inner.get(&session_id).cloned()) 57 | } 58 | 59 | fn set(&self, session_id: Uuid, value: String) -> Result<(), Error> { 60 | let mut inner = self.inner.write().map_err(|e| format_err!("{}", e))?; 61 | inner.insert(session_id, value); 62 | Ok(()) 63 | } 64 | 65 | fn remove(&self, session_id: &Uuid) -> Result<(), Error> { 66 | let mut inner = self.inner.write().map_err(|e| format_err!("{}", e))?; 67 | inner.remove(&session_id); 68 | Ok(()) 69 | } 70 | } 71 | 72 | #[allow(missing_docs)] 73 | #[derive(Debug, Clone, Default)] 74 | pub struct InMemoryBackend { 75 | inner: Arc, 76 | } 77 | 78 | #[derive(Debug, Default)] 79 | struct Inner { 80 | storage: Storage, 81 | } 82 | 83 | impl InMemoryBackend { 84 | fn read_value(&self, input: &mut Input) -> Result<(Option, Option), Error> { 85 | match input.cookies()?.get("session-id") { 86 | Some(cookie) => { 87 | let session_id: Uuid = cookie 88 | .value() 89 | .parse() 90 | .map_err(finchers::error::bad_request)?; 91 | let value = self.inner.storage.get(&session_id)?; 92 | Ok((value, Some(session_id))) 93 | } 94 | None => Ok((None, None)), 95 | } 96 | } 97 | 98 | fn write_value(&self, input: &mut Input, session_id: Uuid, value: String) -> Result<(), Error> { 99 | self.inner.storage.set(session_id.clone(), value)?; 100 | input 101 | .cookies()? 102 | .add(Cookie::new("session-id", session_id.to_string())); 103 | Ok(()) 104 | } 105 | 106 | fn remove_value(&self, input: &mut Input, session_id: Uuid) -> Result<(), Error> { 107 | self.inner.storage.remove(&session_id)?; 108 | input.cookies()?.remove(Cookie::named("session-id")); 109 | Ok(()) 110 | } 111 | } 112 | 113 | impl<'a> Endpoint<'a> for InMemoryBackend { 114 | type Output = (Session,); 115 | type Future = future::FutureResult; 116 | 117 | fn apply(&self, cx: &mut ApplyContext<'_>) -> ApplyResult { 118 | Ok(future::result(self.read_value(cx.input()).map( 119 | |(value, session_id)| { 120 | (Session::new(InMemorySession { 121 | backend: self.clone(), 122 | value, 123 | session_id, 124 | }),) 125 | }, 126 | ))) 127 | } 128 | } 129 | 130 | #[allow(missing_docs)] 131 | #[derive(Debug)] 132 | pub struct InMemorySession { 133 | backend: InMemoryBackend, 134 | session_id: Option, 135 | value: Option, 136 | } 137 | 138 | impl InMemorySession { 139 | fn write_impl(self, input: &mut Input) -> Result<(), Error> { 140 | match self.value { 141 | Some(value) => { 142 | let session_id = self.session_id.unwrap_or_else(Uuid::new_v4); 143 | self.backend.write_value(input, session_id, value) 144 | } 145 | None => match self.session_id { 146 | Some(session_id) => self.backend.remove_value(input, session_id), 147 | None => Ok(()), 148 | }, 149 | } 150 | } 151 | } 152 | 153 | impl RawSession for InMemorySession { 154 | type WriteFuture = future::FutureResult<(), Error>; 155 | 156 | fn get(&self) -> Option<&str> { 157 | self.value.as_ref().map(|s| s.as_ref()) 158 | } 159 | 160 | fn set(&mut self, value: String) { 161 | self.value = Some(value); 162 | } 163 | 164 | fn remove(&mut self) { 165 | self.value = None; 166 | } 167 | 168 | fn write(self, input: &mut Input) -> Self::WriteFuture { 169 | future::result(self.write_impl(input)) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /finchers-session/src/lib.rs: -------------------------------------------------------------------------------- 1 | // FIXME: remove it as soon as the rustc version used in docs.rs is updated 2 | #![cfg_attr(finchers_inject_extern_prelude, feature(extern_prelude))] 3 | 4 | //! Session support for Finchers. 5 | //! 6 | //! Supported backends: 7 | //! 8 | //! * Cookie 9 | //! * In-memory database 10 | //! * Redis (requires the feature flag `feature = "redis"`) 11 | //! 12 | //! # Feature Flags 13 | //! 14 | //! * `redis` - enable Redis backend (default: off) 15 | //! * `secure` - enable signing and encryption support for Cookie values 16 | //! (default: on. it adds the crate `ring` to dependencies). 17 | 18 | #![doc(html_root_url = "https://docs.rs/finchers-session/0.2.0")] 19 | #![warn( 20 | missing_docs, 21 | missing_debug_implementations, 22 | nonstandard_style, 23 | rust_2018_idioms, 24 | rust_2018_compatibility, 25 | unused 26 | )] 27 | 28 | #[macro_use] 29 | extern crate failure; 30 | extern crate finchers; 31 | #[cfg_attr(feature = "redis", macro_use)] 32 | extern crate futures; 33 | extern crate time; 34 | extern crate uuid; 35 | 36 | #[cfg(test)] 37 | extern crate http; 38 | 39 | mod session; 40 | #[cfg(test)] 41 | mod tests; 42 | mod util; 43 | 44 | pub mod cookie; 45 | pub mod in_memory; 46 | #[cfg(feature = "redis")] 47 | pub mod redis; 48 | 49 | pub use self::session::{RawSession, Session}; 50 | -------------------------------------------------------------------------------- /finchers-session/src/session.rs: -------------------------------------------------------------------------------- 1 | use finchers::endpoint; 2 | use finchers::error::Error; 3 | use finchers::input::Input; 4 | 5 | use futures::{Future, IntoFuture, Poll}; 6 | 7 | /// The trait representing the backend to manage session value. 8 | #[allow(missing_docs)] 9 | pub trait RawSession { 10 | type WriteFuture: Future; 11 | 12 | fn get(&self) -> Option<&str>; 13 | fn set(&mut self, value: String); 14 | fn remove(&mut self); 15 | fn write(self, input: &mut Input) -> Self::WriteFuture; 16 | } 17 | 18 | /// A struct which manages the session value per request. 19 | #[derive(Debug)] 20 | #[must_use = "The value must be convert into a Future to finish the session handling."] 21 | pub struct Session { 22 | raw: S, 23 | } 24 | 25 | impl Session 26 | where 27 | S: RawSession, 28 | { 29 | #[allow(missing_docs)] 30 | pub fn new(raw: S) -> Session { 31 | Session { raw } 32 | } 33 | 34 | /// Get the session value if available. 35 | pub fn get(&self) -> Option<&str> { 36 | self.raw.get() 37 | } 38 | 39 | /// Set the session value. 40 | pub fn set(&mut self, value: impl Into) { 41 | self.raw.set(value.into()); 42 | } 43 | 44 | /// Annotates to remove session value to the backend. 45 | pub fn remove(&mut self) { 46 | self.raw.remove(); 47 | } 48 | 49 | #[allow(missing_docs)] 50 | pub fn with( 51 | mut self, 52 | f: impl FnOnce(&mut Self) -> R, 53 | ) -> impl Future 54 | where 55 | R: IntoFuture, 56 | { 57 | f(&mut self) 58 | .into_future() 59 | .and_then(move |item| self.into_future().map(move |()| item)) 60 | } 61 | } 62 | 63 | impl IntoFuture for Session 64 | where 65 | S: RawSession, 66 | { 67 | type Item = (); 68 | type Error = Error; 69 | type Future = WriteSessionFuture; 70 | 71 | fn into_future(self) -> Self::Future { 72 | WriteSessionFuture { 73 | future: endpoint::with_get_cx(|input| self.raw.write(input)), 74 | } 75 | } 76 | } 77 | 78 | #[derive(Debug)] 79 | #[must_use = "futures do not anything unless polled."] 80 | pub struct WriteSessionFuture { 81 | future: F, 82 | } 83 | 84 | impl Future for WriteSessionFuture 85 | where 86 | F: Future, 87 | F::Error: Into, 88 | { 89 | type Item = (); 90 | type Error = Error; 91 | 92 | fn poll(&mut self) -> Poll { 93 | self.future.poll().map_err(Into::into) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /finchers-session/src/tests.rs: -------------------------------------------------------------------------------- 1 | use finchers::error::Error; 2 | use finchers::input::Input; 3 | use finchers::prelude::*; 4 | use finchers::test; 5 | 6 | use futures::future; 7 | use http::Request; 8 | 9 | use session::{RawSession, Session}; 10 | 11 | use std::cell::RefCell; 12 | use std::rc::Rc; 13 | 14 | #[derive(Debug, Clone, PartialEq)] 15 | enum Op { 16 | Get, 17 | Set(String), 18 | Remove, 19 | Write, 20 | } 21 | 22 | #[derive(Default)] 23 | struct CallChain { 24 | chain: RefCell>, 25 | } 26 | 27 | impl CallChain { 28 | fn register(&self, op: Op) { 29 | self.chain.borrow_mut().push(op); 30 | } 31 | 32 | fn result(&self) -> Vec { 33 | self.chain.borrow().clone() 34 | } 35 | } 36 | 37 | struct MockSession { 38 | call_chain: Rc, 39 | } 40 | 41 | impl RawSession for MockSession { 42 | type WriteFuture = future::FutureResult<(), Error>; 43 | 44 | fn get(&self) -> Option<&str> { 45 | self.call_chain.register(Op::Get); 46 | None 47 | } 48 | 49 | fn set(&mut self, value: String) { 50 | self.call_chain.register(Op::Set(value)); 51 | } 52 | 53 | fn remove(&mut self) { 54 | self.call_chain.register(Op::Remove); 55 | } 56 | 57 | fn write(self, _: &mut Input) -> Self::WriteFuture { 58 | self.call_chain.register(Op::Write); 59 | future::ok(()) 60 | } 61 | } 62 | 63 | #[test] 64 | fn test_session_with() { 65 | let call_chain = Rc::new(CallChain::default()); 66 | 67 | let mut runner = test::runner({ 68 | let session_endpoint = endpoint::apply({ 69 | let call_chain = call_chain.clone(); 70 | move |_cx| { 71 | Ok(Ok(Session::new(MockSession { 72 | call_chain: call_chain.clone(), 73 | }))) 74 | } 75 | }); 76 | let endpoint = session_endpoint.and_then(|session: Session| { 77 | session.with(|session| { 78 | session.get(); 79 | session.set("foo"); 80 | session.remove(); 81 | Ok("done") 82 | }) 83 | }); 84 | 85 | endpoint 86 | }); 87 | 88 | let response = runner 89 | .perform(Request::get("/").header("host", "localhost:3000")) 90 | .unwrap(); 91 | assert!(!response.headers().contains_key("set-cookie")); 92 | 93 | assert_eq!( 94 | call_chain.result(), 95 | vec![Op::Get, Op::Set("foo".into()), Op::Remove, Op::Write,] 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /finchers-session/src/util.rs: -------------------------------------------------------------------------------- 1 | pub(crate) trait BuilderExt: Sized { 2 | fn if_some(self, value: Option, f: impl FnOnce(Self, T) -> Self) -> Self { 3 | if let Some(value) = value { 4 | f(self, value) 5 | } else { 6 | self 7 | } 8 | } 9 | } 10 | 11 | impl BuilderExt for T {} 12 | -------------------------------------------------------------------------------- /finchers-template/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "finchers-template" 3 | version = "0.2.0-dev" 4 | authors = ["Yusuke Sasaki "] 5 | description = """ 6 | Template engine support for Finchers. 7 | """ 8 | license = "MIT OR Apache-2.0" 9 | readme = "README.md" 10 | repository = "https://github.com/finchers-rs/finchers-template.git" 11 | 12 | build = "build.rs" 13 | 14 | include = [ 15 | "/Cargo.toml", 16 | "/build.rs", 17 | "/src/**/*", 18 | "/tests/**/*", 19 | "/examples/**/*", 20 | "/benches/**/*", 21 | "/LICENSE-MIT", 22 | "/LICENSE-APACHE", 23 | ] 24 | 25 | [package.metadata.docs.rs] 26 | features = [ 27 | "use-tera", 28 | "use-handlebars", 29 | "use-askama", 30 | "use-horrorshow", 31 | ] 32 | # FIXME: remove it as soon as the rustc version used in docs.rs is updated 33 | rustdoc-args = ["--cfg", "finchers_inject_extern_prelude"] 34 | 35 | [dependencies] 36 | finchers = "0.13" 37 | 38 | failure = "0.1.2" 39 | futures = "0.1.24" 40 | http = "0.1.13" 41 | lazy_static = "1.1.0" 42 | mime = "0.3.9" 43 | 44 | askama = { version = "0.7", optional = true } 45 | handlebars = { version = "1", optional = true } 46 | horrorshow = { version = "0.6", optional = true } 47 | tera = { version = "0.11", optional = true } 48 | 49 | mime_guess = { version = "2.0.0-alpha.6", optional = true } 50 | serde = { version = "1.0.79", optional = true } 51 | 52 | [dev-dependencies] 53 | matches = "0.1.8" 54 | 55 | [dev-dependencies.cargo-husky] 56 | version = "1" 57 | default-features = false 58 | features = ["user-hooks"] 59 | 60 | [features] 61 | use-handlebars = ["handlebars", "mime_guess", "serde"] 62 | use-tera = ["tera", "mime_guess", "serde"] 63 | use-askama = ["askama", "mime_guess"] 64 | use-horrorshow = ["horrorshow"] 65 | -------------------------------------------------------------------------------- /finchers-template/README.md: -------------------------------------------------------------------------------- 1 | # `finchers-template` 2 | 3 | [![crates.io](https://img.shields.io/crates/v/finchers-template.svg)](https://crates.io/crates/finchers-template) 4 | [![Docs.rs](https://docs.rs/finchers-template/badge.svg)](https://docs.rs/finchers-template) 5 | [![dependency status](https://deps.rs/crate/finchers-template/0.1.1/status.svg)](https://deps.rs/crate/finchers-template/0.1.1) 6 | [![Build Status](https://travis-ci.org/finchers-rs/finchers-template.svg?branch=master)](https://travis-ci.org/finchers-rs/finchers-template) 7 | 8 | Template engine support for Finchers. 9 | 10 | # Supported Template Engines 11 | 12 | * Handlebars (https://github.com/sunng87/handlebars-rust) 13 | * Tera (https://github.com/Keats/tera) 14 | * Askama (https://github.com/djc/askama) 15 | * Horrorshow (https://github.com/Stebalien/horrorshow-rs) 16 | 17 | # License 18 | [MIT license](../LICENSE-MIT) or [Apache License, Version 2.0](../LICENSE-APACHE) at your option. 19 | -------------------------------------------------------------------------------- /finchers-template/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | if env::var_os("FINCHERS_DENY_WARNINGS").is_some() { 5 | println!("cargo:rustc-cfg=finchers_deny_warnings"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /finchers-template/changelog/v0.1.x.md: -------------------------------------------------------------------------------- 1 | 2 | ## 0.1.1 (2018-10-02) 3 | * update project metadata 4 | * rename `TemplateEndpoint` to `RenderEndpoint` 5 | * add support for horrorshow 6 | 7 | 8 | # 0.1.0 (2018-10-01) 9 | The initial release on this iteration 10 | -------------------------------------------------------------------------------- /finchers-template/release.toml: -------------------------------------------------------------------------------- 1 | tag-prefix = "v" 2 | tag-message = "release {{version}}" 3 | disable-push = true 4 | no-dev-version = true 5 | pre-release-commit-message = "bump version to {{version}}" 6 | 7 | [[pre-release-replacements]] 8 | file = "README.md" 9 | search = "https://deps.rs/crate/finchers-template/[a-z0-9\\.-]+" 10 | replace = "https://deps.rs/crate/finchers-template/{{version}}" 11 | 12 | [[pre-release-replacements]] 13 | file = "src/lib.rs" 14 | search = "https://docs.rs/finchers-template/[a-z0-9\\.-]+" 15 | replace = "https://docs.rs/finchers-template/{{version}}" 16 | -------------------------------------------------------------------------------- /finchers-template/src/backend/askama.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "use-askama")] 2 | 3 | use super::engine::{Engine, EngineImpl}; 4 | use renderer::Renderer; 5 | 6 | use askama::Template; 7 | use http::header::HeaderValue; 8 | use mime_guess::get_mime_type_str; 9 | use std::marker::PhantomData; 10 | 11 | pub fn askama() -> Renderer> { 12 | Renderer::new(AskamaEngine::default()) 13 | } 14 | 15 | #[derive(Debug)] 16 | pub struct AskamaEngine { 17 | content_type_cache: Option, 18 | _marker: PhantomData, 19 | } 20 | 21 | impl Default for AskamaEngine { 22 | fn default() -> Self { 23 | AskamaEngine { 24 | content_type_cache: None, 25 | _marker: PhantomData, 26 | } 27 | } 28 | } 29 | 30 | impl AskamaEngine { 31 | /// Precompute the value of content-type by using the given instance of context. 32 | pub fn precompute_content_type(&mut self, hint: &CtxT) { 33 | self.content_type_cache = hint.extension().and_then(|ext| { 34 | get_mime_type_str(ext) 35 | .map(|mime_str| mime_str.parse().expect("should be a valid header value")) 36 | }); 37 | } 38 | } 39 | 40 | impl Engine for AskamaEngine {} 41 | 42 | impl EngineImpl for AskamaEngine { 43 | type Body = String; 44 | type Error = ::askama::Error; 45 | 46 | // FIXME: cache parsed value 47 | fn content_type_hint(&self, value: &CtxT) -> Option { 48 | self.content_type_cache.clone().or_else(|| { 49 | let ext = value.extension()?; 50 | get_mime_type_str(ext)?.parse().ok() 51 | }) 52 | } 53 | 54 | fn render(&self, value: CtxT) -> Result { 55 | value.render() 56 | } 57 | } 58 | 59 | #[test] 60 | fn test_askama() { 61 | use askama::Error; 62 | use std::fmt; 63 | 64 | #[derive(Debug)] 65 | struct Context { 66 | name: String, 67 | } 68 | 69 | impl Template for Context { 70 | fn render_into(&self, writer: &mut dyn fmt::Write) -> Result<(), Error> { 71 | write!(writer, "{}", self.name).map_err(Into::into) 72 | } 73 | 74 | fn extension(&self) -> Option<&str> { 75 | Some("html") 76 | } 77 | } 78 | 79 | let value = Context { 80 | name: "Alice".into(), 81 | }; 82 | 83 | let engine = AskamaEngine::default(); 84 | assert_matches!( 85 | engine.content_type_hint(&value), 86 | Some(ref h) if h == "text/html" 87 | ); 88 | assert_matches!( 89 | engine.render(value), 90 | Ok(ref body) if body == "Alice" 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /finchers-template/src/backend/engine.rs: -------------------------------------------------------------------------------- 1 | use failure; 2 | use finchers::output::body::ResBody; 3 | use http::header::HeaderValue; 4 | 5 | /// A trait representing a template engine. 6 | pub trait Engine: EngineImpl {} 7 | 8 | pub trait EngineImpl { 9 | type Body: ResBody; 10 | type Error: Into; 11 | 12 | #[allow(unused_variables)] 13 | fn content_type_hint(&self, ctx: &CtxT) -> Option { 14 | None 15 | } 16 | 17 | fn render(&self, ctx: CtxT) -> Result; 18 | } 19 | -------------------------------------------------------------------------------- /finchers-template/src/backend/handlebars.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "use-handlebars")] 2 | 3 | use super::engine::{Engine, EngineImpl}; 4 | use renderer::Renderer; 5 | 6 | use failure::SyncFailure; 7 | use handlebars::Handlebars; 8 | use http::header::HeaderValue; 9 | use mime_guess::guess_mime_type_opt; 10 | use serde::Serialize; 11 | use std::borrow::Cow; 12 | 13 | pub trait AsHandlebars { 14 | fn as_handlebars(&self) -> &Handlebars; 15 | } 16 | 17 | impl AsHandlebars for Handlebars { 18 | fn as_handlebars(&self) -> &Handlebars { 19 | self 20 | } 21 | } 22 | 23 | impl AsHandlebars for Box { 24 | fn as_handlebars(&self) -> &Handlebars { 25 | (**self).as_handlebars() 26 | } 27 | } 28 | 29 | impl AsHandlebars for ::std::rc::Rc { 30 | fn as_handlebars(&self) -> &Handlebars { 31 | (**self).as_handlebars() 32 | } 33 | } 34 | 35 | impl AsHandlebars for ::std::sync::Arc { 36 | fn as_handlebars(&self) -> &Handlebars { 37 | (**self).as_handlebars() 38 | } 39 | } 40 | 41 | pub fn handlebars( 42 | registry: H, 43 | name: impl Into>, 44 | ) -> Renderer> 45 | where 46 | H: AsHandlebars, 47 | { 48 | Renderer::new(HandlebarsEngine::new(registry, name)) 49 | } 50 | 51 | #[derive(Debug)] 52 | pub struct HandlebarsEngine { 53 | registry: H, 54 | name: Cow<'static, str>, 55 | content_type: Option, 56 | } 57 | 58 | impl HandlebarsEngine 59 | where 60 | H: AsHandlebars, 61 | { 62 | pub fn new(registry: H, name: impl Into>) -> HandlebarsEngine { 63 | let name = name.into(); 64 | let content_type = guess_mime_type_opt(&*name) 65 | .map(|s| s.as_ref().parse().expect("should be a valid header value")); 66 | HandlebarsEngine { 67 | registry, 68 | name, 69 | content_type, 70 | } 71 | } 72 | 73 | pub fn set_template_name(&mut self, name: impl Into>) { 74 | self.name = name.into(); 75 | if let Some(value) = guess_mime_type_opt(&*self.name) 76 | .map(|s| s.as_ref().parse().expect("should be a valid header name")) 77 | { 78 | self.content_type = Some(value); 79 | } 80 | } 81 | } 82 | 83 | impl Engine for HandlebarsEngine where H: AsHandlebars {} 84 | 85 | impl EngineImpl for HandlebarsEngine 86 | where 87 | H: AsHandlebars, 88 | { 89 | type Body = String; 90 | type Error = SyncFailure<::handlebars::RenderError>; 91 | 92 | fn content_type_hint(&self, _: &CtxT) -> Option { 93 | self.content_type.clone() 94 | } 95 | 96 | fn render(&self, value: CtxT) -> Result { 97 | self.registry 98 | .as_handlebars() 99 | .render(&self.name, &value) 100 | .map_err(SyncFailure::new) 101 | } 102 | } 103 | 104 | #[test] 105 | fn test_handlebars() { 106 | #[derive(Debug, Serialize)] 107 | struct Context { 108 | name: String, 109 | } 110 | 111 | let mut registry = Handlebars::new(); 112 | registry 113 | .register_template_string("index.html", "{{ name }}") 114 | .unwrap(); 115 | 116 | let value = Context { 117 | name: "Alice".into(), 118 | }; 119 | 120 | let engine = HandlebarsEngine::new(registry, "index.html"); 121 | let body = engine.render(value).unwrap(); 122 | assert_eq!(body, "Alice"); 123 | } 124 | -------------------------------------------------------------------------------- /finchers-template/src/backend/horrorshow.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "use-horrorshow")] 2 | 3 | use horrorshow::Template; 4 | 5 | use super::engine::{Engine, EngineImpl}; 6 | use renderer::Renderer; 7 | 8 | pub fn horrorshow() -> Renderer { 9 | Renderer::new(HorrorshowEngine::default()) 10 | } 11 | 12 | #[derive(Debug, Default)] 13 | pub struct HorrorshowEngine { 14 | _priv: (), 15 | } 16 | 17 | impl Engine for HorrorshowEngine {} 18 | 19 | impl EngineImpl for HorrorshowEngine { 20 | type Body = String; 21 | type Error = ::horrorshow::Error; 22 | 23 | fn render(&self, value: CtxT) -> Result { 24 | value.into_string() 25 | } 26 | } 27 | 28 | #[test] 29 | fn test_horrorshow() { 30 | let value = { 31 | html! { 32 | p: "Alice"; 33 | } 34 | }; 35 | 36 | let engine = HorrorshowEngine::default(); 37 | let body = engine.render(value).unwrap(); 38 | assert_eq!(body, "

Alice

"); 39 | } 40 | -------------------------------------------------------------------------------- /finchers-template/src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | pub(crate) mod askama; 4 | pub(crate) mod engine; 5 | pub(crate) mod handlebars; 6 | pub(crate) mod horrorshow; 7 | pub(crate) mod tera; 8 | 9 | pub use self::engine::Engine; 10 | 11 | #[cfg(feature = "use-askama")] 12 | pub use self::askama::AskamaEngine; 13 | 14 | #[cfg(feature = "use-handlebars")] 15 | pub use self::handlebars::{AsHandlebars, HandlebarsEngine}; 16 | 17 | #[cfg(feature = "use-horrorshow")] 18 | pub use self::horrorshow::HorrorshowEngine; 19 | 20 | #[cfg(feature = "use-tera")] 21 | pub use self::tera::{AsTera, TeraEngine}; 22 | -------------------------------------------------------------------------------- /finchers-template/src/backend/tera.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "use-tera")] 2 | 3 | use failure::SyncFailure; 4 | use http::header::HeaderValue; 5 | use mime_guess::guess_mime_type_opt; 6 | use serde::Serialize; 7 | use std::borrow::Cow; 8 | use tera::Tera; 9 | 10 | use super::engine::{Engine, EngineImpl}; 11 | use renderer::Renderer; 12 | 13 | pub trait AsTera { 14 | fn as_tera(&self) -> &Tera; 15 | } 16 | 17 | impl AsTera for Tera { 18 | fn as_tera(&self) -> &Tera { 19 | self 20 | } 21 | } 22 | 23 | impl AsTera for Box { 24 | fn as_tera(&self) -> &Tera { 25 | (**self).as_tera() 26 | } 27 | } 28 | 29 | impl AsTera for ::std::rc::Rc { 30 | fn as_tera(&self) -> &Tera { 31 | (**self).as_tera() 32 | } 33 | } 34 | 35 | impl AsTera for ::std::sync::Arc { 36 | fn as_tera(&self) -> &Tera { 37 | (**self).as_tera() 38 | } 39 | } 40 | 41 | pub fn tera(tera: T, name: impl Into>) -> Renderer> 42 | where 43 | T: AsTera, 44 | { 45 | Renderer::new(TeraEngine::new(tera, name)) 46 | } 47 | 48 | #[derive(Debug)] 49 | pub struct TeraEngine { 50 | tera: T, 51 | name: Cow<'static, str>, 52 | content_type: Option, 53 | } 54 | 55 | impl TeraEngine 56 | where 57 | T: AsTera, 58 | { 59 | pub fn new(tera: T, name: impl Into>) -> TeraEngine { 60 | let name = name.into(); 61 | let content_type = guess_mime_type_opt(&*name).map(|mime| { 62 | mime.as_ref() 63 | .parse() 64 | .expect("should be a valid header value") 65 | }); 66 | TeraEngine { 67 | tera, 68 | name, 69 | content_type, 70 | } 71 | } 72 | 73 | pub fn set_template_name(&mut self, name: impl Into>) { 74 | self.name = name.into(); 75 | if let Some(value) = guess_mime_type_opt(&*self.name) 76 | .map(|s| s.as_ref().parse().expect("should be a valid header name")) 77 | { 78 | self.content_type = Some(value); 79 | } 80 | } 81 | } 82 | 83 | impl Engine for TeraEngine where T: AsTera {} 84 | 85 | impl EngineImpl for TeraEngine 86 | where 87 | T: AsTera, 88 | { 89 | type Body = String; 90 | type Error = SyncFailure<::tera::Error>; 91 | 92 | fn content_type_hint(&self, _: &CtxT) -> Option { 93 | self.content_type.clone() 94 | } 95 | 96 | fn render(&self, value: CtxT) -> Result { 97 | self.tera 98 | .as_tera() 99 | .render(&self.name, &value) 100 | .map_err(SyncFailure::new) 101 | } 102 | } 103 | 104 | #[test] 105 | fn test_tera() { 106 | use std::sync::Arc; 107 | 108 | #[derive(Debug, Serialize)] 109 | struct Context { 110 | name: String, 111 | } 112 | 113 | let mut tera = Tera::default(); 114 | tera.add_raw_template("index.html", "{{ name }}").unwrap(); 115 | 116 | let value = Context { 117 | name: "Alice".into(), 118 | }; 119 | 120 | let engine = TeraEngine::new(Arc::new(tera), "index.html"); 121 | let body = engine.render(value).unwrap(); 122 | assert_eq!(body, "Alice"); 123 | } 124 | -------------------------------------------------------------------------------- /finchers-template/src/lib.rs: -------------------------------------------------------------------------------- 1 | // FIXME: remove this feature gate as soon as the rustc version used in docs.rs is updated 2 | #![cfg_attr(finchers_inject_extern_prelude, feature(extern_prelude))] 3 | 4 | //! Template support for Finchers 5 | 6 | #![doc(html_root_url = "https://docs.rs/finchers-template/0.2.0-dev")] 7 | #![warn( 8 | missing_docs, 9 | missing_debug_implementations, 10 | nonstandard_style, 11 | rust_2018_idioms, 12 | unused 13 | )] 14 | //#![warn(rust_2018_compatibility)] 15 | #![cfg_attr(finchers_deny_warnings, deny(warnings))] 16 | #![cfg_attr(finchers_deny_warnings, doc(test(attr(deny(warnings)))))] 17 | 18 | extern crate failure; 19 | extern crate finchers; 20 | #[macro_use] 21 | extern crate futures; 22 | extern crate http; 23 | #[macro_use] 24 | extern crate lazy_static; 25 | extern crate mime; 26 | #[cfg(any( 27 | feature = "use-tera", 28 | feature = "use-handlebars", 29 | feature = "use-askama" 30 | ))] 31 | extern crate mime_guess; 32 | 33 | #[cfg(any(feature = "use-tera", feature = "use-handlebars"))] 34 | #[cfg_attr( 35 | all(test, any(feature = "use-handlebars", feature = "use-tera")), 36 | macro_use 37 | )] 38 | extern crate serde; 39 | 40 | #[cfg(test)] 41 | #[macro_use] 42 | extern crate matches; 43 | 44 | #[cfg(feature = "use-handlebars")] 45 | extern crate handlebars; 46 | 47 | #[cfg(feature = "use-tera")] 48 | extern crate tera; 49 | 50 | #[cfg(feature = "use-askama")] 51 | extern crate askama; 52 | 53 | #[cfg(feature = "use-horrorshow")] 54 | #[cfg_attr(test, macro_use)] 55 | extern crate horrorshow; 56 | 57 | pub mod backend; 58 | mod renderer; 59 | 60 | pub use self::renderer::Renderer; 61 | 62 | #[cfg(feature = "use-askama")] 63 | pub use self::backend::askama::askama; 64 | 65 | #[cfg(feature = "use-handlebars")] 66 | pub use self::backend::handlebars::handlebars; 67 | 68 | #[cfg(feature = "use-horrorshow")] 69 | pub use self::backend::horrorshow::horrorshow; 70 | 71 | #[cfg(feature = "use-tera")] 72 | pub use self::backend::tera::tera; 73 | -------------------------------------------------------------------------------- /finchers-tungstenite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "finchers-tungstenite" 3 | version = "0.3.0-dev" 4 | edition = "2018" 5 | authors = ["Yusuke Sasaki "] 6 | description = """ 7 | WebSocket support for Finchers, based on tungstenite. 8 | """ 9 | license = "MIT OR Apache-2.0" 10 | readme = "README.md" 11 | repository = "https://github.com/finchers-rs/finchers-tungstenite.git" 12 | 13 | include = [ 14 | "Cargo.toml", 15 | "build.rs", 16 | "src/**/*", 17 | "tests/**/*", 18 | "examples/**/*", 19 | "benches/**/*", 20 | "LICENSE-MIT", 21 | "LICENSE-APACHE", 22 | ] 23 | 24 | [dependencies] 25 | finchers = { version = "0.14.0-dev", path = ".." } 26 | 27 | base64 = "0.10" 28 | failure = "0.1.3" 29 | futures = "0.1.24" 30 | http = "0.1.13" 31 | izanami-util = "0.1.0-preview.1" 32 | log = "0.4" 33 | sha1 = "0.6.0" 34 | tokio-tungstenite = { version = "0.6.0", default-features = false } 35 | tungstenite = "0.6.0" 36 | 37 | [dev-dependencies] 38 | izanami = "0.1.0-preview.1" 39 | log = "0.4.5" 40 | matches = "0.1.8" 41 | pretty_env_logger = "0.3.0" 42 | version-sync = "0.8" 43 | -------------------------------------------------------------------------------- /finchers-tungstenite/README.md: -------------------------------------------------------------------------------- 1 | # `finchers-tungstenite` 2 | 3 | [![crates.io](https://img.shields.io/crates/v/finchers-tungstenite.svg)](https://crates.io/crates/finchers-tungstenite) 4 | [![Docs.rs](https://docs.rs/finchers-tungstenite/badge.svg)](https://docs.rs/finchers-tungstenite) 5 | [![dependency status](https://deps.rs/crate/finchers-tungstenite/0.2.0/status.svg)](https://deps.rs/crate/finchers-tungstenite/0.2.0) 6 | [![Build Status](https://travis-ci.org/finchers-rs/finchers-tungstenite.svg?branch=master)](https://travis-ci.org/finchers-rs/finchers-tungstenite) 7 | [![Coverage Status](https://coveralls.io/repos/github/finchers-rs/finchers-tungstenite/badge.svg?branch=master)](https://coveralls.io/github/finchers-rs/finchers-tungstenite?branch=master) 8 | 9 | WebSocket support for Finchers based on [tungstenite]. 10 | 11 | [tungstenite]: https://github.com/snapview/tungstenite-rs 12 | 13 | # License 14 | [MIT license](../LICENSE-MIT) or [Apache License, Version 2.0](../LICENSE-APACHE) at your option. 15 | -------------------------------------------------------------------------------- /finchers-tungstenite/examples/server.rs: -------------------------------------------------------------------------------- 1 | use finchers::prelude::*; 2 | use finchers_tungstenite::{Message, WsError, WsTransport}; 3 | use futures::prelude::*; 4 | use http::Response; 5 | 6 | fn main() -> izanami::Result<()> { 7 | std::env::set_var("RUST_LOG", "server=info"); 8 | pretty_env_logger::init(); 9 | 10 | let index = finchers::path!("/").map(|| { 11 | Response::builder() 12 | .header("content-type", "text/html; charset=utf-8") 13 | .body( 14 | r#" 15 | 16 | 17 | 18 | Index 19 | 20 | 21 | 22 | 23 | "#, 24 | ) 25 | .unwrap() 26 | }); 27 | 28 | let ws_endpoint = finchers::path!(@get "/ws") // 29 | .and(finchers_tungstenite::ws( 30 | |stream: WsTransport| { 31 | let (tx, rx) = stream.split(); 32 | rx.filter_map(|m| { 33 | log::info!("Message from client: {:?}", m); 34 | match m { 35 | Message::Ping(p) => Some(Message::Pong(p)), 36 | Message::Pong(..) => None, 37 | m => Some(m), 38 | } 39 | }) 40 | .forward(tx) 41 | .map(|_| ()) 42 | .map_err(|e| match e { 43 | WsError::ConnectionClosed(..) => log::info!("connection is closed"), 44 | e => log::error!("error during handling WebSocket connection: {}", e), 45 | }) 46 | }, 47 | )); 48 | 49 | let endpoint = index.or(ws_endpoint); 50 | 51 | let addr: std::net::SocketAddr = ([127, 0, 0, 1], 5000).into(); 52 | log::info!("Listening on http://{}", addr); 53 | izanami::Server::bind_tcp(&addr)?.start(endpoint.into_service()) 54 | } 55 | -------------------------------------------------------------------------------- /finchers-tungstenite/release.toml: -------------------------------------------------------------------------------- 1 | tag-prefix = "v" 2 | tag-message = "release {{version}}" 3 | dev-version-ext = "dev" 4 | pre-release-commit-message = "bump version to {{version}}" 5 | 6 | [[pre-release-replacements]] 7 | file = "README.md" 8 | search = "https://deps.rs/crate/finchers-tungstenite/[a-z0-9\\.-]+" 9 | replace = "https://deps.rs/crate/finchers-tungstenite/{{version}}" 10 | 11 | [[pre-release-replacements]] 12 | file = "src/lib.rs" 13 | search = "https://docs.rs/finchers-tungstenite/[a-z0-9\\.-]+" 14 | replace = "https://docs.rs/finchers-tungstenite/{{version}}" 15 | -------------------------------------------------------------------------------- /finchers-tungstenite/src/handshake.rs: -------------------------------------------------------------------------------- 1 | //! The implementation of WebSocket handshake process. 2 | 3 | use { 4 | failure::Fail, 5 | finchers::error::HttpError, 6 | http::{header, Request, StatusCode}, 7 | sha1::Sha1, 8 | }; 9 | 10 | #[derive(Debug)] 11 | pub(crate) struct Accept { 12 | pub(crate) hash: String, 13 | _priv: (), 14 | } 15 | 16 | /// Check if the specified HTTP response is a valid WebSocket handshake request. 17 | /// 18 | /// If successful, it returns a SHA1 hash used as `Sec-WebSocket-Accept` header in the response. 19 | pub(crate) fn handshake(request: &Request<()>) -> Result { 20 | let h = request 21 | .headers() 22 | .get(header::CONNECTION) 23 | .ok_or_else(|| HandshakeErrorKind::MissingHeader { name: "Connection" })?; 24 | if h != "Upgrade" && h != "upgrade" { 25 | return Err(HandshakeErrorKind::InvalidHeader { name: "Connection" }.into()); 26 | } 27 | 28 | let h = request 29 | .headers() 30 | .get(header::UPGRADE) 31 | .ok_or_else(|| HandshakeErrorKind::MissingHeader { name: "Upgrade" })?; 32 | if h != "Websocket" && h != "websocket" { 33 | return Err(HandshakeErrorKind::InvalidHeader { name: "Upgrade" }.into()); 34 | } 35 | 36 | let h = request 37 | .headers() 38 | .get(header::SEC_WEBSOCKET_VERSION) 39 | .ok_or_else(|| HandshakeErrorKind::MissingHeader { 40 | name: "Sec-WebSocket-Version", 41 | })?; 42 | if h != "13" { 43 | return Err(HandshakeErrorKind::InvalidSecWebSocketVersion.into()); 44 | } 45 | 46 | let h = request 47 | .headers() 48 | .get(header::SEC_WEBSOCKET_KEY) 49 | .ok_or_else(|| HandshakeErrorKind::MissingHeader { 50 | name: "Sec-WebSocket-Key", 51 | })?; 52 | let decoded = base64::decode(h).map_err(|_| HandshakeErrorKind::InvalidSecWebSocketKey)?; 53 | if decoded.len() != 16 { 54 | return Err(HandshakeErrorKind::InvalidSecWebSocketKey.into()); 55 | } 56 | 57 | let mut m = Sha1::new(); 58 | m.update(h.as_bytes()); 59 | m.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); 60 | 61 | let hash = base64::encode(&m.digest().bytes()[..]); 62 | 63 | Ok(Accept { hash, _priv: () }) 64 | } 65 | 66 | /// The error type during handling WebSocket handshake. 67 | #[derive(Debug, Fail)] 68 | #[fail(display = "handshake error: {}", kind)] 69 | pub struct HandshakeError { 70 | kind: HandshakeErrorKind, 71 | } 72 | 73 | impl From for HandshakeError { 74 | fn from(kind: HandshakeErrorKind) -> Self { 75 | HandshakeError { kind } 76 | } 77 | } 78 | 79 | impl HttpError for HandshakeError { 80 | fn status_code(&self) -> StatusCode { 81 | StatusCode::BAD_REQUEST 82 | } 83 | } 84 | 85 | impl HandshakeError { 86 | #[allow(missing_docs)] 87 | pub fn kind(&self) -> &HandshakeErrorKind { 88 | &self.kind 89 | } 90 | } 91 | 92 | #[allow(missing_docs)] 93 | #[derive(Debug, Fail)] 94 | #[cfg_attr(test, derive(PartialEq))] 95 | pub enum HandshakeErrorKind { 96 | #[fail(display = "missing header: `{}'", name)] 97 | MissingHeader { name: &'static str }, 98 | 99 | #[fail(display = "The header value is invalid: `{}'", name)] 100 | InvalidHeader { name: &'static str }, 101 | 102 | #[fail(display = "The value of `Sec-WebSocket-Key` is invalid")] 103 | InvalidSecWebSocketKey, 104 | 105 | #[fail(display = "The value of `Sec-WebSocket-Version` must be equal to '13'")] 106 | InvalidSecWebSocketVersion, 107 | } 108 | -------------------------------------------------------------------------------- /finchers-tungstenite/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! WebSocket support for Finchers based on tungstenite. 2 | 3 | #![doc(html_root_url = "https://docs.rs/finchers-tungstenite/0.3.0-dev")] 4 | #![deny( 5 | missing_docs, 6 | missing_debug_implementations, 7 | nonstandard_style, 8 | rust_2018_compatibility, 9 | rust_2018_idioms, 10 | unused 11 | )] 12 | #![forbid(clippy::unimplemented)] 13 | #![cfg_attr(test, doc(test(attr(deny(warnings)))))] 14 | 15 | mod handshake; 16 | 17 | #[doc(no_inline)] 18 | pub use tungstenite::{ 19 | self, 20 | error::Error as WsError, 21 | protocol::{Message, WebSocketConfig}, 22 | }; 23 | 24 | // re-exports 25 | pub use crate::{ 26 | handshake::{HandshakeError, HandshakeErrorKind}, 27 | imp::{ws, WsEndpoint, WsTransport}, 28 | }; 29 | 30 | mod imp { 31 | use { 32 | crate::handshake::handshake, 33 | finchers::{ 34 | action::{ 35 | ActionContext, // 36 | EndpointAction, 37 | Preflight, 38 | PreflightContext, 39 | }, 40 | endpoint::{Endpoint, IsEndpoint}, 41 | error::Error, 42 | }, 43 | futures::{Async, Future, IntoFuture, Poll}, 44 | http::{header, Response}, 45 | izanami_util::http::Upgrade, 46 | izanami_util::rt::{DefaultExecutor, Executor}, 47 | tokio_tungstenite::WebSocketStream, 48 | tungstenite::protocol::Role, 49 | }; 50 | 51 | #[allow(missing_docs)] 52 | pub type WsTransport = WebSocketStream<::Upgraded>; 53 | 54 | /// Create an endpoint which handles the WebSocket handshake request. 55 | pub fn ws(on_upgrade: F) -> WsEndpoint { 56 | WsEndpoint { on_upgrade } 57 | } 58 | 59 | /// An instance of `Endpoint` which handles the WebSocket handshake request. 60 | #[derive(Debug, Copy, Clone)] 61 | pub struct WsEndpoint { 62 | on_upgrade: F, 63 | } 64 | 65 | impl IsEndpoint for WsEndpoint {} 66 | 67 | impl Endpoint for WsEndpoint 68 | where 69 | F: Fn(WsTransport) -> R + Clone + Send + 'static, 70 | R: IntoFuture, 71 | R::Future: Send + 'static, 72 | Bd: Upgrade + Send + 'static, 73 | Bd::Upgraded: Send + 'static, 74 | { 75 | type Output = (Response<&'static str>,); 76 | type Action = WsAction; 77 | 78 | fn action(&self) -> Self::Action { 79 | WsAction { 80 | on_upgrade: Some(self.on_upgrade.clone()), 81 | } 82 | } 83 | } 84 | 85 | #[derive(Debug)] 86 | pub struct WsAction { 87 | on_upgrade: Option, 88 | } 89 | 90 | impl EndpointAction for WsAction 91 | where 92 | F: Fn(WsTransport) -> R + Send + 'static, 93 | R: IntoFuture, 94 | R::Future: Send + 'static, 95 | Bd: Upgrade + Send + 'static, 96 | Bd::Upgraded: Send + 'static, 97 | { 98 | type Output = (Response<&'static str>,); 99 | 100 | fn preflight( 101 | &mut self, 102 | _: &mut PreflightContext<'_>, 103 | ) -> Result, Error> { 104 | Ok(Preflight::Incomplete) 105 | } 106 | 107 | fn poll_action(&mut self, cx: &mut ActionContext<'_, Bd>) -> Poll { 108 | let accept = handshake(cx)?; 109 | 110 | let on_upgrade = self 111 | .on_upgrade 112 | .take() 113 | .expect("the action has already been polled"); 114 | let upgrade_task = cx 115 | .take_body()? 116 | .on_upgrade() 117 | .map_err(|_e| log::error!("upgrade error")) 118 | .and_then(move |upgraded| { 119 | let ws_stream = WebSocketStream::from_raw_socket(upgraded, Role::Server, None); 120 | on_upgrade(ws_stream).into_future() 121 | }); 122 | 123 | DefaultExecutor::current() 124 | .spawn(Box::new(upgrade_task)) 125 | .map_err(finchers::error::internal_server_error)?; 126 | 127 | let response = Response::builder() 128 | .status(http::StatusCode::SWITCHING_PROTOCOLS) 129 | .header(header::CONNECTION, "upgrade") 130 | .header(header::UPGRADE, "websocket") 131 | .header(header::SEC_WEBSOCKET_ACCEPT, &*accept.hash) 132 | .body("") 133 | .unwrap(); 134 | 135 | Ok(Async::Ready((response,))) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /finchers-tungstenite/tests/test_tungstenite.rs: -------------------------------------------------------------------------------- 1 | use {finchers::prelude::*, http::Request, matches::assert_matches}; 2 | 3 | #[test] 4 | fn version_sync() { 5 | version_sync::assert_html_root_url_updated!("src/lib.rs"); 6 | } 7 | 8 | #[test] 9 | fn test_handshake() -> izanami::Result<()> { 10 | let mut server = izanami::test::server({ 11 | finchers_tungstenite::ws(|stream| { 12 | drop(stream); 13 | futures::future::ok(()) 14 | }) 15 | .into_service() 16 | })?; 17 | 18 | let response = server.perform( 19 | Request::get("/") 20 | .header("host", "localhost:4000") 21 | .header("connection", "upgrade") 22 | .header("upgrade", "websocket") 23 | .header("sec-websocket-version", "13") 24 | .header("sec-websocket-key", "dGhlIHNhbXBsZSBub25jZQ=="), 25 | )?; 26 | 27 | assert_eq!(response.status().as_u16(), 101); 28 | //assert!(response.body().is_upgraded()); 29 | assert_matches!( 30 | response.headers().get("connection"), 31 | Some(h) if h.to_str().unwrap().to_lowercase() == "upgrade" 32 | ); 33 | assert_matches!( 34 | response.headers().get("upgrade"), 35 | Some(h) if h.to_str().unwrap().to_lowercase() == "websocket" 36 | ); 37 | assert_matches!( 38 | response.headers().get("sec-websocket-accept"), 39 | Some(h) if h == "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" 40 | ); 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | tag-prefix = "v" 2 | disable-push = true 3 | no-dev-version = true 4 | pre-release-commit-message = "(cargo-release) bump version to {{version}}" 5 | tag-message = "(cargo-release) version {{version}}" 6 | 7 | [[pre-release-replacements]] 8 | file = "README.md" 9 | search = "https://deps.rs/crate/finchers/[a-z0-9\\.-]+" 10 | replace = "https://deps.rs/crate/finchers/{{version}}" 11 | 12 | [[pre-release-replacements]] 13 | file = "README.md" 14 | search = "finchers = \"[a-z0-9\\.-]+\"" 15 | replace = "finchers = \"{{version}}\"" 16 | 17 | [[pre-release-replacements]] 18 | file = "src/lib.rs" 19 | search = "https://docs.rs/finchers/[a-z0-9\\.-]+" 20 | replace = "https://docs.rs/finchers/{{version}}" 21 | -------------------------------------------------------------------------------- /scripts/update_local_registry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | MANIFEST_DIR="$(cd $(dirname $BASH_SOURCE)/..; pwd)" 6 | 7 | echo "[regenerate Cargo.lock...]" 8 | cd 9 | (set -x; cargo generate-lockfile --manifest-path=$MANIFEST_DIR/Cargo.toml) 10 | 11 | echo "[remove the old files in the local registry...]" 12 | rm -f $MANIFEST_DIR/.registry-index/*.crate 13 | rm -rf $MANIFEST_DIR/.registry-index/index/ 14 | rm -f $MANIFEST_DIR/.registry-index/Cargo.lock 15 | 16 | echo "[fetch local registry...]" 17 | (set -x; cargo local-registry --verbose -s $MANIFEST_DIR/Cargo.lock $MANIFEST_DIR/.registry-index) 18 | (set -x; cp $MANIFEST_DIR/Cargo.lock $MANIFEST_DIR/.registry-index/Cargo.lock) 19 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | mod combine; 2 | mod func; 3 | mod hlist; 4 | 5 | pub(crate) use self::combine::Combine; 6 | pub(crate) use self::func::Func; 7 | pub(crate) use self::hlist::Tuple; 8 | -------------------------------------------------------------------------------- /src/common/combine.rs: -------------------------------------------------------------------------------- 1 | use super::hlist::{HCons, HList, HNil, Tuple}; 2 | 3 | pub trait CombineList { 4 | type Out: HList; 5 | 6 | fn combine(self, other: T) -> Self::Out; 7 | } 8 | 9 | impl CombineList for HNil { 10 | type Out = T; 11 | 12 | fn combine(self, other: T) -> Self::Out { 13 | other 14 | } 15 | } 16 | 17 | impl CombineList for HCons 18 | where 19 | T: CombineList, 20 | HCons>::Out>: HList, 21 | { 22 | type Out = HCons>::Out>; 23 | 24 | #[inline(always)] 25 | fn combine(self, other: U) -> Self::Out { 26 | HCons { 27 | head: self.head, 28 | tail: self.tail.combine(other), 29 | } 30 | } 31 | } 32 | 33 | pub trait Combine: Tuple + sealed::Sealed { 34 | type Out: Tuple; 35 | 36 | fn combine(self, other: T) -> Self::Out; 37 | } 38 | 39 | impl Combine for H 40 | where 41 | H::HList: CombineList, 42 | { 43 | type Out = <>::Out as HList>::Tuple; 44 | 45 | fn combine(self, other: T) -> Self::Out { 46 | self.hlist().combine(other.hlist()).tuple() 47 | } 48 | } 49 | 50 | mod sealed { 51 | use super::{CombineList, Tuple}; 52 | 53 | pub trait Sealed {} 54 | 55 | impl Sealed for H where H::HList: CombineList {} 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::*; 61 | 62 | fn combine(h: H, t: T) -> H::Out 63 | where 64 | H: Combine, 65 | { 66 | h.combine(t) 67 | } 68 | 69 | #[test] 70 | fn case1_units() { 71 | assert_eq!(combine((), ()), ()); 72 | } 73 | 74 | #[test] 75 | fn case2_unit1() { 76 | assert_eq!(combine((10,), ()), (10,)); 77 | } 78 | 79 | #[test] 80 | fn case3_unit2() { 81 | assert_eq!(combine((), (10,)), (10,)); 82 | } 83 | 84 | #[test] 85 | fn case4_complicated() { 86 | assert_eq!( 87 | combine(("a", "b", "c"), (10, 20, 30)), 88 | ("a", "b", "c", 10, 20, 30) 89 | ); 90 | } 91 | 92 | #[test] 93 | fn case5_nested() { 94 | assert_eq!( 95 | combine(("a", ("b", "c")), (10, (20,), 30)), 96 | ("a", ("b", "c"), 10, (20,), 30) 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/common/func.rs: -------------------------------------------------------------------------------- 1 | use super::hlist::Tuple; 2 | 3 | pub trait Func { 4 | type Out; 5 | 6 | fn call(&self, args: Args) -> Self::Out; 7 | } 8 | 9 | impl Func<()> for F 10 | where 11 | F: Fn() -> R, 12 | { 13 | type Out = R; 14 | 15 | #[inline] 16 | fn call(&self, _: ()) -> Self::Out { 17 | (*self)() 18 | } 19 | } 20 | 21 | macro_rules! generics { 22 | ($T:ident) => { 23 | impl Func<($T,)> for F 24 | where 25 | F: Fn($T) -> R, 26 | { 27 | type Out = R; 28 | 29 | #[inline] 30 | fn call(&self, args: ($T,)) -> Self::Out { 31 | (*self)(args.0) 32 | } 33 | } 34 | }; 35 | ($H:ident, $($T:ident),*) => { 36 | generics!($($T),*); 37 | 38 | impl Func<($H, $($T),*)> for F 39 | where 40 | F: Fn($H, $($T),*) -> R, 41 | { 42 | type Out = R; 43 | 44 | #[inline] 45 | fn call(&self, args: ($H, $($T),*)) -> Self::Out { 46 | #[allow(non_snake_case)] 47 | let ($H, $($T),*) = args; 48 | (*self)($H, $($T),*) 49 | } 50 | } 51 | }; 52 | 53 | ($H:ident, $($T:ident,)*) => { generics! { $H, $($T),* } }; 54 | } 55 | 56 | generics! { 57 | T15, T14, T13, T12, T11, T10, T9, T8, T7, T6, T5, T4, T3, T2, T1, T0, 58 | } 59 | -------------------------------------------------------------------------------- /src/common/hlist.rs: -------------------------------------------------------------------------------- 1 | pub trait Tuple: Sized { 2 | type HList: HList; 3 | 4 | fn hlist(self) -> Self::HList; 5 | } 6 | 7 | impl Tuple for () { 8 | type HList = HNil; 9 | 10 | #[inline(always)] 11 | fn hlist(self) -> Self::HList { 12 | HNil 13 | } 14 | } 15 | 16 | pub trait HList: Sized { 17 | type Tuple: Tuple; 18 | 19 | fn tuple(self) -> Self::Tuple; 20 | } 21 | 22 | #[derive(Debug, Copy, Clone, PartialEq)] 23 | pub struct HNil; 24 | 25 | #[allow(clippy::unused_unit)] 26 | impl HList for HNil { 27 | type Tuple = (); 28 | 29 | #[inline(always)] 30 | fn tuple(self) -> Self::Tuple { 31 | () 32 | } 33 | } 34 | 35 | #[derive(Debug, Copy, Clone, PartialEq)] 36 | pub struct HCons { 37 | pub head: H, 38 | pub tail: T, 39 | } 40 | 41 | macro_rules! hcons { 42 | ($H:expr) => { 43 | HCons { 44 | head: $H, 45 | tail: HNil, 46 | } 47 | }; 48 | ($H:expr, $($T:expr),*) => { 49 | HCons { 50 | head: $H, 51 | tail: hcons!($($T),*), 52 | } 53 | }; 54 | } 55 | 56 | macro_rules! HCons { 57 | ($H:ty) => { HCons<$H, HNil> }; 58 | ($H:ty, $($T:ty),*) => { HCons<$H, HCons!($($T),*)> }; 59 | } 60 | 61 | macro_rules! hcons_pat { 62 | ($H:pat) => { 63 | HCons { 64 | head: $H, 65 | tail: HNil, 66 | } 67 | }; 68 | ($H:pat, $($T:pat),*) => { 69 | HCons { 70 | head: $H, 71 | tail: hcons_pat!($($T),*), 72 | } 73 | }; 74 | } 75 | 76 | // == generics == 77 | 78 | macro_rules! generics { 79 | ($T:ident) => { 80 | impl<$T> HList for HCons!($T) { 81 | type Tuple = ($T,); 82 | 83 | #[inline(always)] 84 | fn tuple(self) -> Self::Tuple { 85 | (self.head,) 86 | } 87 | } 88 | 89 | impl<$T> Tuple for ($T,) { 90 | type HList = HCons!($T); 91 | 92 | #[inline(always)] 93 | fn hlist(self) -> Self::HList { 94 | hcons!(self.0) 95 | } 96 | } 97 | }; 98 | ($H:ident, $($T:ident),*) => { 99 | generics!($($T),*); 100 | 101 | impl<$H, $($T),*> HList for HCons!($H, $($T),*) { 102 | type Tuple = ($H, $($T),*); 103 | 104 | #[inline(always)] 105 | fn tuple(self) -> Self::Tuple { 106 | #[allow(non_snake_case)] 107 | let hcons_pat!($H, $($T),*) = self; 108 | ($H, $($T),*) 109 | } 110 | } 111 | 112 | impl<$H, $($T),*> Tuple for ($H, $($T),*) { 113 | type HList = HCons!($H, $($T),*); 114 | 115 | #[inline(always)] 116 | fn hlist(self) -> Self::HList { 117 | #[allow(non_snake_case)] 118 | let ($H, $($T),*) = self; 119 | hcons!($H, $($T),*) 120 | } 121 | } 122 | }; 123 | ($H:ident, $($T:ident,)*) => { 124 | generics!($H, $($T),*); 125 | }; 126 | } 127 | 128 | generics! { 129 | // T31, T30, T29, T28, T27, T26, T25, T24, T23, T22, T21, T20, T19, T18, T17, T16, 130 | T15, T14, T13, T12, T11, T10, T9, T8, T7, T6, T5, T4, T3, T2, T1, T0, 131 | } 132 | -------------------------------------------------------------------------------- /src/endpoint/boxed.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | action::{ 4 | ActionContext, // 5 | EndpointAction, 6 | Preflight, 7 | PreflightContext, 8 | }, 9 | common::Tuple, 10 | endpoint::{Endpoint, IsEndpoint}, 11 | error::Error, 12 | }, 13 | futures::Poll, 14 | std::fmt, 15 | }; 16 | 17 | trait BoxedEndpoint { 18 | type Output: Tuple; 19 | 20 | fn action(&self) -> EndpointActionObj; 21 | } 22 | 23 | impl BoxedEndpoint for E 24 | where 25 | E: Endpoint, 26 | E::Action: Send + 'static, 27 | { 28 | type Output = E::Output; 29 | 30 | fn action(&self) -> EndpointActionObj { 31 | EndpointActionObj { 32 | inner: Box::new(self.action()), 33 | } 34 | } 35 | } 36 | 37 | /// A type that holds an instance of `Endpoint` as type-erased form. 38 | /// 39 | /// This type guarantees the thread safety. 40 | pub struct EndpointObj { 41 | inner: Box + Send + Sync + 'static>, 42 | } 43 | 44 | impl EndpointObj 45 | where 46 | T: Tuple, 47 | { 48 | pub(super) fn new(endpoint: E) -> Self 49 | where 50 | E: Endpoint + Send + Sync + 'static, 51 | E::Action: Send + 'static, 52 | { 53 | EndpointObj { 54 | inner: Box::new(endpoint), 55 | } 56 | } 57 | } 58 | 59 | impl fmt::Debug for EndpointObj 60 | where 61 | T: Tuple, 62 | { 63 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 64 | formatter.debug_struct("EndpointObj").finish() 65 | } 66 | } 67 | 68 | impl IsEndpoint for EndpointObj where T: Tuple {} 69 | 70 | impl Endpoint for EndpointObj 71 | where 72 | T: Tuple, 73 | { 74 | type Output = T; 75 | type Action = EndpointActionObj; 76 | 77 | #[inline] 78 | fn action(&self) -> Self::Action { 79 | self.inner.action() 80 | } 81 | } 82 | 83 | #[allow(missing_debug_implementations)] 84 | pub struct EndpointActionObj 85 | where 86 | T: Tuple, 87 | { 88 | inner: Box + Send + 'static>, 89 | } 90 | 91 | impl EndpointAction for EndpointActionObj 92 | where 93 | T: Tuple, 94 | { 95 | type Output = T; 96 | 97 | #[inline] 98 | fn preflight( 99 | &mut self, 100 | cx: &mut PreflightContext<'_>, 101 | ) -> Result, Error> { 102 | self.inner.preflight(cx) 103 | } 104 | 105 | #[inline] 106 | fn poll_action(&mut self, cx: &mut ActionContext<'_, Bd>) -> Poll { 107 | self.inner.poll_action(cx) 108 | } 109 | } 110 | 111 | // ==== BoxedLocal ==== 112 | 113 | trait LocalBoxedEndpoint { 114 | type Output: Tuple; 115 | 116 | fn action(&self) -> LocalEndpointActionObj; 117 | } 118 | 119 | impl LocalBoxedEndpoint for E 120 | where 121 | E: Endpoint, 122 | E::Action: 'static, 123 | { 124 | type Output = E::Output; 125 | 126 | fn action(&self) -> LocalEndpointActionObj { 127 | LocalEndpointActionObj { 128 | inner: Box::new(self.action()), 129 | } 130 | } 131 | } 132 | 133 | /// A type that holds an instance of `Endpoint` as type-erased form. 134 | /// 135 | /// Unlike `EndpointObj`, this type does not guarantee the thread safety. 136 | pub struct LocalEndpointObj 137 | where 138 | T: Tuple, 139 | { 140 | inner: Box + 'static>, 141 | } 142 | 143 | impl LocalEndpointObj 144 | where 145 | T: Tuple, 146 | { 147 | pub(super) fn new(endpoint: E) -> Self 148 | where 149 | E: Endpoint + 'static, 150 | E::Action: 'static, 151 | { 152 | LocalEndpointObj { 153 | inner: Box::new(endpoint), 154 | } 155 | } 156 | } 157 | 158 | impl fmt::Debug for LocalEndpointObj 159 | where 160 | T: Tuple, 161 | { 162 | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 163 | formatter.debug_struct("LocalEndpointObj").finish() 164 | } 165 | } 166 | 167 | impl IsEndpoint for LocalEndpointObj where T: Tuple {} 168 | 169 | impl Endpoint for LocalEndpointObj 170 | where 171 | T: Tuple, 172 | { 173 | type Output = T; 174 | type Action = LocalEndpointActionObj; 175 | 176 | #[inline] 177 | fn action(&self) -> Self::Action { 178 | self.inner.action() 179 | } 180 | } 181 | 182 | #[allow(missing_debug_implementations)] 183 | pub struct LocalEndpointActionObj 184 | where 185 | T: Tuple, 186 | { 187 | inner: Box + 'static>, 188 | } 189 | 190 | impl EndpointAction for LocalEndpointActionObj 191 | where 192 | T: Tuple, 193 | { 194 | type Output = T; 195 | 196 | #[inline] 197 | fn preflight( 198 | &mut self, 199 | cx: &mut PreflightContext<'_>, 200 | ) -> Result, Error> { 201 | self.inner.preflight(cx) 202 | } 203 | 204 | #[inline] 205 | fn poll_action(&mut self, cx: &mut ActionContext<'_, Bd>) -> Poll { 206 | self.inner.poll_action(cx) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/endpoint/ext.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | mod and; 4 | mod and_then; 5 | mod map; 6 | mod map_err; 7 | mod or; 8 | mod or_strict; 9 | mod recover; 10 | 11 | pub use self::{ 12 | and::And, // 13 | and_then::AndThen, 14 | map::Map, 15 | map_err::MapErr, 16 | or::Or, 17 | or_strict::OrStrict, 18 | recover::Recover, 19 | }; 20 | 21 | use { 22 | super::IsEndpoint, // 23 | crate::error::{Error, HttpError}, 24 | }; 25 | 26 | /// A set of extension methods for combining the multiple endpoints. 27 | pub trait EndpointExt: IsEndpoint + Sized { 28 | /// Create an endpoint which evaluates `self` and `e` and returns a pair of their tasks. 29 | /// 30 | /// The returned future from this endpoint contains both futures from 31 | /// `self` and `e` and resolved as a pair of values returned from theirs. 32 | fn and(self, other: E) -> And { 33 | And { 34 | e1: self, 35 | e2: other, 36 | } 37 | } 38 | 39 | /// Create an endpoint which evaluates `self` and `e` sequentially. 40 | /// 41 | /// The returned future from this endpoint contains the one returned 42 | /// from either `self` or `e` matched "better" to the input. 43 | fn or(self, other: E) -> Or { 44 | Or { 45 | e1: self, 46 | e2: other, 47 | } 48 | } 49 | 50 | /// Create an endpoint which evaluates `self` and `e` sequentially. 51 | /// 52 | /// The differences of behaviour to `Or` are as follows: 53 | /// 54 | /// * The associated type `E::Output` must be equal to `Self::Output`. 55 | /// It means that the generated endpoint has the same output type 56 | /// as the original endpoints and the return value will be used later. 57 | /// * If `self` is matched to the request, `other.apply(cx)` 58 | /// is not called and the future returned from `self.apply(cx)` is 59 | /// immediately returned. 60 | fn or_strict(self, other: E) -> OrStrict { 61 | OrStrict { 62 | e1: self, 63 | e2: other, 64 | } 65 | } 66 | 67 | #[allow(missing_docs)] 68 | fn map(self, f: F) -> Map { 69 | Map { endpoint: self, f } 70 | } 71 | 72 | #[allow(missing_docs)] 73 | fn and_then(self, f: F) -> AndThen { 74 | AndThen { endpoint: self, f } 75 | } 76 | 77 | #[allow(missing_docs)] 78 | fn map_err(self, f: F) -> MapErr { 79 | MapErr { endpoint: self, f } 80 | } 81 | 82 | #[allow(missing_docs)] 83 | fn recover(self, f: F) -> Recover { 84 | Recover { endpoint: self, f } 85 | } 86 | } 87 | 88 | impl EndpointExt for E {} 89 | 90 | /// An `HttpError` indicating that the endpoint could not determine the route. 91 | /// 92 | /// The value of this error is typically thrown from `Or` or `OrStrict`. 93 | #[derive(Debug, failure::Fail)] 94 | #[fail(display = "not matched")] 95 | pub struct NotMatched { 96 | /// The error value returned from the first endpoint. 97 | pub left: Error, 98 | 99 | /// The error value returned from the second endpoint. 100 | pub right: Error, 101 | 102 | _priv: (), 103 | } 104 | 105 | impl HttpError for NotMatched { 106 | fn status_code(&self) -> http::StatusCode { 107 | http::StatusCode::NOT_FOUND 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/endpoint/ext/and.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | action::{ 4 | ActionContext, // 5 | EndpointAction, 6 | Preflight, 7 | PreflightContext, 8 | }, 9 | common::Combine, 10 | endpoint::{Endpoint, IsEndpoint}, 11 | error::Error, 12 | }, 13 | futures::{Async, Poll}, 14 | }; 15 | 16 | #[allow(missing_docs)] 17 | #[derive(Copy, Clone, Debug)] 18 | pub struct And { 19 | pub(super) e1: E1, 20 | pub(super) e2: E2, 21 | } 22 | 23 | impl IsEndpoint for And {} 24 | 25 | impl Endpoint for And 26 | where 27 | E1: Endpoint, 28 | E2: Endpoint, 29 | E1::Output: Combine, 30 | { 31 | type Output = >::Out; 32 | type Action = AndAction; 33 | 34 | fn action(&self) -> Self::Action { 35 | AndAction { 36 | f1: MaybeDone::Init(Some(self.e1.action())), 37 | f2: MaybeDone::Init(Some(self.e2.action())), 38 | } 39 | } 40 | } 41 | 42 | #[allow(missing_debug_implementations)] 43 | pub struct AndAction 44 | where 45 | F1: EndpointAction, 46 | F2: EndpointAction, 47 | { 48 | f1: MaybeDone, 49 | f2: MaybeDone, 50 | } 51 | 52 | impl AndAction 53 | where 54 | F1: EndpointAction, 55 | F2: EndpointAction, 56 | F1::Output: Combine, 57 | { 58 | fn take_item(&mut self) -> Option<>::Out> { 59 | let v1 = self.f1.take_item()?; 60 | let v2 = self.f2.take_item()?; 61 | Some(Combine::combine(v1, v2)) 62 | } 63 | } 64 | 65 | impl EndpointAction for AndAction 66 | where 67 | F1: EndpointAction, 68 | F2: EndpointAction, 69 | F1::Output: Combine, 70 | { 71 | type Output = >::Out; 72 | 73 | fn preflight( 74 | &mut self, 75 | cx: &mut PreflightContext<'_>, 76 | ) -> Result, Error> { 77 | let x1 = self.f1.preflight(cx)?; 78 | let x2 = self.f2.preflight(cx)?; 79 | if x1.is_completed() && x2.is_completed() { 80 | let out = self.take_item().expect("the value shoud be ready."); 81 | Ok(Preflight::Completed(out)) 82 | } else { 83 | Ok(Preflight::Incomplete) 84 | } 85 | } 86 | 87 | fn poll_action(&mut self, cx: &mut ActionContext<'_, Bd>) -> Poll { 88 | futures::try_ready!(self.f1.poll_action(cx)); 89 | futures::try_ready!(self.f2.poll_action(cx)); 90 | let out = self.take_item().expect("the value should be ready."); 91 | Ok(out.into()) 92 | } 93 | } 94 | 95 | #[derive(Debug)] 96 | #[must_use = "futures do nothing unless polled."] 97 | pub enum MaybeDone> { 98 | Init(Option), 99 | InFlight(F), 100 | Ready(F::Output), 101 | Gone, 102 | } 103 | 104 | impl> MaybeDone { 105 | pub fn take_item(&mut self) -> Option { 106 | match std::mem::replace(self, MaybeDone::Gone) { 107 | MaybeDone::Ready(output) => Some(output), 108 | _ => None, 109 | } 110 | } 111 | } 112 | 113 | impl> EndpointAction for MaybeDone { 114 | type Output = (); 115 | 116 | fn preflight( 117 | &mut self, 118 | cx: &mut PreflightContext<'_>, 119 | ) -> Result, Error> { 120 | *self = match self { 121 | MaybeDone::Init(ref mut action) => { 122 | let mut action = action.take().unwrap(); 123 | if let Preflight::Completed(output) = action.preflight(cx)? { 124 | MaybeDone::Ready(output) 125 | } else { 126 | MaybeDone::InFlight(action) 127 | } 128 | } 129 | _ => panic!("unexpected condition"), 130 | }; 131 | 132 | Ok(Preflight::Incomplete) 133 | } 134 | 135 | fn poll_action(&mut self, cx: &mut ActionContext<'_, Bd>) -> Poll { 136 | loop { 137 | *self = match self { 138 | MaybeDone::Init(..) => panic!("The action has not yet initialized."), 139 | MaybeDone::Ready(..) => return Ok(Async::Ready(())), 140 | MaybeDone::InFlight(ref mut future) => { 141 | let output = futures::try_ready!(future.poll_action(cx)); 142 | MaybeDone::Ready(output) 143 | } 144 | MaybeDone::Gone => panic!("The action has already been polled."), 145 | }; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/endpoint/ext/and_then.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | action::{ 4 | ActionContext, // 5 | EndpointAction, 6 | Preflight, 7 | PreflightContext, 8 | }, 9 | common::Func, 10 | endpoint::{Endpoint, IsEndpoint}, 11 | error::Error, 12 | }, 13 | futures::{Future, IntoFuture, Poll}, 14 | }; 15 | 16 | #[allow(missing_docs)] 17 | #[derive(Debug, Copy, Clone)] 18 | pub struct AndThen { 19 | pub(super) endpoint: E, 20 | pub(super) f: F, 21 | } 22 | 23 | impl IsEndpoint for AndThen {} 24 | 25 | impl Endpoint for AndThen 26 | where 27 | E: Endpoint, 28 | F: Func + Clone, 29 | R: IntoFuture, 30 | R::Error: Into, 31 | { 32 | type Output = (R::Item,); 33 | type Action = AndThenAction; 34 | 35 | fn action(&self) -> Self::Action { 36 | AndThenAction { 37 | action: self.endpoint.action(), 38 | f: self.f.clone(), 39 | in_flight: None, 40 | } 41 | } 42 | } 43 | 44 | #[allow(missing_debug_implementations)] 45 | pub struct AndThenAction { 46 | action: Act, 47 | f: F, 48 | in_flight: Option, 49 | } 50 | 51 | impl EndpointAction for AndThenAction 52 | where 53 | Act: EndpointAction, 54 | F: Func, 55 | R: IntoFuture, 56 | R::Error: Into, 57 | { 58 | type Output = (R::Item,); 59 | 60 | fn preflight( 61 | &mut self, 62 | cx: &mut PreflightContext<'_>, 63 | ) -> Result, Error> { 64 | debug_assert!(self.in_flight.is_none()); 65 | if let Preflight::Completed(output) = self.action.preflight(cx)? { 66 | self.in_flight = Some(self.f.call(output).into_future()); 67 | } 68 | Ok(Preflight::Incomplete) 69 | } 70 | 71 | fn poll_action(&mut self, cx: &mut ActionContext<'_, Bd>) -> Poll { 72 | loop { 73 | if let Some(ref mut in_flight) = self.in_flight { 74 | return in_flight 75 | .poll() 76 | .map(|x| x.map(|out| (out,))) 77 | .map_err(Into::into); 78 | } 79 | 80 | let args = futures::try_ready!(self.action.poll_action(cx)); 81 | self.in_flight = Some(self.f.call(args).into_future()); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/endpoint/ext/map.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | action::{ 4 | ActionContext, // 5 | EndpointAction, 6 | Preflight, 7 | PreflightContext, 8 | }, 9 | common::Func, 10 | endpoint::{Endpoint, IsEndpoint}, 11 | error::Error, 12 | }, 13 | futures::Poll, 14 | }; 15 | 16 | #[allow(missing_docs)] 17 | #[derive(Debug, Copy, Clone)] 18 | pub struct Map { 19 | pub(super) endpoint: E, 20 | pub(super) f: F, 21 | } 22 | 23 | impl IsEndpoint for Map {} 24 | 25 | impl Endpoint for Map 26 | where 27 | E: Endpoint, 28 | F: Func + Clone, 29 | { 30 | type Output = (F::Out,); 31 | type Action = MapAction; 32 | 33 | fn action(&self) -> Self::Action { 34 | MapAction { 35 | action: self.endpoint.action(), 36 | f: self.f.clone(), 37 | } 38 | } 39 | } 40 | 41 | #[derive(Debug)] 42 | pub struct MapAction { 43 | action: A, 44 | f: F, 45 | } 46 | 47 | impl EndpointAction for MapAction 48 | where 49 | A: EndpointAction, 50 | F: Func, 51 | { 52 | type Output = (F::Out,); 53 | 54 | fn preflight( 55 | &mut self, 56 | cx: &mut PreflightContext<'_>, 57 | ) -> Result, Error> { 58 | self.action 59 | .preflight(cx) 60 | .map(|x| x.map(|args| (self.f.call(args),))) 61 | } 62 | 63 | fn poll_action(&mut self, cx: &mut ActionContext<'_, Bd>) -> Poll { 64 | self.action 65 | .poll_action(cx) 66 | .map(|x| x.map(|args| (self.f.call(args),))) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/endpoint/ext/map_err.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | action::{ 4 | ActionContext, // 5 | EndpointAction, 6 | Preflight, 7 | PreflightContext, 8 | }, 9 | endpoint::{Endpoint, IsEndpoint}, 10 | error::Error, 11 | }, 12 | futures::Poll, 13 | }; 14 | 15 | #[allow(missing_docs)] 16 | #[derive(Debug, Copy, Clone)] 17 | pub struct MapErr { 18 | pub(super) endpoint: E, 19 | pub(super) f: F, 20 | } 21 | 22 | impl IsEndpoint for MapErr {} 23 | 24 | impl Endpoint for MapErr 25 | where 26 | E: Endpoint, 27 | F: Fn(Error) -> R + Clone, 28 | R: Into, 29 | { 30 | type Output = E::Output; 31 | type Action = MapErrAction; 32 | 33 | fn action(&self) -> Self::Action { 34 | MapErrAction { 35 | action: self.endpoint.action(), 36 | f: self.f.clone(), 37 | } 38 | } 39 | } 40 | 41 | #[allow(missing_debug_implementations)] 42 | pub struct MapErrAction { 43 | action: Act, 44 | f: F, 45 | } 46 | 47 | impl EndpointAction for MapErrAction 48 | where 49 | Act: EndpointAction, 50 | F: Fn(Error) -> R, 51 | R: Into, 52 | { 53 | type Output = Act::Output; 54 | 55 | fn preflight( 56 | &mut self, 57 | cx: &mut PreflightContext<'_>, 58 | ) -> Result, Error> { 59 | self.action 60 | .preflight(cx) 61 | .map_err(|err| (self.f)(err).into()) 62 | } 63 | 64 | fn poll_action(&mut self, cx: &mut ActionContext<'_, Bd>) -> Poll { 65 | self.action 66 | .poll_action(cx) 67 | .map_err(|err| (self.f)(err).into()) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/endpoint/ext/or.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::NotMatched, 3 | crate::{ 4 | action::{ 5 | ActionContext, // 6 | EndpointAction, 7 | Preflight, 8 | PreflightContext, 9 | }, 10 | endpoint::{Endpoint, IsEndpoint}, 11 | error::Error, 12 | }, 13 | either::Either, 14 | futures::Poll, 15 | }; 16 | 17 | #[allow(missing_docs)] 18 | #[derive(Debug, Copy, Clone)] 19 | pub struct Or { 20 | pub(super) e1: E1, 21 | pub(super) e2: E2, 22 | } 23 | 24 | impl IsEndpoint for Or {} 25 | 26 | impl Endpoint for Or 27 | where 28 | E1: Endpoint, 29 | E2: Endpoint, 30 | { 31 | type Output = (Either,); 32 | type Action = OrAction; 33 | 34 | fn action(&self) -> Self::Action { 35 | OrAction { 36 | state: State::Init(self.e1.action(), self.e2.action()), 37 | } 38 | } 39 | } 40 | 41 | #[allow(missing_debug_implementations)] 42 | enum State { 43 | Init(L, R), 44 | Left(L), 45 | Right(R), 46 | Done, 47 | } 48 | 49 | #[allow(missing_debug_implementations)] 50 | pub struct OrAction { 51 | state: State, 52 | } 53 | 54 | impl EndpointAction for OrAction 55 | where 56 | E1: EndpointAction, 57 | E2: EndpointAction, 58 | { 59 | type Output = (Either,); 60 | 61 | fn preflight( 62 | &mut self, 63 | cx: &mut PreflightContext<'_>, 64 | ) -> Result, Error> { 65 | self.state = match std::mem::replace(&mut self.state, State::Done) { 66 | State::Init(mut left, mut right) => { 67 | let orig_cx = cx.clone(); 68 | let left_output = left.preflight(cx); 69 | let mut cx1 = std::mem::replace(cx, orig_cx); 70 | let right_output = right.preflight(cx); 71 | 72 | match (left_output, right_output) { 73 | (Ok(l), Ok(r)) => { 74 | // If both endpoints are matched, the one with the larger number of 75 | // (consumed) path segments is choosen. 76 | if cx1.cursor().num_popped_segments() >= cx.cursor().num_popped_segments() { 77 | *cx = cx1; 78 | if let Preflight::Completed((output,)) = l { 79 | return Ok(Preflight::Completed((Either::Left(output),))); 80 | } else { 81 | State::Left(left) 82 | } 83 | } else if let Preflight::Completed((output,)) = r { 84 | return Ok(Preflight::Completed((Either::Right(output),))); 85 | } else { 86 | State::Right(right) 87 | } 88 | } 89 | 90 | (Ok(l), Err(..)) => { 91 | *cx = cx1; 92 | if let Preflight::Completed((output,)) = l { 93 | return Ok(Preflight::Completed((Either::Left(output),))); 94 | } else { 95 | State::Left(left) 96 | } 97 | } 98 | 99 | (Err(..), Ok(r)) => { 100 | if let Preflight::Completed((output,)) = r { 101 | return Ok(Preflight::Completed((Either::Right(output),))); 102 | } else { 103 | State::Right(right) 104 | } 105 | } 106 | 107 | (Err(left), Err(right)) => { 108 | return Err(NotMatched { 109 | left, 110 | right, 111 | _priv: (), 112 | } 113 | .into()); 114 | } 115 | } 116 | } 117 | _ => panic!("unexpected condition"), 118 | }; 119 | 120 | Ok(Preflight::Incomplete) 121 | } 122 | 123 | #[inline] 124 | fn poll_action(&mut self, cx: &mut ActionContext<'_, Bd>) -> Poll { 125 | match self.state { 126 | State::Left(ref mut t) => t 127 | .poll_action(cx) 128 | .map(|x| x.map(|(out,)| (Either::Left(out),))) 129 | .map_err(Into::into), 130 | State::Right(ref mut t) => t 131 | .poll_action(cx) 132 | .map(|x| x.map(|(out,)| (Either::Right(out),))) 133 | .map_err(Into::into), 134 | _ => panic!("unexpected condition"), 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/endpoint/ext/or_strict.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::NotMatched, 3 | crate::{ 4 | action::{ 5 | ActionContext, // 6 | EndpointAction, 7 | Preflight, 8 | PreflightContext, 9 | }, 10 | endpoint::{Endpoint, IsEndpoint}, 11 | error::Error, 12 | }, 13 | futures::Poll, 14 | }; 15 | 16 | #[allow(missing_docs)] 17 | #[derive(Debug, Copy, Clone)] 18 | pub struct OrStrict { 19 | pub(super) e1: E1, 20 | pub(super) e2: E2, 21 | } 22 | 23 | impl IsEndpoint for OrStrict {} 24 | 25 | impl Endpoint for OrStrict 26 | where 27 | E1: Endpoint, 28 | E2: Endpoint, 29 | { 30 | type Output = E1::Output; 31 | type Action = OrStrictAction; 32 | 33 | fn action(&self) -> Self::Action { 34 | OrStrictAction { 35 | state: State::Init(self.e1.action(), self.e2.action()), 36 | } 37 | } 38 | } 39 | 40 | #[allow(missing_debug_implementations)] 41 | enum State { 42 | Init(L, R), 43 | Left(L), 44 | Right(R), 45 | Done, 46 | } 47 | 48 | #[allow(missing_debug_implementations)] 49 | pub struct OrStrictAction { 50 | state: State, 51 | } 52 | 53 | impl EndpointAction for OrStrictAction 54 | where 55 | L: EndpointAction, 56 | R: EndpointAction, 57 | { 58 | type Output = L::Output; 59 | 60 | fn preflight( 61 | &mut self, 62 | cx: &mut PreflightContext<'_>, 63 | ) -> Result, Error> { 64 | self.state = match std::mem::replace(&mut self.state, State::Done) { 65 | State::Init(mut left, mut right) => { 66 | let orig_cx = cx.clone(); 67 | match left.preflight(cx) { 68 | Ok(Preflight::Incomplete) => State::Left(left), 69 | Ok(Preflight::Completed(output)) => return Ok(Preflight::Completed(output)), 70 | Err(e1) => { 71 | *cx = orig_cx; 72 | match right.preflight(cx) { 73 | Ok(Preflight::Incomplete) => State::Right(right), 74 | Ok(Preflight::Completed(output)) => { 75 | return Ok(Preflight::Completed(output)); 76 | } 77 | Err(e2) => { 78 | return Err(NotMatched { 79 | left: e1, 80 | right: e2, 81 | _priv: (), 82 | } 83 | .into()); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | _ => panic!("unexpected condition"), 90 | }; 91 | 92 | Ok(Preflight::Incomplete) 93 | } 94 | 95 | #[inline] 96 | fn poll_action(&mut self, cx: &mut ActionContext<'_, Bd>) -> Poll { 97 | match self.state { 98 | State::Init(..) => panic!(), 99 | State::Left(ref mut t) => t.poll_action(cx).map_err(Into::into), 100 | State::Right(ref mut t) => t.poll_action(cx).map_err(Into::into), 101 | State::Done => panic!(), 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/endpoint/ext/recover.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | action::{ 4 | ActionContext, // 5 | EndpointAction, 6 | Preflight, 7 | PreflightContext, 8 | }, 9 | endpoint::{Endpoint, IsEndpoint}, 10 | error::Error, 11 | }, 12 | futures::{Future, IntoFuture, Poll}, 13 | }; 14 | 15 | #[allow(missing_docs)] 16 | #[derive(Debug, Copy, Clone)] 17 | pub struct Recover { 18 | pub(super) endpoint: E, 19 | pub(super) f: F, 20 | } 21 | 22 | impl IsEndpoint for Recover {} 23 | 24 | impl Endpoint for Recover 25 | where 26 | E: Endpoint, 27 | F: Fn(Error) -> R + Clone, 28 | R: IntoFuture, 29 | R::Error: Into, 30 | { 31 | type Output = (R::Item,); 32 | type Action = RecoverAction; 33 | 34 | fn action(&self) -> Self::Action { 35 | RecoverAction { 36 | action: self.endpoint.action(), 37 | f: self.f.clone(), 38 | in_flight: None, 39 | } 40 | } 41 | } 42 | 43 | #[allow(missing_debug_implementations)] 44 | pub struct RecoverAction { 45 | action: Act, 46 | f: F, 47 | in_flight: Option, 48 | } 49 | 50 | impl EndpointAction for RecoverAction 51 | where 52 | Act: EndpointAction, 53 | F: Fn(Error) -> R, 54 | R: IntoFuture, 55 | R::Error: Into, 56 | { 57 | type Output = (R::Item,); 58 | 59 | fn preflight( 60 | &mut self, 61 | cx: &mut PreflightContext<'_>, 62 | ) -> Result, Error> { 63 | debug_assert!(self.in_flight.is_none()); 64 | self.action.preflight(cx).or_else(|err| { 65 | self.in_flight = Some((self.f)(err).into_future()); 66 | Ok(Preflight::Incomplete) 67 | }) 68 | } 69 | 70 | fn poll_action(&mut self, cx: &mut ActionContext<'_, Bd>) -> Poll { 71 | loop { 72 | if let Some(ref mut in_flight) = self.in_flight { 73 | return in_flight 74 | .poll() 75 | .map(|x| x.map(|out| (out,))) 76 | .map_err(Into::into); 77 | } 78 | match self.action.poll_action(cx) { 79 | Ok(x) => return Ok(x), 80 | Err(err) => self.in_flight = Some((self.f)(err).into_future()), 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/endpoints.rs: -------------------------------------------------------------------------------- 1 | //! Built-in endpoints. 2 | 3 | pub mod body; 4 | pub mod fs; 5 | pub mod header; 6 | pub mod query; 7 | -------------------------------------------------------------------------------- /src/endpoints/fs.rs: -------------------------------------------------------------------------------- 1 | //! Endpoints for serving static contents on the file system. 2 | 3 | use { 4 | crate::{ 5 | action::{ 6 | ActionContext, // 7 | EndpointAction, 8 | Preflight, 9 | PreflightContext, 10 | }, 11 | endpoint::{Endpoint, IsEndpoint}, 12 | error::{self, Error}, 13 | output::fs::{NamedFile, OpenNamedFile}, 14 | }, 15 | futures::Poll, 16 | std::path::PathBuf, 17 | }; 18 | 19 | /// Create an endpoint which serves a specified file on the file system. 20 | #[inline] 21 | pub fn file(path: impl Into) -> File { 22 | File { path: path.into() } 23 | } 24 | 25 | #[allow(missing_docs)] 26 | #[derive(Debug)] 27 | pub struct File { 28 | path: PathBuf, 29 | } 30 | 31 | mod file { 32 | use super::*; 33 | use futures::Future as _Future; 34 | use std::marker::PhantomData; 35 | 36 | impl IsEndpoint for File {} 37 | 38 | impl Endpoint for File { 39 | type Output = (NamedFile,); 40 | type Action = FileAction; 41 | 42 | fn action(&self) -> Self::Action { 43 | FileAction { 44 | path: self.path.clone(), 45 | opening: None, 46 | _marker: PhantomData, 47 | } 48 | } 49 | } 50 | 51 | #[allow(missing_debug_implementations)] 52 | pub struct FileAction { 53 | path: PathBuf, 54 | opening: Option, 55 | _marker: PhantomData, 56 | } 57 | 58 | impl EndpointAction for FileAction { 59 | type Output = (NamedFile,); 60 | 61 | fn poll_action(&mut self, _: &mut ActionContext<'_, Bd>) -> Poll { 62 | loop { 63 | if let Some(ref mut opening) = self.opening { 64 | return opening.poll().map(|x| x.map(|x| (x,))).map_err(Into::into); 65 | } 66 | self.opening = Some(NamedFile::open(self.path.clone())); 67 | } 68 | } 69 | } 70 | } 71 | 72 | /// Create an endpoint which serves files in the specified directory. 73 | #[inline] 74 | pub fn dir(root: impl Into) -> Dir { 75 | Dir { root: root.into() } 76 | } 77 | 78 | #[allow(missing_docs)] 79 | #[derive(Debug)] 80 | pub struct Dir { 81 | root: PathBuf, 82 | } 83 | 84 | mod dir { 85 | use super::*; 86 | use futures::Future as _Future; 87 | 88 | impl IsEndpoint for Dir {} 89 | 90 | impl Endpoint for Dir { 91 | type Output = (NamedFile,); 92 | type Action = DirAction; 93 | 94 | fn action(&self) -> Self::Action { 95 | DirAction { 96 | root: self.root.clone(), 97 | state: State::Init, 98 | } 99 | } 100 | } 101 | 102 | #[allow(missing_debug_implementations)] 103 | pub struct DirAction { 104 | root: PathBuf, 105 | state: State, 106 | } 107 | 108 | enum State { 109 | Init, 110 | Opening(OpenNamedFile), 111 | } 112 | 113 | impl EndpointAction for DirAction { 114 | type Output = (NamedFile,); 115 | 116 | fn preflight( 117 | &mut self, 118 | cx: &mut PreflightContext<'_>, 119 | ) -> Result, Error> { 120 | let path = cx 121 | .cursor() 122 | .remaining_path() 123 | .percent_decode() 124 | .map(|path| PathBuf::from(path.into_owned())); 125 | let _ = cx.cursor().count(); 126 | let path = path.map_err(error::bad_request)?; 127 | 128 | let mut path = self.root.join(path); 129 | if path.is_dir() { 130 | path = path.join("index.html"); 131 | } 132 | 133 | self.state = State::Opening(NamedFile::open(path)); 134 | Ok(Preflight::Incomplete) 135 | } 136 | 137 | fn poll_action(&mut self, _: &mut ActionContext<'_, Bd>) -> Poll { 138 | match self.state { 139 | State::Init => unreachable!(), 140 | State::Opening(ref mut f) => f.poll().map(|x| x.map(|x| (x,))).map_err(Into::into), 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A combinator library for building asynchronous HTTP services. 2 | //! 3 | //! The concept and design was highly inspired by [`finch`](https://github.com/finagle/finch). 4 | //! 5 | //! # Features 6 | //! 7 | //! * Asynchronous handling powerd by futures and Tokio 8 | //! * Building an HTTP service by *combining* the primitive components 9 | //! * Type-safe routing without (unstable) procedural macros 10 | //! 11 | //! # Example 12 | //! 13 | //! ``` 14 | //! use finchers::prelude::*; 15 | //! use finchers::endpoint::syntax::path; 16 | //! 17 | //! # fn main() -> izanami::Result<()> { 18 | //! let get_post = path!(@get "/") 19 | //! .map(|id: u64| format!("GET: id={}", id)); 20 | //! 21 | //! let create_post = path!(@post "/") 22 | //! .and(endpoints::body::text()) 23 | //! .map(|data: String| format!("POST: body={}", data)); 24 | //! 25 | //! let endpoint = path!("/posts") 26 | //! .and(get_post.or(create_post)); 27 | //! 28 | //! # drop(move || -> izanami::Result<_> { 29 | //! izanami::Server::bind_tcp(&"127.0.0.1:4000".parse()?)? 30 | //! .start(endpoint.into_service()) 31 | //! # }); 32 | //! # Ok(()) 33 | //! # } 34 | //! ``` 35 | 36 | #![doc(html_root_url = "https://docs.rs/finchers/0.14.0-dev")] 37 | #![warn( 38 | missing_docs, 39 | missing_debug_implementations, 40 | nonstandard_style, 41 | rust_2018_compatibility, 42 | rust_2018_idioms, 43 | unused 44 | )] 45 | #![forbid(clippy::unimplemented)] 46 | #![doc(test(attr(deny(warnings))))] 47 | 48 | mod common; 49 | 50 | pub mod action; 51 | pub mod endpoint; 52 | pub mod endpoints; 53 | pub mod error; 54 | pub mod output; 55 | pub mod service; 56 | pub mod test; 57 | pub mod util; 58 | 59 | /// A prelude for crates using the `finchers` crate. 60 | pub mod prelude { 61 | pub use crate::endpoint; 62 | pub use crate::endpoint::{Endpoint, EndpointExt}; 63 | pub use crate::endpoints; 64 | pub use crate::error::HttpError; 65 | pub use crate::service::EndpointServiceExt; 66 | } 67 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | //! Components for constructing HTTP responses. 2 | 3 | pub mod fs; 4 | pub mod status; 5 | 6 | mod binary; 7 | mod debug; 8 | mod json; 9 | mod redirect; 10 | mod text; 11 | 12 | use either::Either; 13 | use http::{Request, Response, StatusCode}; 14 | 15 | pub use self::debug::Debug; 16 | pub use self::fs::NamedFile; 17 | pub use self::json::Json; 18 | pub use self::redirect::Redirect; 19 | 20 | /// A trait representing the value to be converted into an HTTP response. 21 | pub trait IntoResponse { 22 | /// The type of response body. 23 | type Body; 24 | 25 | /// Converts `self` into an HTTP response. 26 | fn into_response(self, request: &Request<()>) -> Response; 27 | } 28 | 29 | impl IntoResponse for Response { 30 | type Body = T; 31 | 32 | #[inline] 33 | fn into_response(self, _: &Request<()>) -> Response { 34 | self 35 | } 36 | } 37 | 38 | impl IntoResponse for () { 39 | type Body = &'static [u8]; 40 | 41 | fn into_response(self, _: &Request<()>) -> Response { 42 | let mut response = Response::new(&[] as &[u8]); 43 | *response.status_mut() = StatusCode::NO_CONTENT; 44 | response 45 | } 46 | } 47 | 48 | impl IntoResponse for (T,) { 49 | type Body = T::Body; 50 | 51 | #[inline] 52 | fn into_response(self, request: &Request<()>) -> Response { 53 | self.0.into_response(request) 54 | } 55 | } 56 | 57 | impl IntoResponse for Either 58 | where 59 | L: IntoResponse, 60 | R: IntoResponse, 61 | { 62 | type Body = izanami_util::buf_stream::Either; 63 | 64 | fn into_response(self, request: &Request<()>) -> Response { 65 | match self { 66 | Either::Left(l) => l 67 | .into_response(request) 68 | .map(izanami_util::buf_stream::Either::Left), 69 | Either::Right(r) => r 70 | .into_response(request) 71 | .map(izanami_util::buf_stream::Either::Right), 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/output/binary.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use http::header::HeaderValue; 3 | use http::{header, Request, Response}; 4 | use std::borrow::Cow; 5 | 6 | use super::IntoResponse; 7 | 8 | impl IntoResponse for &'static [u8] { 9 | type Body = &'static [u8]; 10 | 11 | #[inline] 12 | fn into_response(self, _: &Request<()>) -> Response { 13 | make_binary_response(self) 14 | } 15 | } 16 | 17 | impl IntoResponse for Vec { 18 | type Body = Vec; 19 | 20 | #[inline] 21 | fn into_response(self, _: &Request<()>) -> Response { 22 | make_binary_response(self) 23 | } 24 | } 25 | 26 | impl IntoResponse for Cow<'static, [u8]> { 27 | type Body = Cow<'static, [u8]>; 28 | 29 | #[inline] 30 | fn into_response(self, _: &Request<()>) -> Response { 31 | make_binary_response(self) 32 | } 33 | } 34 | 35 | impl IntoResponse for Bytes { 36 | type Body = Bytes; 37 | 38 | #[inline] 39 | fn into_response(self, _: &Request<()>) -> Response { 40 | make_binary_response(self) 41 | } 42 | } 43 | 44 | fn make_binary_response(body: T) -> Response { 45 | let mut response = Response::new(body); 46 | response.headers_mut().insert( 47 | header::CONTENT_TYPE, 48 | HeaderValue::from_static("application/octet-stream"), 49 | ); 50 | response 51 | } 52 | -------------------------------------------------------------------------------- /src/output/debug.rs: -------------------------------------------------------------------------------- 1 | use http::{Request, Response}; 2 | use std::fmt; 3 | 4 | use super::IntoResponse; 5 | 6 | /// An instance of `Output` representing text responses with debug output. 7 | /// 8 | /// NOTE: This wrapper is only for debugging and should not use in the production code. 9 | #[derive(Debug)] 10 | pub struct Debug(pub T); 11 | 12 | impl IntoResponse for Debug { 13 | type Body = String; 14 | 15 | fn into_response(self, _: &Request<()>) -> Response { 16 | let body = format!("{:?}", self.0); 17 | Response::builder() 18 | .header("content-type", "text/plain; charset=utf-8") 19 | .body(body) 20 | .unwrap() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/output/fs.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use std::cmp; 4 | use std::fs::Metadata; 5 | use std::io; 6 | use std::mem; 7 | use std::path::PathBuf; 8 | 9 | use futures::{try_ready, Async, Future, Poll}; 10 | use izanami_util::buf_stream::BufStream; 11 | use tokio::fs::file::{File, MetadataFuture, OpenFuture}; 12 | use tokio::io::AsyncRead; 13 | 14 | use bytes::{BufMut, Bytes, BytesMut}; 15 | use http::{header, Request, Response}; 16 | use mime_guess::guess_mime_type; 17 | 18 | use super::IntoResponse; 19 | 20 | /// An instance of `Output` representing a file on the file system. 21 | #[derive(Debug)] 22 | pub struct NamedFile { 23 | file: File, 24 | meta: Metadata, 25 | path: PathBuf, 26 | } 27 | 28 | impl NamedFile { 29 | #[allow(missing_docs)] 30 | pub fn open(path: PathBuf) -> OpenNamedFile { 31 | OpenNamedFile { 32 | state: State::Opening(File::open(path.clone())), 33 | path: Some(path), 34 | } 35 | } 36 | } 37 | 38 | #[allow(missing_docs)] 39 | #[derive(Debug)] 40 | pub struct OpenNamedFile { 41 | state: State, 42 | path: Option, 43 | } 44 | 45 | #[derive(Debug)] 46 | enum State { 47 | Opening(OpenFuture), 48 | Metadata(MetadataFuture), 49 | Done, 50 | } 51 | 52 | impl Future for OpenNamedFile { 53 | type Item = NamedFile; 54 | type Error = io::Error; 55 | 56 | fn poll(&mut self) -> Poll { 57 | enum Polled { 58 | Opening(File), 59 | Metadata((File, Metadata)), 60 | } 61 | 62 | loop { 63 | let polled = match self.state { 64 | State::Opening(ref mut f) => Polled::Opening(try_ready!(f.poll())), 65 | State::Metadata(ref mut f) => Polled::Metadata(try_ready!(f.poll())), 66 | State::Done => panic!("The future cannot poll twice."), 67 | }; 68 | 69 | match (mem::replace(&mut self.state, State::Done), polled) { 70 | (State::Opening(..), Polled::Opening(file)) => { 71 | self.state = State::Metadata(file.metadata()); 72 | } 73 | (State::Metadata(..), Polled::Metadata((file, meta))) => { 74 | let named_file = NamedFile { 75 | file, 76 | meta, 77 | path: self.path.take().unwrap(), 78 | }; 79 | return Ok(Async::Ready(named_file)); 80 | } 81 | _ => unreachable!("unexpected condition"), 82 | } 83 | } 84 | } 85 | } 86 | 87 | impl IntoResponse for NamedFile { 88 | type Body = FileStream; 89 | 90 | fn into_response(self, _: &Request<()>) -> Response { 91 | let NamedFile { file, meta, path } = self; 92 | 93 | let body = FileStream::new(file, &meta); 94 | 95 | let content_type = guess_mime_type(&path); 96 | 97 | Response::builder() 98 | .header(header::CONTENT_LENGTH, meta.len()) 99 | .header(header::CONTENT_TYPE, content_type.as_ref()) 100 | .body(body) 101 | .unwrap() 102 | } 103 | } 104 | 105 | #[allow(missing_docs)] 106 | #[derive(Debug)] 107 | pub struct FileStream { 108 | file: File, 109 | buf: BytesMut, 110 | buf_size: usize, 111 | len: u64, 112 | } 113 | 114 | impl FileStream { 115 | fn new(file: File, meta: &Metadata) -> FileStream { 116 | let buf_size = optimal_buf_size(&meta); 117 | let len = meta.len(); 118 | FileStream { 119 | file, 120 | buf: BytesMut::new(), 121 | buf_size, 122 | len, 123 | } 124 | } 125 | } 126 | 127 | impl BufStream for FileStream { 128 | type Item = io::Cursor; 129 | type Error = io::Error; 130 | 131 | fn poll_buf(&mut self) -> Result>, Self::Error> { 132 | if self.len == 0 { 133 | return Ok(Async::Ready(None)); 134 | } 135 | 136 | if self.buf.remaining_mut() < self.buf_size { 137 | self.buf.reserve(self.buf_size); 138 | } 139 | 140 | let n = match try_ready!(self.file.read_buf(&mut self.buf)) { 141 | 0 => return Ok(Async::Ready(None)), 142 | n => n as u64, 143 | }; 144 | 145 | let mut chunk = self.buf.take().freeze(); 146 | if n > self.len { 147 | chunk = chunk.split_to(self.len as usize); 148 | self.len = 0; 149 | } else { 150 | self.len = n; 151 | } 152 | 153 | Ok(Async::Ready(Some(io::Cursor::new(chunk)))) 154 | } 155 | } 156 | 157 | fn optimal_buf_size(meta: &Metadata) -> usize { 158 | let blk_size = get_block_size(meta); 159 | cmp::min(blk_size as u64, meta.len()) as usize 160 | } 161 | 162 | #[cfg(unix)] 163 | fn get_block_size(meta: &Metadata) -> usize { 164 | use std::os::unix::fs::MetadataExt; 165 | meta.blksize() as usize 166 | } 167 | 168 | #[cfg(not(unix))] 169 | fn get_block_size(_: &Metadata) -> usize { 170 | 8192 171 | } 172 | -------------------------------------------------------------------------------- /src/output/json.rs: -------------------------------------------------------------------------------- 1 | use http::header::HeaderValue; 2 | use http::{header, Request, Response, StatusCode}; 3 | use serde::Serialize; 4 | use serde_json; 5 | use serde_json::Value; 6 | 7 | use super::IntoResponse; 8 | 9 | /// An instance of `Output` representing statically typed JSON responses. 10 | #[derive(Debug)] 11 | pub struct Json(pub T); 12 | 13 | impl From for Json { 14 | #[inline] 15 | fn from(inner: T) -> Self { 16 | Json(inner) 17 | } 18 | } 19 | 20 | impl IntoResponse for Json { 21 | type Body = String; 22 | 23 | fn into_response(self, _: &Request<()>) -> Response { 24 | let (status, body) = match serde_json::to_string(&self.0) { 25 | Ok(body) => (StatusCode::OK, body), 26 | Err(err) => ( 27 | StatusCode::INTERNAL_SERVER_ERROR, 28 | serde_json::json!({ 29 | "code": 500, 30 | "message": format!("failed to construct JSON response: {}", err), 31 | }) 32 | .to_string(), 33 | ), 34 | }; 35 | 36 | let mut response = Response::new(body); 37 | *response.status_mut() = status; 38 | response.headers_mut().insert( 39 | header::CONTENT_TYPE, 40 | HeaderValue::from_static("application/json"), 41 | ); 42 | response 43 | } 44 | } 45 | 46 | impl IntoResponse for Value { 47 | type Body = String; 48 | 49 | fn into_response(self, _: &Request<()>) -> Response { 50 | let body = self.to_string(); 51 | let mut response = Response::new(body); 52 | response.headers_mut().insert( 53 | header::CONTENT_TYPE, 54 | HeaderValue::from_static("application/json"), 55 | ); 56 | 57 | response 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/output/redirect.rs: -------------------------------------------------------------------------------- 1 | use http::header::{HeaderValue, LOCATION}; 2 | use http::{Request, Response, StatusCode}; 3 | 4 | use super::IntoResponse; 5 | 6 | /// An instance of `Output` representing redirect responses. 7 | #[derive(Debug, Clone)] 8 | pub struct Redirect { 9 | status: StatusCode, 10 | location: Option, 11 | } 12 | 13 | impl Redirect { 14 | /// Create a new `Redirect` with the specified HTTP status code. 15 | pub fn new(status: StatusCode) -> Redirect { 16 | Redirect { 17 | status, 18 | location: None, 19 | } 20 | } 21 | 22 | /// Sets the value of header field `Location`. 23 | pub fn location(self, location: &'static str) -> Redirect { 24 | Redirect { 25 | location: Some(HeaderValue::from_static(location)), 26 | ..self 27 | } 28 | } 29 | } 30 | 31 | macro_rules! impl_constructors { 32 | ($($name:ident => $STATUS:ident;)*) => {$( 33 | pub fn $name(location: &'static str) -> Redirect { 34 | Redirect { 35 | status: StatusCode::$STATUS, 36 | location: Some(HeaderValue::from_static(location)), 37 | } 38 | } 39 | )*} 40 | } 41 | 42 | #[allow(missing_docs)] 43 | impl Redirect { 44 | impl_constructors! { 45 | moved_permanently => MOVED_PERMANENTLY; 46 | found => FOUND; 47 | see_other => SEE_OTHER; 48 | temporary_redirect => TEMPORARY_REDIRECT; 49 | permanent_redirect => PERMANENT_REDIRECT; 50 | } 51 | 52 | pub fn not_modified() -> Redirect { 53 | Redirect::new(StatusCode::NOT_MODIFIED) 54 | } 55 | } 56 | 57 | impl IntoResponse for Redirect { 58 | type Body = (); 59 | 60 | fn into_response(self, _: &Request<()>) -> Response { 61 | let mut response = Response::new(()); 62 | *response.status_mut() = self.status; 63 | if let Some(location) = self.location { 64 | response.headers_mut().insert(LOCATION, location); 65 | } 66 | response 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/output/status.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use http::{Request, Response, StatusCode}; 4 | 5 | use super::IntoResponse; 6 | 7 | impl IntoResponse for StatusCode { 8 | type Body = &'static [u8]; 9 | 10 | #[inline] 11 | fn into_response(self, _: &Request<()>) -> Response { 12 | let mut response = Response::new(&[] as &[u8]); 13 | *response.status_mut() = self; 14 | response 15 | } 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct Created(pub T); 20 | 21 | impl IntoResponse for Created { 22 | type Body = T::Body; 23 | 24 | fn into_response(self, request: &Request<()>) -> Response { 25 | let mut response = self.0.into_response(request); 26 | *response.status_mut() = StatusCode::CREATED; 27 | response 28 | } 29 | } 30 | 31 | #[derive(Debug)] 32 | pub struct NoContent; 33 | 34 | impl IntoResponse for NoContent { 35 | type Body = (); 36 | 37 | fn into_response(self, _: &Request<()>) -> Response { 38 | let mut response = Response::new(()); 39 | *response.status_mut() = StatusCode::NO_CONTENT; 40 | response 41 | } 42 | } 43 | 44 | #[derive(Debug)] 45 | pub struct Status { 46 | pub value: T, 47 | pub status: StatusCode, 48 | } 49 | 50 | impl IntoResponse for Status { 51 | type Body = T::Body; 52 | 53 | fn into_response(self, request: &Request<()>) -> Response { 54 | let mut response = self.value.into_response(request); 55 | *response.status_mut() = self.status; 56 | response 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/output/text.rs: -------------------------------------------------------------------------------- 1 | use http::header::HeaderValue; 2 | use http::{header, Request, Response}; 3 | use std::borrow::Cow; 4 | 5 | use super::IntoResponse; 6 | 7 | impl IntoResponse for &'static str { 8 | type Body = &'static str; 9 | 10 | #[inline] 11 | fn into_response(self, _: &Request<()>) -> Response { 12 | make_text_response(self) 13 | } 14 | } 15 | 16 | impl IntoResponse for String { 17 | type Body = String; 18 | 19 | #[inline] 20 | fn into_response(self, _: &Request<()>) -> Response { 21 | make_text_response(self) 22 | } 23 | } 24 | 25 | impl IntoResponse for Cow<'static, str> { 26 | type Body = Cow<'static, str>; 27 | 28 | #[inline] 29 | fn into_response(self, _: &Request<()>) -> Response { 30 | make_text_response(self) 31 | } 32 | } 33 | 34 | fn make_text_response(body: T) -> Response { 35 | let mut response = Response::new(body); 36 | response.headers_mut().insert( 37 | header::CONTENT_TYPE, 38 | HeaderValue::from_static("text/plain; charset=utf-8"), 39 | ); 40 | response 41 | } 42 | -------------------------------------------------------------------------------- /src/server/builder.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | 3 | use futures::{future, Future}; 4 | use http::{Request, Response}; 5 | use hyper::body::{Body, Payload}; 6 | use tower_service; 7 | use tower_service::NewService; 8 | 9 | use super::error::{ServerError, ServerResult}; 10 | use super::http_server::ServerConfig; 11 | 12 | /// A builder of HTTP service. 13 | #[derive(Debug)] 14 | pub struct ServiceBuilder { 15 | new_service: S, 16 | } 17 | 18 | impl ServiceBuilder 19 | where 20 | S: NewService, 21 | { 22 | /// Creates a new `ServerBuilder` from the specified NewService. 23 | pub fn new(new_service: S) -> Self { 24 | ServiceBuilder { new_service } 25 | } 26 | } 27 | 28 | impl NewService for ServiceBuilder 29 | where 30 | S: NewService, 31 | { 32 | type Request = S::Request; 33 | type Response = S::Response; 34 | type Error = S::Error; 35 | type Service = S::Service; 36 | type InitError = S::InitError; 37 | type Future = S::Future; 38 | 39 | #[inline] 40 | fn new_service(&self) -> Self::Future { 41 | self.new_service.new_service() 42 | } 43 | } 44 | 45 | impl ServiceBuilder 46 | where 47 | S: NewService, Response = Response> + Send + Sync + 'static, 48 | S::Error: Into>, 49 | S::InitError: Into>, 50 | S::Service: Send, 51 | S::Future: Send + 'static, 52 | ::Future: Send + 'static, 53 | Bd: Payload, 54 | { 55 | /// Start the server with the specified configuration. 56 | pub fn serve(self, config: impl ServerConfig) -> ServerResult<()> { 57 | self.serve_with_graceful_shutdown(config, future::empty::<(), ()>()) 58 | } 59 | 60 | /// Start the server with the specified configuration and shutdown signal. 61 | pub fn serve_with_graceful_shutdown( 62 | self, 63 | server_config: impl ServerConfig, 64 | signal: impl Future + Send + 'static, 65 | ) -> ServerResult<()> { 66 | server_config 67 | .build() 68 | .map_err(ServerError::config)? 69 | .serve_with_graceful_shutdown(self, signal); 70 | Ok(()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/server/error.rs: -------------------------------------------------------------------------------- 1 | use failure::Error; 2 | use std::error; 3 | use std::fmt; 4 | 5 | /// A type alias of `Result` whose error type is restrected to `ServerError`. 6 | pub type ServerResult = Result; 7 | 8 | #[derive(Debug)] 9 | enum ServerErrorKind { 10 | Config(Error), 11 | Custom(Error), 12 | } 13 | 14 | /// The error type which will be returned from `ServiceBuilder::serve()`. 15 | #[derive(Debug)] 16 | pub struct ServerError { 17 | kind: ServerErrorKind, 18 | } 19 | 20 | impl ServerError { 21 | pub(super) fn config(err: impl Into) -> ServerError { 22 | ServerError { 23 | kind: ServerErrorKind::Config(err.into()), 24 | } 25 | } 26 | 27 | /// Create a value of `ServerError` from an arbitrary error value. 28 | pub fn custom(err: impl Into) -> ServerError { 29 | ServerError { 30 | kind: ServerErrorKind::Custom(err.into()), 31 | } 32 | } 33 | } 34 | 35 | impl fmt::Display for ServerError { 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 | use self::ServerErrorKind::*; 38 | match self.kind { 39 | Config(ref e) => write!(f, "failed to build server config: {}", e), 40 | Custom(ref e) => fmt::Display::fmt(e, f), 41 | } 42 | } 43 | } 44 | 45 | impl error::Error for ServerError { 46 | fn description(&self) -> &str { 47 | "failed to start the server" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | use std::{error, fmt}; 4 | 5 | /// A type which has no possible values. 6 | #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq)] 7 | pub enum Never {} 8 | 9 | impl Never { 10 | /// Consume itself and transform into an arbitrary type. 11 | /// 12 | /// NOTE: This function has never been actually called because the possible values don't exist. 13 | pub fn never_into(self) -> T { 14 | match self {} 15 | } 16 | } 17 | 18 | impl fmt::Display for Never { 19 | fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | match *self {} 21 | } 22 | } 23 | 24 | impl error::Error for Never { 25 | fn description(&self) -> &str { 26 | match *self {} 27 | } 28 | 29 | fn cause(&self) -> Option<&dyn error::Error> { 30 | match *self {} 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/endpoint/and.rs: -------------------------------------------------------------------------------- 1 | use finchers::endpoint::{unit, value, EndpointExt}; 2 | use finchers::test; 3 | use matches::assert_matches; 4 | 5 | #[test] 6 | fn test_and_all_ok() { 7 | let mut runner = test::runner(value("Hello").and(value("world"))); 8 | 9 | assert_matches!(runner.apply_raw("/"), Ok(("Hello", "world"))); 10 | } 11 | 12 | #[test] 13 | fn test_and_flatten() { 14 | let mut runner = test::runner( 15 | value("Hello") 16 | .and(unit()) 17 | .and(value("world").and(value(":)"))), 18 | ); 19 | 20 | assert_matches!(runner.apply_raw("/"), Ok(("Hello", "world", ":)"))); 21 | } 22 | -------------------------------------------------------------------------------- /tests/endpoint/and_then.rs: -------------------------------------------------------------------------------- 1 | use finchers::prelude::*; 2 | use finchers::test; 3 | use futures::future; 4 | use matches::assert_matches; 5 | 6 | #[test] 7 | fn test_and_then_1() { 8 | let mut runner = test::runner( 9 | endpoint::value("Foo") // 10 | .and_then(|_| future::ok::<_, finchers::util::Never>("Bar")), 11 | ); 12 | assert_matches!( 13 | runner.apply("/"), 14 | Ok(s) if s == "Bar" 15 | ) 16 | } 17 | 18 | #[test] 19 | fn test_and_then_2() { 20 | let mut runner = test::runner( 21 | endpoint::value("Foo") // 22 | .and_then(|_| future::err::<(), _>(finchers::error::bad_request("Bar"))), 23 | ); 24 | assert_matches!(runner.apply("/"), Err(..)) 25 | } 26 | -------------------------------------------------------------------------------- /tests/endpoint/boxed.rs: -------------------------------------------------------------------------------- 1 | use finchers; 2 | use finchers::endpoint::{syntax, IsEndpoint}; 3 | use finchers::prelude::*; 4 | use finchers::test; 5 | use matches::assert_matches; 6 | 7 | #[test] 8 | fn test_boxed() { 9 | let endpoint = syntax::verb::get().and(syntax::segment("foo")); 10 | let mut runner = test::runner(endpoint.boxed()); 11 | assert_matches!(runner.apply_raw("/foo"), Ok(())); 12 | } 13 | 14 | #[test] 15 | fn test_boxed_local() { 16 | let endpoint = syntax::verb::get().and(syntax::segment("foo")); 17 | let mut runner = test::runner(endpoint.boxed_local()); 18 | assert_matches!(runner.apply_raw("/foo"), Ok(..)); 19 | } 20 | -------------------------------------------------------------------------------- /tests/endpoint/macros.rs: -------------------------------------------------------------------------------- 1 | use finchers::endpoint::syntax; 2 | 3 | #[test] 4 | fn compile_test_path() { 5 | let _ = syntax::path!("/"); 6 | let _ = syntax::path!("/foo"); 7 | let _ = syntax::path!("/foo/"); 8 | let _ = syntax::path!("/foo/"); 9 | let _ = syntax::path!("/foo/<..std::path::PathBuf>"); 10 | 11 | let _ = syntax::path!(@get "/"); 12 | let _ = syntax::path!(@get "/foo//bar"); 13 | let _ = syntax::path!(@get "/foo///bar/"); 14 | let _ = syntax::path!(@get "/"); 15 | let _ = syntax::path!(@get "//"); 16 | let _ = syntax::path!(@get "/posts//repo/<..String>"); 17 | } 18 | 19 | // #[test] 20 | // fn compile_test_routes() { 21 | // use finchers::endpoint::syntax; 22 | 23 | // let e1 = syntax::segment("foo"); 24 | // let e2 = routes!(e1, syntax::segment("bar"), syntax::segment("baz")); 25 | // let e3 = routes!(syntax::segment("foobar"), e2); 26 | // let _e4 = routes!(syntax::segment("foobar"), e3,); 27 | // } 28 | 29 | #[test] 30 | fn test_extract_path_statics() { 31 | let mut runner = finchers::test::runner({ 32 | syntax::path!("/foo/bar") // 33 | }); 34 | 35 | matches::assert_matches!(runner.apply_raw("/foo/bar"), Ok(())); 36 | matches::assert_matches!(runner.apply_raw("/"), Err(..)); 37 | } 38 | 39 | #[test] 40 | fn test_extract_path_single_param() { 41 | let mut runner = finchers::test::runner({ syntax::path!("/foo/") }); 42 | 43 | matches::assert_matches!(runner.apply("/foo/42"), Ok(42_i32)); 44 | matches::assert_matches!(runner.apply("/"), Err(..)); 45 | matches::assert_matches!(runner.apply("/foo/bar"), Err(..)); 46 | } 47 | 48 | #[test] 49 | fn test_extract_path_catch_all_param() { 50 | let mut runner = finchers::test::runner({ syntax::path!("/foo/<..String>") }); 51 | 52 | assert_eq!(runner.apply("/foo/").ok(), Some("".into())); 53 | assert_eq!(runner.apply("/foo/bar/baz").ok(), Some("bar/baz".into())); 54 | matches::assert_matches!(runner.apply("/"), Err(..)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/endpoint/map.rs: -------------------------------------------------------------------------------- 1 | use finchers::prelude::*; 2 | use finchers::test; 3 | use matches::assert_matches; 4 | 5 | #[test] 6 | fn test_map() { 7 | let mut runner = test::runner(endpoint::value("Foo").map(|_| "Bar")); 8 | assert_matches!(runner.apply("/"), Ok("Bar")); 9 | } 10 | -------------------------------------------------------------------------------- /tests/endpoint/mod.rs: -------------------------------------------------------------------------------- 1 | mod and; 2 | mod and_then; 3 | mod boxed; 4 | mod macros; 5 | mod map; 6 | mod or; 7 | mod or_strict; 8 | mod recover; 9 | mod syntax; 10 | -------------------------------------------------------------------------------- /tests/endpoint/or.rs: -------------------------------------------------------------------------------- 1 | use finchers::endpoint::syntax; 2 | use finchers::prelude::*; 3 | use finchers::test; 4 | use matches::assert_matches; 5 | 6 | #[test] 7 | fn test_or_1() { 8 | let mut runner = test::runner({ 9 | let e1 = syntax::segment("foo").and(endpoint::value("foo")); 10 | let e2 = syntax::segment("bar").and(endpoint::value("bar")); 11 | e1.or(e2) 12 | }); 13 | 14 | assert_matches!(runner.apply("/foo"), Ok(..)); 15 | 16 | assert_matches!(runner.apply("/bar"), Ok(..)); 17 | } 18 | 19 | #[test] 20 | fn test_or_choose_longer_segments() { 21 | let mut runner = test::runner({ 22 | let e1 = syntax::segment("foo") // 23 | .and(endpoint::value("foo")); 24 | let e2 = syntax::segment("foo") 25 | .and(syntax::segment("bar")) 26 | .and(endpoint::value("foobar")); 27 | e1.or(e2) 28 | }); 29 | 30 | assert_matches!(runner.apply("/foo"), Ok(..)); 31 | assert_matches!(runner.apply("/foo/bar"), Ok(..)); 32 | } 33 | -------------------------------------------------------------------------------- /tests/endpoint/or_strict.rs: -------------------------------------------------------------------------------- 1 | use finchers::endpoint::syntax; 2 | use finchers::endpoints::{body, query}; 3 | use finchers::test; 4 | 5 | use http::Request; 6 | use matches::assert_matches; 7 | use serde::de; 8 | use serde::de::IntoDeserializer; 9 | use std::fmt; 10 | use std::iter::FromIterator; 11 | use std::marker::PhantomData; 12 | 13 | #[allow(missing_debug_implementations)] 14 | struct CSVSeqVisitor { 15 | _marker: PhantomData (I, T)>, 16 | } 17 | 18 | impl<'de, I, T> de::Visitor<'de> for CSVSeqVisitor 19 | where 20 | I: FromIterator, 21 | T: de::Deserialize<'de>, 22 | { 23 | type Value = I; 24 | 25 | fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | f.write_str("a string") 27 | } 28 | 29 | fn visit_str(self, s: &str) -> Result 30 | where 31 | E: de::Error, 32 | { 33 | s.split(',') 34 | .map(|s| de::Deserialize::deserialize(s.into_deserializer())) 35 | .collect() 36 | } 37 | } 38 | 39 | fn from_csv<'de, D, I, T>(de: D) -> Result 40 | where 41 | D: de::Deserializer<'de>, 42 | I: FromIterator, 43 | T: de::Deserialize<'de>, 44 | { 45 | de.deserialize_str(CSVSeqVisitor { 46 | _marker: PhantomData, 47 | }) 48 | } 49 | 50 | #[derive(Debug, serde::Deserialize, PartialEq)] 51 | struct Param { 52 | query: String, 53 | count: Option, 54 | #[serde(deserialize_with = "from_csv", default)] 55 | tags: Vec, 56 | } 57 | 58 | #[test] 59 | fn test_or_strict() { 60 | let query_str = "query=rustlang&count=42&tags=tokio,hyper"; 61 | 62 | let mut runner = test::runner({ 63 | use finchers::endpoint::EndpointExt; 64 | 65 | let query = syntax::verb::get().and(query::required::()); 66 | let form = syntax::verb::post().and(body::urlencoded::()); 67 | query.or_strict(form) 68 | }); 69 | 70 | assert_matches!( 71 | runner.apply(format!("/?{}", query_str)), 72 | Ok(ref param) if *param == Param { 73 | query: "rustlang".into(), 74 | count: Some(42), 75 | tags: vec!["tokio".into(), "hyper".into()] 76 | } 77 | ); 78 | 79 | assert_matches!( 80 | runner.apply(Request::post("/") 81 | .header("content-type", "application/x-www-form-urlencoded") 82 | .body(query_str)), 83 | Ok(ref param) if *param == Param { 84 | query: "rustlang".into(), 85 | count: Some(42), 86 | tags: vec!["tokio".into(), "hyper".into()] 87 | } 88 | ); 89 | 90 | assert_matches!(runner.apply("/"), Err(..)); 91 | 92 | assert_matches!( 93 | runner.apply(Request::delete(format!("/?{}", query_str))), 94 | Err(..) 95 | ); 96 | 97 | assert_matches!( 98 | runner.apply( 99 | Request::put("/") 100 | .header("content-type", "application/x-www-form-urlencoded") 101 | .body(query_str) 102 | ), 103 | Err(..) 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /tests/endpoint/recover.rs: -------------------------------------------------------------------------------- 1 | use finchers::error::Error; 2 | use finchers::prelude::*; 3 | use finchers::test; 4 | use matches::assert_matches; 5 | 6 | #[test] 7 | fn test_recover() { 8 | #[derive(Debug)] 9 | struct Id(Option); 10 | 11 | let mut runner = test::runner( 12 | endpoint::syntax::path!(@get "/foo/bar/") 13 | .map(|id| Id(Some(id))) 14 | .recover(|_: Error| Ok::<_, finchers::error::Error>(Id(None))), 15 | ); 16 | 17 | assert_matches!(runner.apply("/foo/bar/42"), Ok(Id(Some(42)))); 18 | assert_matches!(runner.apply("/foo/bar"), Ok(Id(None))); 19 | // assert_matches!(runner.apply("/foo/bar/baz"), Err(..)); 20 | } 21 | -------------------------------------------------------------------------------- /tests/endpoint/syntax.rs: -------------------------------------------------------------------------------- 1 | use finchers::endpoint::syntax; 2 | use finchers::prelude::*; 3 | use finchers::test; 4 | use matches::assert_matches; 5 | 6 | #[test] 7 | fn test_match_single_segment() { 8 | let mut runner = test::runner(syntax::segment("foo")); 9 | 10 | assert_matches!(runner.apply_raw("/foo"), Ok(())); 11 | assert_matches!(runner.apply_raw("/bar"), Err(..)); 12 | } 13 | 14 | #[test] 15 | fn test_match_multi_segments() { 16 | let mut runner = test::runner({ syntax::segment("foo").and(syntax::segment("bar")) }); 17 | 18 | assert_matches!(runner.apply_raw("/foo/bar"), Ok(())); 19 | assert_matches!(runner.apply_raw("/foo/bar/"), Ok(())); 20 | assert_matches!(runner.apply_raw("/foo/bar/baz"), Ok(())); 21 | assert_matches!(runner.apply_raw("/foo"), Err(..)); 22 | assert_matches!(runner.apply_raw("/foo/baz"), Err(..)); 23 | } 24 | 25 | #[test] 26 | fn test_match_encoded_path() { 27 | let mut runner = test::runner(syntax::segment("foo/bar")); 28 | 29 | assert_matches!(runner.apply_raw("/foo%2Fbar"), Ok(())); 30 | assert_matches!(runner.apply_raw("/foo/bar"), Err(..)); 31 | } 32 | 33 | #[test] 34 | fn test_extract_integer() { 35 | let mut runner = test::runner(syntax::param::()); 36 | 37 | assert_matches!(runner.apply("/42"), Ok(42i32)); 38 | assert_matches!(runner.apply("/foo"), Err(..)); 39 | } 40 | 41 | #[test] 42 | fn test_extract_strings() { 43 | let mut runner = test::runner(syntax::segment("foo").and(syntax::remains::())); 44 | 45 | assert_matches!( 46 | runner.apply("/foo/bar/baz/"), 47 | Ok(ref s) if s == "bar/baz/" 48 | ); 49 | } 50 | 51 | // #[test] 52 | // fn test_path_macro() { 53 | // let mut runner = test::runner( 54 | // path!(@get / "posts" / u32 / "stars" /) 55 | // .map(|id: u32| format!("id={}", id)) 56 | // .with_output::<(String,)>(), 57 | // ); 58 | // assert_matches!( 59 | // runner.apply("/posts/42/stars"), 60 | // Ok(ref s) if s == "id=42" 61 | // ); 62 | // } 63 | -------------------------------------------------------------------------------- /tests/endpoints/body.rs: -------------------------------------------------------------------------------- 1 | use finchers::endpoints::body; 2 | use finchers::test; 3 | use http::Request; 4 | use matches::assert_matches; 5 | 6 | #[test] 7 | fn test_body_text() { 8 | let message = "The quick brown fox jumps over the lazy dog"; 9 | 10 | let mut runner = test::runner(body::text()); 11 | 12 | assert_matches!( 13 | runner.apply(Request::post("/") 14 | .header("content-type", "text/plain; charset=utf-8") 15 | .body(message)), 16 | Ok(ref s) if s == message 17 | ); 18 | } 19 | 20 | #[test] 21 | fn test_body_json() { 22 | #[derive(Debug, PartialEq, serde::Deserialize)] 23 | struct Param { 24 | text: String, 25 | } 26 | 27 | let mut runner = test::runner(body::json::()); 28 | 29 | assert_matches!( 30 | runner.apply(Request::post("/") 31 | .header("content-type", "application/json") 32 | .body(r#"{ "text": "TRPL2" }"#)), 33 | Ok(ref param) if *param == Param { text: "TRPL2".into() } 34 | ); 35 | 36 | // missing Content-type 37 | assert_matches!( 38 | runner.apply(Request::post("/").body(r#"{ "text": "TRPL2" }"#)), 39 | Err(..) 40 | ); 41 | 42 | // invalid content-type 43 | assert_matches!( 44 | runner.apply( 45 | Request::post("/") 46 | .header("content-type", "text/plain") 47 | .body(r#"{ "text": "TRPL2" }"#) 48 | ), 49 | Err(..) 50 | ); 51 | 52 | // invalid data 53 | assert_matches!( 54 | runner.apply( 55 | Request::post("/") 56 | .header("content-type", "application/json") 57 | .body(r#"invalid JSON data"#) 58 | ), 59 | Err(..) 60 | ); 61 | } 62 | 63 | #[test] 64 | fn test_body_urlencoded() { 65 | #[derive(Debug, PartialEq, serde::Deserialize)] 66 | struct AccessTokenRequest { 67 | grant_type: String, 68 | code: String, 69 | redirect_uri: String, 70 | } 71 | 72 | let form_str = r#"grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb"#; 73 | 74 | let mut runner = test::runner(body::urlencoded::()); 75 | 76 | assert_matches!( 77 | runner.apply(Request::post("/") 78 | .header("content-type", "application/x-www-form-urlencoded") 79 | .body(form_str)), 80 | Ok(ref req) if *req == AccessTokenRequest { 81 | grant_type: "authorization_code".into(), 82 | code: "SplxlOBeZQQYbYS6WxSbIA".into(), 83 | redirect_uri: "https://client.example.com/cb".into(), 84 | } 85 | ); 86 | 87 | // missing Content-type 88 | assert_matches!(runner.apply(Request::post("/").body(form_str)), Err(..)); 89 | 90 | // invalid content-type 91 | assert_matches!( 92 | runner.apply( 93 | Request::post("/") 94 | .header("content-type", "text/plain") 95 | .body(form_str) 96 | ), 97 | Err(..) 98 | ); 99 | 100 | // invalid data 101 | assert_matches!( 102 | runner.apply( 103 | Request::post("/") 104 | .header("content-type", "application/x-www-form-urlencoded") 105 | .body(r#"{ "graht_code": "authorization_code" }"#) 106 | ), 107 | Err(..) 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /tests/endpoints/cookie.rs: -------------------------------------------------------------------------------- 1 | use cookie::Cookie; 2 | use finchers::input::Cookies; 3 | use finchers::prelude::*; 4 | use finchers::test; 5 | use http::Request; 6 | 7 | #[test] 8 | fn test_cookies_get() { 9 | let mut runner = test::runner({ 10 | endpoints::cookie::cookies().map(|cookies: Cookies| cookies.get("session-id")) 11 | }); 12 | 13 | assert_matches!( 14 | runner.apply(Request::get("/") 15 | .header("cookie", "session-id=xxxx")), 16 | Ok(Some(ref cookie)) 17 | if cookie.name() == "session-id" && 18 | cookie.value() == "xxxx" 19 | ); 20 | } 21 | 22 | #[test] 23 | fn test_cookies_add() { 24 | let mut runner = test::runner({ 25 | endpoints::cookie::cookies().map(|mut cookies: Cookies| { 26 | cookies.add(Cookie::new("session-id", "xxxx")); 27 | }) 28 | }); 29 | 30 | let response = runner.perform("/").unwrap(); 31 | 32 | let h_str = response 33 | .headers() 34 | .get("set-cookie") 35 | .expect("the header set-cookie is missing") 36 | .to_str() 37 | .unwrap(); 38 | let cookie = Cookie::parse_encoded(h_str).expect("failed to parse Set-Cookie"); 39 | 40 | assert_eq!(cookie.name(), "session-id"); 41 | assert_eq!(cookie.value(), "xxxx"); 42 | } 43 | 44 | #[test] 45 | fn test_cookies_remove() { 46 | let mut runner = test::runner({ 47 | endpoints::cookie::cookies().map(|mut cookies: Cookies| { 48 | cookies.remove(Cookie::named("session-id")); 49 | }) 50 | }); 51 | 52 | let response = runner 53 | .perform(Request::get("/").header("cookie", "session-id=xxxx")) 54 | .unwrap(); 55 | 56 | let h_str = response 57 | .headers() 58 | .get("set-cookie") 59 | .expect("the header set-cookie is missing") 60 | .to_str() 61 | .unwrap(); 62 | let cookie = Cookie::parse_encoded(h_str).expect("failed to parse Set-Cookie"); 63 | 64 | assert_eq!(cookie.name(), "session-id"); 65 | assert_eq!(cookie.value(), ""); 66 | } 67 | -------------------------------------------------------------------------------- /tests/endpoints/header.rs: -------------------------------------------------------------------------------- 1 | use finchers::prelude::*; 2 | use finchers::test; 3 | 4 | use http::header::CONTENT_TYPE; 5 | use http::Request; 6 | use matches::assert_matches; 7 | use mime; 8 | use mime::Mime; 9 | 10 | #[test] 11 | fn test_header_raw() { 12 | let mut runner = test::runner(endpoints::header::raw(CONTENT_TYPE)); 13 | 14 | assert_matches!( 15 | runner.apply(Request::get("/") 16 | .header("content-type", "application/json")), 17 | Ok(Some(ref value)) if value.as_bytes() == &b"application/json"[..] 18 | ); 19 | 20 | assert_matches!(runner.apply(Request::new(())), Ok(None)); 21 | } 22 | 23 | #[test] 24 | fn test_header_parse() { 25 | let mut runner = test::runner( 26 | endpoints::header::parse::("content-type"), // 27 | ); 28 | 29 | assert_matches!( 30 | runner.apply(Request::post("/") 31 | .header("content-type", "application/json")), 32 | Ok(ref m) if *m == mime::APPLICATION_JSON 33 | ); 34 | 35 | assert_matches!(runner.apply(Request::new(())), Err(..)); 36 | } 37 | 38 | #[test] 39 | fn test_header_optional() { 40 | let mut runner = test::runner( 41 | endpoints::header::optional::("content-type"), // 42 | ); 43 | 44 | assert_matches!( 45 | runner.apply(Request::post("/") 46 | .header("content-type", "application/json")), 47 | Ok(Some(ref m)) if *m == mime::APPLICATION_JSON 48 | ); 49 | 50 | assert_matches!(runner.apply(Request::new(())), Ok(None)); 51 | } 52 | -------------------------------------------------------------------------------- /tests/endpoints/mod.rs: -------------------------------------------------------------------------------- 1 | mod body; 2 | //mod cookie; 3 | mod header; 4 | mod query; 5 | //mod upgrade; 6 | -------------------------------------------------------------------------------- /tests/endpoints/query.rs: -------------------------------------------------------------------------------- 1 | use finchers::endpoints::query; 2 | use finchers::test; 3 | use matches::assert_matches; 4 | 5 | #[test] 6 | fn test_query_raw() { 7 | let mut runner = test::runner( 8 | query::raw(), // 9 | ); 10 | 11 | assert_matches!( 12 | runner.apply("/?foo=bar"), 13 | Ok(Some(ref s)) if s == "foo=bar" 14 | ); 15 | 16 | assert_matches!(runner.apply("/"), Ok(None)); 17 | } 18 | 19 | #[test] 20 | fn test_query_parse() { 21 | #[derive(Debug, serde::Deserialize)] 22 | struct Query { 23 | param: String, 24 | count: Option, 25 | } 26 | 27 | let mut runner = test::runner(query::required::()); 28 | 29 | assert_matches!( 30 | runner.apply("/?count=20¶m=rustlang"), 31 | Ok(ref query) if query.param == "rustlang" && query.count == Some(20) 32 | ); 33 | 34 | assert_matches!(runner.apply("/"), Err(..)); 35 | } 36 | 37 | #[test] 38 | fn test_query_optional() { 39 | #[derive(Debug, serde::Deserialize)] 40 | struct Query { 41 | param: String, 42 | count: Option, 43 | } 44 | 45 | let mut runner = test::runner(query::optional::()); 46 | 47 | assert_matches!( 48 | runner.apply("/?count=20¶m=rustlang"), 49 | Ok(Some(ref query)) if query.param == "rustlang" && query.count == Some(20) 50 | ); 51 | 52 | assert_matches!(runner.apply("/"), Ok(None)); 53 | } 54 | -------------------------------------------------------------------------------- /tests/endpoints/upgrade.rs: -------------------------------------------------------------------------------- 1 | use finchers::endpoints::upgrade::Builder; 2 | use finchers::prelude::*; 3 | use finchers::test; 4 | 5 | #[test] 6 | fn test_upgrade() { 7 | let mut runner = test::runner({ 8 | endpoints::upgrade::builder().map(|builder: Builder| { 9 | builder 10 | .header("sec-websocket-accept", "xxxx") 11 | .finish(|upgraded| { 12 | drop(upgraded); 13 | Ok(()) 14 | }) 15 | }) 16 | }); 17 | 18 | let response = runner.perform("/").unwrap(); 19 | 20 | assert_eq!(response.status().as_u16(), 101); 21 | assert_matches!( 22 | response.headers().get("sec-websocket-accept"), 23 | Some(h) if h == "xxxx" 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | mod endpoint; 2 | mod endpoints; 3 | 4 | #[test] 5 | fn version_sync() { 6 | version_sync::assert_html_root_url_updated!("src/lib.rs"); 7 | version_sync::assert_markdown_deps_updated!("README.md"); 8 | } 9 | 10 | // #[test] 11 | // fn test_path_macro() { 12 | // let _ = path!(@get /); 13 | // let _ = path!(@get / "foo" / u32); 14 | // let _ = path!(@get / "foo" / { syntax::remains::() }); 15 | // } 16 | 17 | // #[test] 18 | // fn test_routes_macro() { 19 | // let _ = routes![endpoint::unit(), endpoint::value(42),]; 20 | // } 21 | --------------------------------------------------------------------------------