├── .github ├── CODEOWNERS ├── workflows │ ├── audit.yml │ ├── clippy_check.yml │ └── rust.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── dependabot.yml ├── .gitignore ├── saphir ├── examples │ ├── files_to_serve │ │ ├── css │ │ │ └── styles.css │ │ ├── js │ │ │ └── scripts.js │ │ └── index.html │ ├── macro.rs │ └── basic.rs ├── src │ ├── cookie.rs │ ├── handler.rs │ ├── file │ │ ├── etag.rs │ │ ├── content_range.rs │ │ ├── range_requests.rs │ │ ├── cache.rs │ │ └── range.rs │ ├── extension.rs │ ├── guard.rs │ ├── lib.rs │ ├── responder.rs │ ├── controller.rs │ ├── middleware.rs │ ├── macros.rs │ └── error.rs └── Cargo.toml ├── .vscode └── settings.json ├── rustfmt.toml ├── Cargo.toml ├── saphir_macro ├── src │ ├── utils.rs │ ├── middleware │ │ ├── mod.rs │ │ └── fun.rs │ ├── guard │ │ ├── mod.rs │ │ └── fun.rs │ ├── openapi │ │ └── mod.rs │ ├── lib.rs │ └── controller │ │ └── controller_attr.rs └── Cargo.toml ├── saphir_cli ├── Cargo.toml └── src │ ├── main.rs │ └── openapi │ ├── mod.rs │ └── generate │ ├── crate_syn_browser │ ├── file.rs │ ├── browser.rs │ ├── mod.rs │ ├── target.rs │ ├── package.rs │ └── item.rs │ ├── route_info.rs │ ├── utils.rs │ ├── controller_info.rs │ ├── type_info.rs │ └── handler_info.rs ├── LICENSE ├── README.md ├── logo.svg └── deny.toml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @richerarc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/*.rs.bk 3 | .idea/ 4 | *.lock 5 | .DS_Store -------------------------------------------------------------------------------- /saphir/examples/files_to_serve/css/styles.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: cadetblue; 3 | } -------------------------------------------------------------------------------- /saphir/examples/files_to_serve/js/scripts.js: -------------------------------------------------------------------------------- 1 | function log() { 2 | console.log("This is an example"); 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.linkedProjects": [ 3 | "./saphir/Cargo.toml" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | condense_wildcard_suffixes = true 2 | reorder_impl_items = true 3 | reorder_imports = true 4 | imports_granularity = "Crate" 5 | max_width = 160 6 | wrap_comments = true 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "saphir", 5 | "saphir_macro", 6 | "saphir_cli", 7 | ] 8 | 9 | [profile.cli-release] 10 | inherits = "release" 11 | lto = "fat" 12 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | push: 4 | paths: 5 | - '**/Cargo.toml' 6 | - '**/Cargo.lock' 7 | jobs: 8 | security_audit: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions-rs/audit-check@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /saphir/examples/files_to_serve/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | This is HTML 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

This is an example

16 | 17 | 18 | -------------------------------------------------------------------------------- /saphir_macro/src/utils.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Ident; 2 | use syn::{Error, ItemImpl, Result, Type}; 3 | 4 | pub fn parse_item_impl_ident(input: &ItemImpl) -> Result<&Ident> { 5 | if let Type::Path(p) = input.self_ty.as_ref() { 6 | if let Some(f) = p.path.segments.first() { 7 | return Ok(&f.ident); 8 | } 9 | } 10 | 11 | Err(Error::new_spanned(input, "Unable to parse impl ident. this is fatal")) 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: richerarc 7 | 8 | --- 9 | 10 | **Description** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Additional info (please complete the following information):** 17 | - OS & OS Version: [e.g. macOS 10.15.2] 18 | - Saphir Version [e.g. 2.0.0-alpha2] 19 | - Rust toolchain & version [e.g. nightly 1.42] 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: richerarc 7 | 8 | --- 9 | 10 | **Description of the feature** 11 | A clear and concise description of the feature 12 | 13 | **Is your feature request related to a problem?** 14 | Describe what the problem is, and how this feature might fix it 15 | 16 | **Code example of the said feature (If applicable)** 17 | The feature would look like this: 18 | ```rust 19 | my_custom_type.this_feature_method()?; 20 | ``` 21 | -------------------------------------------------------------------------------- /.github/workflows/clippy_check.yml: -------------------------------------------------------------------------------- 1 | name: Clippy check 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | clippy_check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: nightly 17 | components: clippy 18 | override: true 19 | - uses: actions-rs/clippy-check@v1 20 | with: 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | args: --all-targets --all-features 23 | -------------------------------------------------------------------------------- /saphir/src/cookie.rs: -------------------------------------------------------------------------------- 1 | use crate::{http_context::HttpContext, responder::Responder, response::Builder}; 2 | pub use cookie::*; 3 | 4 | impl Responder for Cookie<'static> { 5 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder { 6 | builder.cookie(self) 7 | } 8 | } 9 | 10 | impl Responder for CookieBuilder<'static> { 11 | fn respond_with_builder(self, builder: Builder, ctx: &HttpContext) -> Builder { 12 | self.build().respond_with_builder(builder, ctx) 13 | } 14 | } 15 | 16 | impl Responder for CookieJar { 17 | fn respond_with_builder(self, mut builder: Builder, _ctx: &HttpContext) -> Builder { 18 | *builder.cookies_mut() = self; 19 | builder 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | timezone: America/Montreal 9 | pull-request-branch-name: 10 | separator: "-" 11 | open-pull-requests-limit: 10 12 | reviewers: 13 | - richerarc 14 | assignees: 15 | - richerarc 16 | ignore: 17 | - dependency-name: hyper 18 | versions: 19 | - 0.14.2 20 | - 0.14.4 21 | - 0.14.5 22 | - 0.14.6 23 | - dependency-name: tokio 24 | versions: 25 | - 1.1.0 26 | - 1.1.1 27 | - 1.2.0 28 | - 1.3.0 29 | - 1.4.0 30 | - dependency-name: cargo_metadata 31 | versions: 32 | - 0.13.0 33 | - dependency-name: nom 34 | versions: 35 | - 6.0.0 36 | -------------------------------------------------------------------------------- /saphir_macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "saphir_macro" 3 | version = "2.2.0" 4 | authors = ["Richer Archambault "] 5 | edition = "2021" 6 | description = "Macro generation for http server framework" 7 | documentation = "https://docs.rs/saphir" 8 | homepage = "https://github.com/richerarc/saphir" 9 | repository = "https://github.com/richerarc/saphir" 10 | readme = "../README.md" 11 | keywords = ["hyper", "http", "server", "web", "async"] 12 | license = "MIT" 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [lib] 16 | name = "saphir_macro" 17 | proc-macro = true 18 | 19 | [features] 20 | default = [] 21 | full = ["validate-requests"] 22 | validate-requests = [] 23 | tracing-instrument = [] 24 | 25 | [dependencies] 26 | proc-macro2 = "1.0" 27 | quote = "1.0" 28 | syn = { version = "1.0", features = ["full", "extra-traits"] } 29 | http = "0.2" 30 | -------------------------------------------------------------------------------- /saphir_cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "saphir-cli" 3 | version = "0.5.2" 4 | authors = ["Samuel Bergeron-Drouin "] 5 | edition = "2021" 6 | description = "CLI utility for the Saphir web framework" 7 | documentation = "https://docs.rs/saphir" 8 | homepage = "https://github.com/richerarc/saphir" 9 | repository = "https://github.com/richerarc/saphir" 10 | readme = "../README.md" 11 | keywords = ["hyper", "http", "server", "web", "async"] 12 | license = "MIT" 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [[bin]] 16 | name = "saphir" 17 | path = "src/main.rs" 18 | 19 | [dependencies] 20 | syn = { version = "1.0", features = ["full", "extra-traits"] } 21 | clap = { version = "4.0", features = ["derive"] } 22 | serde = "1.0" 23 | serde_derive = "1.0" 24 | serde_yaml = "0.9" 25 | toml = "0.8" 26 | convert_case = "0.6" 27 | cargo_metadata = "0.18" 28 | lazycell = "1.2" 29 | http = "0.2" 30 | once_cell = "1.4" 31 | regex = "1.5.5" 32 | -------------------------------------------------------------------------------- /saphir_cli/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::match_like_matches_macro)] 2 | use clap::{Parser, Subcommand}; 3 | 4 | mod openapi; 5 | 6 | use crate::openapi::Openapi; 7 | 8 | type CommandResult = std::result::Result<(), String>; 9 | 10 | pub(crate) trait Command: Sized { 11 | type Args; 12 | fn new(args: Self::Args) -> Self; 13 | fn run(self) -> CommandResult; 14 | } 15 | 16 | /// Saphir web framework's CLI utility. 17 | #[derive(Parser, Debug)] 18 | #[command(name = "saphir")] 19 | #[command(bin_name = "saphir")] 20 | // #[command(about = "Saphir web framework's CLI utility.", long_about = None)] 21 | struct SaphirCli { 22 | #[command(subcommand)] 23 | cmd: SaphirCliCommand, 24 | } 25 | 26 | #[derive(Subcommand, Debug)] 27 | enum SaphirCliCommand { 28 | Openapi(::Args), 29 | } 30 | 31 | fn main() { 32 | let cli = SaphirCli::parse(); 33 | if let Err(e) = match cli.cmd { 34 | SaphirCliCommand::Openapi(args) => { 35 | let openapi = Openapi::new(args); 36 | openapi.run() 37 | } 38 | } { 39 | eprintln!("{}", e); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{openapi::generate::Gen, Command, CommandResult}; 2 | use clap::{Args, Subcommand}; 3 | 4 | mod generate; 5 | mod schema; 6 | 7 | /// OpenAPI v3 generation 8 | /// 9 | /// See: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md 10 | #[derive(Args, Debug)] 11 | #[command(args_conflicts_with_subcommands = true)] 12 | pub(crate) struct OpenapiArgs { 13 | #[command(subcommand)] 14 | cmd: OpenapiCommand, 15 | } 16 | 17 | #[derive(Subcommand, Debug)] 18 | pub(crate) enum OpenapiCommand { 19 | Gen(::Args), 20 | } 21 | 22 | pub(crate) struct Openapi { 23 | pub args: ::Args, 24 | } 25 | 26 | impl Command for Openapi { 27 | type Args = OpenapiArgs; 28 | 29 | fn new(args: Self::Args) -> Self { 30 | Self { args } 31 | } 32 | 33 | fn run<'b>(self) -> CommandResult { 34 | match self.args.cmd { 35 | OpenapiCommand::Gen(args) => { 36 | let gen = Gen::new(args); 37 | gen.run()?; 38 | } 39 | } 40 | Ok(()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Richer Archambault 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 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/generate/crate_syn_browser/file.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use super::{Error, Target}; 3 | use std::{ 4 | fmt::Debug, 5 | fs::File as FsFile, 6 | io::Read, 7 | path::{Path, PathBuf}, 8 | }; 9 | use syn::File as SynFile; 10 | use Error::*; 11 | 12 | #[derive(Debug)] 13 | pub struct File<'b> { 14 | pub target: &'b Target<'b>, 15 | pub file: SynFile, 16 | pub path: String, 17 | pub(crate) dir: PathBuf, 18 | } 19 | 20 | impl<'b> File<'b> { 21 | pub fn new(target: &'b Target<'b>, dir: &Path, path: String) -> Result, Error> { 22 | let mut f = FsFile::open(dir).map_err(|e| FileIo(Box::new(dir.to_path_buf()), Box::new(e)))?; 23 | let mut buffer = String::new(); 24 | f.read_to_string(&mut buffer).map_err(|e| FileIo(Box::new(dir.to_path_buf()), Box::new(e)))?; 25 | 26 | let file = syn::parse_file(buffer.as_str()).map_err(|e| FileParse(Box::new(dir.to_path_buf()), Box::new(e)))?; 27 | 28 | let file = Self { 29 | target, 30 | file, 31 | path, 32 | dir: dir.parent().expect("Valid file path should have valid parent folder").to_path_buf(), 33 | }; 34 | 35 | Ok(file) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/generate/crate_syn_browser/browser.rs: -------------------------------------------------------------------------------- 1 | use super::{Error, Package}; 2 | use lazycell::LazyCell; 3 | use std::{fmt::Debug, path::PathBuf}; 4 | 5 | #[derive(Debug)] 6 | pub struct Browser<'b> { 7 | pub(crate) crate_metadata: cargo_metadata::Metadata, 8 | packages: LazyCell>>, 9 | } 10 | 11 | impl<'b> Browser<'b> { 12 | pub fn new(crate_path: PathBuf) -> Result { 13 | let crate_metadata = cargo_metadata::MetadataCommand::new().manifest_path(crate_path.join("Cargo.toml")).exec()?; 14 | 15 | let browser = Self { 16 | crate_metadata, 17 | packages: LazyCell::new(), 18 | }; 19 | 20 | Ok(browser) 21 | } 22 | 23 | pub fn package_by_name(&self, name: &str) -> Option<&'b Package> { 24 | self.packages().iter().find(|p| p.meta.name.as_str() == name) 25 | } 26 | 27 | fn init_packages(&'b self) { 28 | if !self.packages.filled() { 29 | let members: Vec = self 30 | .crate_metadata 31 | .workspace_members 32 | .iter() 33 | .map(|id| Package::new(self, id).expect("Should exist since we provided a proper PackageId")) 34 | .collect(); 35 | self.packages.fill(members).expect("We should never be filling this twice"); 36 | } 37 | } 38 | 39 | pub fn packages(&'b self) -> &'b Vec> { 40 | self.init_packages(); 41 | self.packages.borrow().expect("Should have been initialized by the previous statement") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![logo] 2 | [![doc](https://docs.rs/saphir/badge.svg)](https://docs.rs/saphir/) 3 | [![crate](https://img.shields.io/crates/v/saphir.svg)](https://crates.io/crates/saphir) 4 | [![issue](https://img.shields.io/github/issues/richerarc/saphir.svg)](https://github.com/richerarc/saphir/issues) 5 | ![Rust](https://github.com/richerarc/saphir/workflows/Rust/badge.svg?branch=master) 6 | ![downloads](https://img.shields.io/crates/d/saphir.svg) 7 | [![license](https://img.shields.io/crates/l/saphir.svg)](https://github.com/richerarc/saphir/blob/master/LICENSE) 8 | [![dependency status](https://deps.rs/repo/github/richerarc/saphir/status.svg)](https://deps.rs/repo/github/richerarc/saphir) 9 | 10 | ### Saphir is a fully async-await http server framework for rust 11 | The goal is to give low-level control to your web stack (as hyper does) without the time consuming task of doing everything from scratch. 12 | 13 | ## Quick Overview 14 | ```rust 15 | use saphir::prelude::*; 16 | struct TestController {} 17 | #[controller] 18 | impl TestController { 19 | #[get("/{var}/print")] 20 | async fn print_test(&self, var: String) -> (u16, String) { 21 | (200, var) 22 | } 23 | } 24 | async fn test_handler(mut req: Request) -> (u16, Option) { 25 | (200, req.captures_mut().remove("variable")) 26 | } 27 | #[tokio::main] 28 | async fn main() -> Result<(), SaphirError> { 29 | env_logger::init(); 30 | let server = Server::builder() 31 | .configure_listener(|l| { 32 | l.interface("127.0.0.1:3000") 33 | }) 34 | .configure_router(|r| { 35 | r.route("/{variable}/print", Method::GET, test_handler) 36 | .controller(TestController {}) 37 | }) 38 | .build(); 39 | 40 | server.run().await 41 | } 42 | ``` 43 | 44 | [logo]: ./logo.svg "Saphir Logo" -------------------------------------------------------------------------------- /saphir_macro/src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span, TokenStream}; 2 | use syn::{Error, ImplItem, ItemImpl, Result}; 3 | 4 | use quote::quote; 5 | 6 | mod fun; 7 | 8 | pub fn expand_middleware(mut mid_impl: ItemImpl) -> Result { 9 | let mid_fn = remove_middleware_fn(&mut mid_impl)?; 10 | 11 | let fn_def = fun::MidFnDef::new(mid_fn)?; 12 | mid_impl.items.push(fn_def.def); 13 | 14 | let middleware_ident = crate::utils::parse_item_impl_ident(&mid_impl)?; 15 | let middleware_name = middleware_ident.to_string(); 16 | 17 | let mod_ident = Ident::new(&format!("SAPHIR_GEN_MIDDLEWARE_{}", &middleware_name), Span::call_site()); 18 | let fn_ident = fn_def.fn_ident; 19 | 20 | Ok(quote! { 21 | #mid_impl 22 | 23 | mod #mod_ident { 24 | use super::*; 25 | use saphir::prelude::*; 26 | 27 | impl Middleware for #middleware_ident { 28 | fn next(&'static self, ctx: HttpContext, chain: &'static dyn MiddlewareChain) -> BoxFuture<'static, Result> { 29 | self.#fn_ident(ctx, chain).boxed() 30 | } 31 | } 32 | } 33 | }) 34 | } 35 | 36 | fn remove_middleware_fn(input: &mut ItemImpl) -> Result { 37 | let mid_fn_pos = input.items.iter().position(|item| { 38 | if let ImplItem::Method(m) = item { 39 | return m.sig.ident == "next"; 40 | } 41 | 42 | false 43 | }).ok_or_else(|| Error::new_spanned(&input, "No method `next` found in the impl section of the middleware.\nMake sure the impl block contains a fn with the following signature:\n `async fn next(&self, _: HttpContext, _: &dyn MiddlewareChain) -> Result`"))?; 44 | 45 | let mid_fn = input.items.remove(mid_fn_pos); 46 | 47 | Ok(mid_fn) 48 | } 49 | -------------------------------------------------------------------------------- /saphir/src/handler.rs: -------------------------------------------------------------------------------- 1 | use futures::{Future, FutureExt}; 2 | 3 | use crate::{ 4 | request::Request, 5 | responder::{DynResponder, Responder}, 6 | }; 7 | use std::pin::Pin; 8 | 9 | /// Define a Handler of a potential http request 10 | /// 11 | /// Implementing this trait on any type will allow the router to route request 12 | /// towards it. Implemented by default on Controllers and on any `async 13 | /// fn(Request) -> impl Responder` 14 | pub trait Handler { 15 | /// Responder returned by the handler 16 | type Responder: Responder; 17 | /// Specific future returning the responder 18 | type Future: Future; 19 | 20 | /// Handle the http request, returning a future of a responder 21 | fn handle(&self, req: Request) -> Self::Future; 22 | } 23 | 24 | impl Handler for Fun 25 | where 26 | Fun: Fn(Request) -> Fut, 27 | Fut: 'static + Future + Send, 28 | R: Responder, 29 | { 30 | type Future = Box + Unpin + Send>; 31 | type Responder = R; 32 | 33 | #[inline] 34 | fn handle(&self, req: Request) -> Self::Future { 35 | Box::new(Box::pin((*self)(req))) 36 | } 37 | } 38 | 39 | #[doc(hidden)] 40 | pub trait DynHandler { 41 | fn dyn_handle(&self, req: Request) -> Pin> + Unpin + Send>>; 42 | } 43 | 44 | impl DynHandler for H 45 | where 46 | R: 'static + Responder + Send, 47 | Fut: 'static + Future + Unpin + Send, 48 | H: Handler, 49 | { 50 | #[inline] 51 | fn dyn_handle(&self, req: Request) -> Pin> + Unpin + Send>> { 52 | Box::pin(self.handle(req).map(|r| Box::new(Some(r)) as Box)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /saphir_macro/src/guard/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span, TokenStream}; 2 | use syn::{Error, ImplItem, ItemImpl, Result}; 3 | 4 | use quote::quote; 5 | 6 | mod fun; 7 | 8 | pub fn expand_guard(mut guard_impl: ItemImpl) -> Result { 9 | let guard_fn = remove_validate_fn(&mut guard_impl)?; 10 | 11 | let fn_def = fun::GuardFnDef::new(guard_fn)?; 12 | guard_impl.items.push(fn_def.def); 13 | 14 | let guard_ident = crate::utils::parse_item_impl_ident(&guard_impl)?; 15 | let guard_name = guard_ident.to_string(); 16 | 17 | let mod_ident = Ident::new(&format!("SAPHIR_GEN_GUARD_{}", &guard_name), Span::call_site()); 18 | let fn_ident = fn_def.fn_ident; 19 | let resp_type = fn_def.responder; 20 | 21 | Ok(quote! { 22 | #guard_impl 23 | 24 | mod #mod_ident { 25 | use super::*; 26 | use saphir::prelude::*; 27 | 28 | impl Guard for #guard_ident { 29 | type Future = BoxFuture<'static, Result>; 30 | type Responder = #resp_type; 31 | 32 | fn validate(&'static self, req: Request>) -> Self::Future { 33 | self.#fn_ident(req).boxed() 34 | } 35 | } 36 | } 37 | }) 38 | } 39 | 40 | fn remove_validate_fn(input: &mut ItemImpl) -> Result { 41 | let mid_fn_pos = input.items.iter().position(|item| { 42 | if let ImplItem::Method(m) = item { 43 | return m.sig.ident.to_string().eq("validate"); 44 | } 45 | 46 | false 47 | }).ok_or_else(|| Error::new_spanned(&input, "No method `validate` found in the impl section of the middleware.\nMake sure the impl block contains a fn with the following signature:\n `async fn validate(&self, req: Request) -> Result`"))?; 48 | 49 | let mid_fn = input.items.remove(mid_fn_pos); 50 | 51 | Ok(mid_fn) 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - '*' 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | name: Rust 10 | 11 | jobs: 12 | check: 13 | name: Check 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout sources 17 | uses: actions/checkout@v2 18 | 19 | - name: Install stable toolchain 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | override: true 25 | 26 | - name: Run cargo check 27 | uses: actions-rs/cargo@v1 28 | with: 29 | command: check 30 | args: --all-features 31 | 32 | test: 33 | name: Test Suite 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout sources 37 | uses: actions/checkout@v2 38 | 39 | - name: Install stable toolchain 40 | uses: actions-rs/toolchain@v1 41 | with: 42 | profile: minimal 43 | toolchain: stable 44 | override: true 45 | 46 | - name: Run cargo test 47 | uses: actions-rs/cargo@v1 48 | with: 49 | command: test 50 | args: --all-features 51 | 52 | lints: 53 | name: Lints 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: Checkout sources 57 | uses: actions/checkout@v2 58 | 59 | - name: Install stable toolchain 60 | uses: actions-rs/toolchain@v1 61 | with: 62 | profile: minimal 63 | toolchain: stable 64 | override: true 65 | components: rustfmt, clippy 66 | 67 | - name: Run cargo fmt 68 | uses: actions-rs/cargo@v1 69 | with: 70 | command: fmt 71 | args: --all -- --check 72 | 73 | - name: Run cargo clippy 74 | uses: actions-rs/cargo@v1 75 | with: 76 | command: clippy 77 | args: --all-targets --all-features -- -D warnings 78 | -------------------------------------------------------------------------------- /saphir_macro/src/openapi/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::ToTokens; 3 | use syn::{AttributeArgs, Error, Item, Lit, Meta, NestedMeta, Result}; 4 | 5 | const MISSING_ATTRIBUTE: &str = "openapi macro require at least one of the following attributes : 6 | - mime 7 | - name"; 8 | 9 | pub fn validate_openapi(args: AttributeArgs, input: Item) -> Result { 10 | match &input { 11 | Item::Struct(_) | Item::Enum(_) => { 12 | if args.is_empty() { 13 | panic!("{}", MISSING_ATTRIBUTE); 14 | } 15 | } 16 | _ => panic!("openapi attribute can only be placed on Struct and Enum"), 17 | } 18 | let mut mime: Option = None; 19 | let mut name: Option = None; 20 | for arg in args.into_iter() { 21 | if let NestedMeta::Meta(Meta::NameValue(nv)) = arg { 22 | match nv.path.get_ident().map(|i| i.to_string()).as_deref() { 23 | Some("mime") => { 24 | if mime.is_some() { 25 | return Err(Error::new_spanned(nv, "Cannot specify `mime` twice")); 26 | } 27 | mime = match nv.lit { 28 | Lit::Str(s) => Some(s.value()), 29 | _ => None, 30 | } 31 | } 32 | Some("name") => { 33 | if name.is_some() { 34 | return Err(Error::new_spanned(nv, "Cannot specify `name` twice")); 35 | } 36 | name = match nv.lit { 37 | Lit::Str(s) => Some(s.value()), 38 | _ => None, 39 | } 40 | } 41 | _ => return Err(Error::new_spanned(nv, "Unrecognized parameter")), 42 | } 43 | } 44 | } 45 | 46 | if mime.is_none() && name.is_none() { 47 | panic!("{}", MISSING_ATTRIBUTE); 48 | } 49 | 50 | Ok(input.to_token_stream()) 51 | } 52 | -------------------------------------------------------------------------------- /saphir/src/file/etag.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | pub enum EntityTag { 4 | Strong(String), 5 | Weak(String), 6 | } 7 | 8 | impl EntityTag { 9 | pub fn new(is_weak: bool, tag: &str) -> Self { 10 | if is_weak { 11 | EntityTag::Weak(tag.to_string()) 12 | } else { 13 | EntityTag::Strong(tag.to_string()) 14 | } 15 | } 16 | 17 | pub fn parse(tag: &str) -> Self { 18 | let mut is_weak = false; 19 | let parsed_tag = { 20 | if tag.starts_with("W/") { 21 | is_weak = true; 22 | tag.trim_start_matches("W/\"").trim_end_matches('\"') 23 | } else { 24 | tag.trim_start_matches('\"').trim_end_matches('\"') 25 | } 26 | }; 27 | 28 | if is_weak { 29 | EntityTag::Weak(parsed_tag.to_string()) 30 | } else { 31 | EntityTag::Strong(parsed_tag.to_string()) 32 | } 33 | } 34 | 35 | pub fn get_tag(&self) -> String { 36 | match self { 37 | EntityTag::Strong(tag) => format!("\"{}\"", tag), 38 | EntityTag::Weak(tag) => format!("W/\"{}\"", tag), 39 | } 40 | } 41 | 42 | fn is_weak(&self) -> bool { 43 | match self { 44 | EntityTag::Weak(_) => true, 45 | _ => false, 46 | } 47 | } 48 | 49 | pub fn weak_eq(&self, other: EntityTag) -> bool { 50 | self.as_ref() == other.as_ref() 51 | } 52 | 53 | pub fn strong_eq(&self, other: EntityTag) -> bool { 54 | !self.is_weak() && !other.is_weak() && self.as_ref() == other.as_ref() 55 | } 56 | } 57 | 58 | impl AsRef for EntityTag { 59 | fn as_ref(&self) -> &str { 60 | match self { 61 | EntityTag::Strong(str) => str.as_str(), 62 | EntityTag::Weak(str) => str.as_str(), 63 | } 64 | } 65 | } 66 | 67 | pub trait SystemTimeExt { 68 | fn timestamp(&self) -> u64; 69 | } 70 | 71 | impl SystemTimeExt for SystemTime { 72 | /// Convert `SystemTime` to timestamp in seconds. 73 | fn timestamp(&self) -> u64 { 74 | self.duration_since(::std::time::UNIX_EPOCH).unwrap_or_default().as_secs() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /saphir/src/extension.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | http_context::HttpContext, 3 | request::{FromRequest, Request}, 4 | responder::Responder, 5 | response::Builder, 6 | }; 7 | use std::{ 8 | borrow::{Borrow, BorrowMut}, 9 | ops::{Deref, DerefMut}, 10 | }; 11 | 12 | use crate::{body::Body, prelude::Bytes}; 13 | pub use http::Extensions; 14 | 15 | #[derive(Debug)] 16 | pub enum ExtError { 17 | /// The extension type was not found, the type name of the missing extension 18 | /// is returned 19 | MissingExtension(&'static str), 20 | } 21 | 22 | impl Responder for ExtError { 23 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder { 24 | debug!("Missing extension of type: {}", std::any::type_name::()); 25 | builder.status(500) 26 | } 27 | } 28 | 29 | pub struct Ext(pub T); 30 | 31 | impl Ext { 32 | pub fn into_inner(self) -> T { 33 | self.0 34 | } 35 | } 36 | 37 | impl Deref for Ext { 38 | type Target = T; 39 | 40 | fn deref(&self) -> &Self::Target { 41 | &self.0 42 | } 43 | } 44 | 45 | impl DerefMut for Ext { 46 | fn deref_mut(&mut self) -> &mut Self::Target { 47 | &mut self.0 48 | } 49 | } 50 | 51 | impl AsRef for Ext { 52 | fn as_ref(&self) -> &T { 53 | &self.0 54 | } 55 | } 56 | 57 | impl AsMut for Ext { 58 | fn as_mut(&mut self) -> &mut T { 59 | &mut self.0 60 | } 61 | } 62 | 63 | impl Borrow for Ext { 64 | fn borrow(&self) -> &T { 65 | &self.0 66 | } 67 | } 68 | 69 | impl BorrowMut for Ext { 70 | fn borrow_mut(&mut self) -> &mut T { 71 | &mut self.0 72 | } 73 | } 74 | 75 | impl Responder for Ext { 76 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder { 77 | builder.extension(self.into_inner()) 78 | } 79 | } 80 | 81 | impl FromRequest for Ext 82 | where 83 | T: Send + Sync + 'static, 84 | { 85 | type Err = ExtError; 86 | type Fut = futures::future::Ready>; 87 | 88 | fn from_request(req: &mut Request) -> Self::Fut { 89 | futures::future::ready( 90 | req.extensions_mut() 91 | .remove::() 92 | .ok_or_else(|| ExtError::MissingExtension(std::any::type_name::())) 93 | .map(Ext), 94 | ) 95 | } 96 | } 97 | 98 | impl FromRequest for Extensions { 99 | type Err = (); 100 | type Fut = futures::future::Ready>; 101 | 102 | fn from_request(req: &mut Request>) -> Self::Fut { 103 | futures::future::ready(Ok(std::mem::take(req.extensions_mut()))) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/generate/crate_syn_browser/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{Debug, Display, Formatter}, 3 | path::PathBuf, 4 | }; 5 | 6 | mod browser; 7 | mod file; 8 | mod item; 9 | mod module; 10 | mod package; 11 | mod target; 12 | 13 | pub use self::{ 14 | browser::Browser, 15 | file::File, 16 | item::{Impl, ImplItemKind, Item, ItemKind, Method}, 17 | module::Module, 18 | package::Package, 19 | target::Target, 20 | }; 21 | 22 | #[derive(Debug)] 23 | pub enum Error { 24 | CargoToml(Box), 25 | FileIo(Box, Box), 26 | FileParse(Box, Box), 27 | } 28 | 29 | impl From for Error { 30 | fn from(e: cargo_metadata::Error) -> Self { 31 | Error::CargoToml(Box::new(e)) 32 | } 33 | } 34 | 35 | impl From for String { 36 | fn from(e: Error) -> String { 37 | format!("{}", e) 38 | } 39 | } 40 | 41 | impl Display for Error { 42 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 43 | match self { 44 | Error::CargoToml(_) => write!(f, "Unable to properly read the crate's metadata from the Cargo.toml manifest."), 45 | Error::FileIo(s, e) => write!(f, "unable to read `{}` : {}", s.to_str().unwrap_or_default(), e), 46 | Error::FileParse(s, e) => write!(f, "unable to parse `{}` : {}", s.to_str().unwrap_or_default(), e), 47 | } 48 | } 49 | } 50 | 51 | impl std::error::Error for Error { 52 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 53 | match self { 54 | Error::CargoToml(e) => Some(e), 55 | Error::FileIo(_, e) => Some(e), 56 | Error::FileParse(_, e) => Some(e), 57 | } 58 | } 59 | } 60 | 61 | pub trait UseScope<'b> { 62 | fn path(&'b self) -> &'b str; 63 | fn target(&'b self) -> &'b Target<'b>; 64 | fn uses(&'b self) -> &'b Vec; 65 | fn pub_uses(&'b self) -> Vec<&'b ExpandedUse>; 66 | fn find_type_definition(&'b self, name: &str) -> Result>, Error>; 67 | fn find_type_definition_inline(&'b self, name: &str) -> Result>, Error>; 68 | fn expand_path(&'b self, path: &str) -> String { 69 | let mut splitted: Vec<&str> = path.split("::").collect(); 70 | let first = match splitted.first() { 71 | Some(f) => *f, 72 | _ => return "".to_string(), 73 | }; 74 | if splitted.len() == 1 { 75 | return first.to_string(); 76 | } 77 | let first = match first { 78 | "self" => self.path().to_string(), 79 | "crate" => self.target().package.name.clone(), 80 | "super" => { 81 | let split: Vec<&str> = self.path().split("::").collect(); 82 | split[..(split.len() - 1)].join("::") 83 | } 84 | _ => first.to_string(), 85 | }; 86 | 87 | splitted[0] = first.as_str(); 88 | splitted.join("::") 89 | } 90 | } 91 | 92 | #[derive(Debug, Clone)] 93 | pub struct ExpandedUse { 94 | pub path: String, 95 | pub name: String, 96 | pub alias: String, 97 | pub is_pub: bool, 98 | } 99 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/generate/crate_syn_browser/target.rs: -------------------------------------------------------------------------------- 1 | use super::{Error, Module, Package, UseScope}; 2 | use cargo_metadata::Target as MetaTarget; 3 | use lazycell::LazyCell; 4 | use std::{cell::RefCell, collections::HashMap, fmt::Debug}; 5 | 6 | #[derive(Debug)] 7 | pub struct Target<'b> { 8 | pub package: &'b Package<'b>, 9 | pub(crate) target: &'b MetaTarget, 10 | entrypoint: LazyCell>, 11 | pub(crate) modules: RefCell>>, 12 | } 13 | 14 | impl<'b> Target<'b> { 15 | pub fn new(package: &'b Package<'b>, target: &'b MetaTarget) -> Self { 16 | Self { 17 | package, 18 | target, 19 | entrypoint: LazyCell::new(), 20 | modules: RefCell::default(), 21 | } 22 | } 23 | 24 | pub fn entrypoint(&'b self) -> Result<&'b Module<'b>, Error> { 25 | if !self.entrypoint.filled() { 26 | let module = Module::new_crate(self); 27 | self.entrypoint.fill(module).expect("We should never be filling this twice"); 28 | let module = self.entrypoint.borrow().unwrap(); 29 | module.init_crate(self)?; 30 | self.modules.borrow_mut().insert(self.target.name.clone(), module); 31 | } 32 | Ok(self.entrypoint.borrow().expect("Should have been initialized by the previous statement")) 33 | } 34 | 35 | pub fn module_by_use_path(&'b self, path: &str) -> Result>, Error> { 36 | if let Some(module) = self.modules.borrow().get(path) { 37 | return Ok(Some(module)); 38 | } 39 | let mut path_split = path.split("::"); 40 | if let Some(mut root) = path_split.next() { 41 | if root == "crate" { 42 | root = self.package.name.as_str(); 43 | } 44 | let module = if root == self.package.name.as_str() { 45 | let mut cur_module = self.entrypoint()?; 46 | for split in path_split { 47 | if let Some(m) = cur_module.modules()?.iter().find(|m| m.name() == split) { 48 | cur_module = m; 49 | } 50 | } 51 | if cur_module.path() == path { 52 | Some(cur_module) 53 | } else { 54 | None 55 | } 56 | } else { 57 | let dep_package = match self.package.dependancy(root) { 58 | Some(dep) => dep, 59 | None => return Ok(None), 60 | }; 61 | let lib = dep_package.lib_target(); 62 | let lib = match lib { 63 | Some(lib) => lib, 64 | None => return Ok(None), 65 | }; 66 | let mut remaining_path: Vec<&str> = vec![root]; 67 | remaining_path.extend(path_split); 68 | let use_path = remaining_path.join("::"); 69 | lib.module_by_use_path(use_path.as_str())? 70 | }; 71 | if let Some(module) = module { 72 | self.modules.borrow_mut().insert(path.to_string(), module); 73 | } 74 | Ok(module) 75 | } else { 76 | Ok(None) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /saphir/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "saphir" 3 | version = "3.1.1" 4 | edition = "2021" 5 | authors = ["Richer Archambault "] 6 | description = "Fully async-await http server framework" 7 | documentation = "https://docs.rs/saphir" 8 | homepage = "https://github.com/richerarc/saphir" 9 | repository = "https://github.com/richerarc/saphir" 10 | readme = "../README.md" 11 | keywords = ["hyper", "http", "server", "web", "async"] 12 | license = "MIT" 13 | resolver = "2" 14 | 15 | [[example]] 16 | name = "basic" 17 | 18 | [[example]] 19 | name = "macro" 20 | required-features = ["json", "file", "multipart", "form"] 21 | 22 | [package.metadata.docs.rs] 23 | all-features = true 24 | rustdoc-args = ["--cfg", "docsrs"] 25 | 26 | [features] 27 | default = ["macro", "http1"] 28 | full = ["macro", "json", "form", "https", "multipart", "operation", "post-redirect", "file", "http1", "http2"] 29 | post-redirect = ["redirect", "json"] 30 | redirect = ["mime", "form"] 31 | https = ["base64", "rustls", "tokio-rustls", "rustls-pemfile", "rustls-pki-types"] 32 | json = ["serde", "serde_json"] 33 | form = ["serde", "serde_urlencoded"] 34 | macro = ["saphir_macro"] 35 | multipart = ["mime", "multer"] 36 | file = ["mime", "mime_guess", "percent-encoding", "time", "flate2", "brotli", "tokio/fs"] 37 | operation = ["serde", "uuid"] 38 | http1 = ["hyper/http1"] 39 | http2 = ["hyper/http2"] 40 | validate-requests = ["validator", "saphir_macro/validate-requests"] 41 | tracing-instrument = ["tracing", "saphir_macro/tracing-instrument"] 42 | 43 | [dependencies] 44 | async-stream = "0.3" 45 | log = "0.4" 46 | hyper = { version = "0.14", features = ["stream", "server"] } 47 | tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "time", "parking_lot"] } 48 | futures = "0.3" 49 | futures-util = "0.3" 50 | cookie = "0.18" 51 | http = "0.2" 52 | http-body = "0.4" 53 | regex = "1.5.5" 54 | thiserror = "1.0" 55 | 56 | uuid = { version = "1", features = ["serde", "v4"], optional = true } 57 | rustls = { version = "0.23", optional = true } 58 | rustls-pki-types = { version = "1.10.1", optional = true, features = ["alloc"] } 59 | rustls-pemfile = { version = "1.0.4", optional = true } 60 | tracing = { version = "0.1", optional = true, features = ["log"]} 61 | tokio-rustls = { version = "0.26", optional = true } 62 | base64 = { version = "0.22", optional = true } 63 | serde = { version = "1.0", optional = true } 64 | serde_json = { version = "1.0", optional = true } 65 | serde_urlencoded = { version = "0.7", optional = true } 66 | saphir_macro = { path = "../saphir_macro", version = "2.2.0", optional = true } 67 | mime = { version = "0.3", optional = true } 68 | multer = { version = "2.0", optional = true } 69 | mime_guess = { version = "2.0", optional = true } 70 | percent-encoding = { version = "2.1", optional = true } 71 | time = { version = "0.3", optional = true, features = ["std", "serde-human-readable", "macros"] } 72 | flate2 = { version = "1.0", optional = true } 73 | brotli = { version = "7.0", optional = true } 74 | validator = { version = "0.20", optional = true, features = ["derive"] } 75 | 76 | [dev-dependencies] 77 | env_logger = "0.11" 78 | serde = "1.0" 79 | serde_derive = "1.0" 80 | mime = "0.3" 81 | tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "time", "parking_lot", "macros"] } #macros only in dev deps 82 | 83 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/generate/crate_syn_browser/package.rs: -------------------------------------------------------------------------------- 1 | use super::{Browser, Target}; 2 | use cargo_metadata::{Package as MetaPackage, PackageId}; 3 | use lazycell::LazyCell; 4 | use std::{cell::RefCell, collections::HashMap, fmt::Debug}; 5 | 6 | #[derive(Debug)] 7 | pub struct Package<'b> { 8 | pub name: String, 9 | pub browser: &'b Browser<'b>, 10 | pub(crate) meta: &'b MetaPackage, 11 | targets: LazyCell>>, 12 | dependancies: RefCell>>>, 13 | dependancies_to_free: RefCell>>, 14 | } 15 | 16 | impl Drop for Package<'_> { 17 | fn drop(&mut self) { 18 | for free_me in self.dependancies_to_free.borrow_mut().iter() { 19 | unsafe { 20 | let _ = Box::from_raw(*free_me); 21 | } 22 | } 23 | } 24 | } 25 | 26 | impl<'b> Package<'b> { 27 | pub fn new(browser: &'b Browser<'b>, id: &'b PackageId) -> Option { 28 | let package = browser.crate_metadata.packages.iter().find(|p| p.id == *id)?; 29 | let name = package.name.clone(); 30 | 31 | Some(Self { 32 | browser, 33 | name, 34 | meta: package, 35 | targets: LazyCell::new(), 36 | dependancies: RefCell::new(HashMap::new()), 37 | dependancies_to_free: RefCell::new(Vec::new()), 38 | }) 39 | } 40 | 41 | pub fn dependancy(&'b self, name: &str) -> Option<&'b Package<'b>> { 42 | if !self.dependancies.borrow().contains_key(name) { 43 | let package = self 44 | .meta 45 | .dependencies 46 | .iter() 47 | .find(|dep| dep.rename.as_ref().unwrap_or(&dep.name) == name) 48 | .and_then(|dep| { 49 | self.browser 50 | .crate_metadata 51 | .packages 52 | .iter() 53 | .find(|package| package.name == dep.name && dep.req.matches(&package.version)) 54 | .and_then(|p| Package::new(self.browser, &p.id)) 55 | }); 56 | let to_add = if let Some(package) = package { 57 | let raw_mut = Box::into_raw(Box::new(package)); 58 | self.dependancies_to_free.borrow_mut().push(raw_mut); 59 | Some(raw_mut as *const Package) 60 | } else { 61 | None 62 | }; 63 | self.dependancies.borrow_mut().insert(name.to_string(), to_add); 64 | } 65 | self.dependancies 66 | .borrow() 67 | .get(name) 68 | .map(|d| d.as_ref()) 69 | .and_then(|d| d.copied()) 70 | .map(|b| unsafe { &*b }) 71 | } 72 | 73 | fn targets(&'b self) -> &'b Vec> { 74 | if !self.targets.filled() { 75 | let targets = self.meta.targets.iter().map(|t| Target::new(self, t)).collect(); 76 | self.targets.fill(targets).expect("We should never be filling this twice"); 77 | } 78 | self.targets.borrow().expect("Should have been initialized by the previous statement") 79 | } 80 | 81 | pub fn bin_target(&'b self) -> Option<&'b Target<'b>> { 82 | self.targets().iter().find(|t| t.target.kind.contains(&"bin".to_string())) 83 | } 84 | 85 | pub fn lib_target(&'b self) -> Option<&'b Target<'b>> { 86 | self.targets().iter().find(|t| t.target.kind.contains(&"lib".to_string())) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /saphir/src/guard.rs: -------------------------------------------------------------------------------- 1 | //! A guard is called before the request is processed by the router and 2 | //! can modify the request data or stops request processing by returning a 3 | //! response immediately. 4 | 5 | use crate::{ 6 | body::Body, 7 | request::Request, 8 | responder::{DynResponder, Responder}, 9 | }; 10 | use futures::{future::BoxFuture, FutureExt}; 11 | use futures_util::future::Future; 12 | 13 | /// Auto trait implementation over every function that match the definition of a 14 | /// guard. 15 | pub trait Guard { 16 | type Future: Future, Self::Responder>> + Send; 17 | type Responder: Responder + Send; 18 | 19 | fn validate(&'static self, req: Request) -> Self::Future; 20 | } 21 | 22 | impl Guard for Fun 23 | where 24 | Resp: Responder + Send, 25 | Fun: Fn(Request) -> Fut, 26 | Fut: 'static + Future, Resp>> + Send, 27 | { 28 | type Future = BoxFuture<'static, Result, Self::Responder>>; 29 | type Responder = Resp; 30 | 31 | #[inline] 32 | fn validate(&self, req: Request) -> Self::Future { 33 | (*self)(req).boxed() 34 | } 35 | } 36 | 37 | /// Builder to apply guards onto the handler 38 | pub struct Builder { 39 | chain: Chain, 40 | } 41 | 42 | impl Default for Builder { 43 | fn default() -> Self { 44 | Self { chain: GuardChainEnd } 45 | } 46 | } 47 | 48 | impl Builder { 49 | pub fn apply(self, handler: Handler) -> Builder> 50 | where 51 | Handler: 'static + Guard + Sync + Send, 52 | { 53 | Builder { 54 | chain: GuardChainLink { handler, rest: self.chain }, 55 | } 56 | } 57 | 58 | pub(crate) fn build(self) -> Box { 59 | Box::new(self.chain) 60 | } 61 | } 62 | 63 | #[doc(hidden)] 64 | pub trait GuardChain: Sync + Send { 65 | fn validate(&'static self, req: Request) -> BoxFuture<'static, Result, Box>>; 66 | 67 | /// to avoid useless heap allocation if there is only a guard end chain 68 | fn is_end(&self) -> bool; 69 | } 70 | 71 | #[doc(hidden)] 72 | pub struct GuardChainEnd; 73 | 74 | impl GuardChain for GuardChainEnd { 75 | #[inline] 76 | fn validate(&'static self, req: Request) -> BoxFuture<'static, Result, Box>> { 77 | async { Ok(req) }.boxed() 78 | } 79 | 80 | #[inline] 81 | fn is_end(&self) -> bool { 82 | true 83 | } 84 | } 85 | 86 | #[doc(hidden)] 87 | pub struct GuardChainLink { 88 | handler: Handler, 89 | rest: Rest, 90 | } 91 | 92 | impl GuardChain for GuardChainLink 93 | where 94 | Handler: Guard + Sync + Send + 'static, 95 | Rest: GuardChain + 'static, 96 | { 97 | #[inline] 98 | fn validate(&'static self, req: Request) -> BoxFuture<'static, Result, Box>> { 99 | async move { 100 | match self.handler.validate(req).await { 101 | Ok(req) => { 102 | if self.rest.is_end() { 103 | Ok(req) 104 | } else { 105 | self.rest.validate(req).await 106 | } 107 | } 108 | Err(resp) => Err(Box::new(Some(resp)) as Box), 109 | } 110 | } 111 | .boxed() 112 | } 113 | 114 | #[inline] 115 | fn is_end(&self) -> bool { 116 | false 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /saphir_macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | // The `quote!` macro requires deep recursion. 2 | #![recursion_limit = "512"] 3 | #![allow(clippy::match_like_matches_macro)] 4 | 5 | extern crate proc_macro; 6 | 7 | use proc_macro::TokenStream as TokenStream1; 8 | use syn::{parse_macro_input, AttributeArgs, Item, ItemImpl}; 9 | 10 | mod controller; 11 | mod guard; 12 | mod middleware; 13 | mod openapi; 14 | mod utils; 15 | 16 | /// Saphir macro for auto trait implementation on controllers 17 | /// 18 | /// The base macro attribule look like this : `#[controller]` and is to be put 19 | /// on top of a Controller's method impl block 20 | /// 21 | /// ```ignore 22 | /// #use saphir::prelude::*; 23 | /// #use saphir_macro::controller; 24 | /// 25 | /// #struct ExampleController; 26 | /// 27 | /// #[controller] 28 | /// impl ExampleController { 29 | /// // .... 30 | /// } 31 | /// ``` 32 | /// 33 | /// Different arguments can be passed to the controller macro: 34 | /// - `name=""` will take place of the default controller name (by 35 | /// default the controller name is the struct name, lowercase, with the 36 | /// "controller keyword stripped"). the name will result as the basepath of 37 | /// the controller. 38 | /// - `version=` use for api version, the version will be added before the 39 | /// name as the controller basepath 40 | /// - `prefix=""` add a prefix before the basepath and the version. 41 | /// 42 | /// ##Example 43 | /// 44 | /// ```ignore 45 | /// use saphir::prelude::*; 46 | /// use saphir_macro::controller; 47 | /// 48 | /// struct ExampleController; 49 | /// 50 | /// #[controller(name="test", version=1, prefix="api")] 51 | /// impl ExampleController { 52 | /// // .... 53 | /// } 54 | /// ``` 55 | /// 56 | /// This will result in the Example controller being routed to `/api/v1/test` 57 | #[proc_macro_attribute] 58 | pub fn controller(args: TokenStream1, input: TokenStream1) -> TokenStream1 { 59 | let args = parse_macro_input!(args as AttributeArgs); 60 | let input = parse_macro_input!(input as ItemImpl); 61 | 62 | let expanded = controller::expand_controller(args, input).unwrap_or_else(|e| e.to_compile_error()); 63 | 64 | TokenStream1::from(expanded) 65 | } 66 | 67 | #[proc_macro_attribute] 68 | pub fn middleware(_args: TokenStream1, input: TokenStream1) -> TokenStream1 { 69 | let input = parse_macro_input!(input as ItemImpl); 70 | 71 | let expanded = middleware::expand_middleware(input).unwrap_or_else(|e| e.to_compile_error()); 72 | 73 | TokenStream1::from(expanded) 74 | } 75 | 76 | #[proc_macro_attribute] 77 | pub fn guard(_args: TokenStream1, input: TokenStream1) -> TokenStream1 { 78 | let input = parse_macro_input!(input as ItemImpl); 79 | 80 | let expanded = guard::expand_guard(input).unwrap_or_else(|e| e.to_compile_error()); 81 | 82 | TokenStream1::from(expanded) 83 | } 84 | 85 | /// Saphir OpenAPI macro which can be put on top of a struct or enum definition. 86 | /// Allow specifying informations for the corresponding type when generating 87 | /// OpenAPI documentation through saphir's CLI. 88 | /// 89 | /// The syntax looks like this : `#[openapi(mime = "application/json")]`. 90 | /// `mime` can either be a full mimetype, or one of the following keywords: 91 | /// - json (application/json) 92 | /// - form (application/x-www-form-urlencoded) 93 | /// - any (*/*) 94 | #[proc_macro_attribute] 95 | pub fn openapi(args: TokenStream1, input: TokenStream1) -> TokenStream1 { 96 | let args = parse_macro_input!(args as AttributeArgs); 97 | let parsed_input = parse_macro_input!(input as Item); 98 | 99 | let expanded = openapi::validate_openapi(args, parsed_input).unwrap_or_else(|e| e.to_compile_error()); 100 | 101 | TokenStream1::from(expanded) 102 | } 103 | -------------------------------------------------------------------------------- /saphir_macro/src/middleware/fun.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span}; 2 | use syn::{Error, FnArg, ImplItem, Result, Signature, Type, TypeParamBound}; 3 | 4 | pub struct MidFnDef { 5 | pub def: ImplItem, 6 | pub fn_ident: Ident, 7 | } 8 | 9 | impl MidFnDef { 10 | pub fn new(mid_fn: ImplItem) -> Result { 11 | let mut m = if let ImplItem::Method(m) = mid_fn { 12 | m 13 | } else { 14 | return Err(Error::new_spanned(mid_fn, "The token named next is not method")); 15 | }; 16 | 17 | check_signature(&m.sig)?; 18 | 19 | let fn_ident = Ident::new(&format!("{}_wrapped", m.sig.ident), Span::call_site()); 20 | m.sig.ident = fn_ident.clone(); 21 | 22 | Ok(MidFnDef { 23 | def: ImplItem::Method(m), 24 | fn_ident, 25 | }) 26 | } 27 | } 28 | 29 | fn check_signature(m: &Signature) -> Result<()> { 30 | if m.asyncness.is_none() { 31 | return Err(Error::new_spanned(m, "Invalid function signature, the middleware function should be async")); 32 | } 33 | 34 | if m.inputs.len() != 3 { 35 | return Err(Error::new_spanned( 36 | m, 37 | "Invalid middleware function input parameters.\nExpected the following parameters:\n (&self, _: HttpContext, _: &dyn MiddlewareChain)", 38 | )); 39 | } 40 | 41 | let mut input_args = m.inputs.iter(); 42 | 43 | match input_args.next().expect("len was checked above") { 44 | FnArg::Receiver(_) => {} 45 | arg => { 46 | return Err(Error::new_spanned(arg, "Invalid 1st parameter, expected `&self`")); 47 | } 48 | } 49 | 50 | let arg2 = input_args.next().expect("len was checked above"); 51 | let passed = match arg2 { 52 | FnArg::Typed(t) => { 53 | if let Type::Path(pt) = &*t.ty { 54 | pt.path 55 | .segments 56 | .first() 57 | .ok_or_else(|| Error::new_spanned(&t.ty, "Unexpected type"))? 58 | .ident 59 | .to_string() 60 | .eq("HttpContext") 61 | } else { 62 | false 63 | } 64 | } 65 | _ => false, 66 | }; 67 | 68 | if !passed { 69 | return Err(Error::new_spanned(arg2, "Invalid 2nd parameter, expected `HttpContext`")); 70 | } 71 | 72 | let arg3 = input_args.next().expect("len was checked above"); 73 | let passed = match arg3 { 74 | FnArg::Typed(t) => { 75 | if let Type::Reference(tr) = &*t.ty { 76 | if let Type::TraitObject(to) = &*tr.elem { 77 | if let TypeParamBound::Trait(bo) = to.bounds.first().ok_or_else(|| Error::new_spanned(&t.ty, "Unexpected type"))? { 78 | bo.path 79 | .segments 80 | .first() 81 | .ok_or_else(|| Error::new_spanned(&t.ty, "Unexpected type"))? 82 | .ident 83 | .to_string() 84 | .eq("MiddlewareChain") 85 | } else { 86 | false 87 | } 88 | } else if let Type::Path(pt) = &*tr.elem { 89 | pt.path 90 | .segments 91 | .first() 92 | .ok_or_else(|| Error::new_spanned(&t.ty, "Unexpected type"))? 93 | .ident 94 | .to_string() 95 | .eq("MiddlewareChain") 96 | } else { 97 | false 98 | } 99 | } else { 100 | false 101 | } 102 | } 103 | _ => false, 104 | }; 105 | 106 | if !passed { 107 | return Err(Error::new_spanned(arg3, "Invalid 3nd parameter, expected `&dyn MiddlewareChain`")); 108 | } 109 | 110 | Ok(()) 111 | } 112 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/generate/route_info.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::{ 2 | generate::{crate_syn_browser::Method, Gen}, 3 | schema::OpenApiPathMethod, 4 | }; 5 | use convert_case::Casing; 6 | use syn::{Attribute, Lit, Meta, NestedMeta, Signature}; 7 | 8 | #[derive(Clone, Debug)] 9 | pub(crate) struct RouteInfo { 10 | pub(crate) method: OpenApiPathMethod, 11 | pub(crate) uri: String, 12 | pub(crate) uri_params: Vec, 13 | pub(crate) operation_id: String, 14 | pub(crate) operation_name: String, 15 | } 16 | 17 | impl Gen { 18 | /// Retrieve Routes (RouteInfo) from a method with a saphir route macro. 19 | pub(crate) fn extract_routes_info_from_method_macro(&self, method: &Method, controller_path: &str) -> Vec { 20 | let routes: Vec<_> = method 21 | .syn 22 | .attrs 23 | .iter() 24 | .filter_map(|attr| self.handler_method_from_attr(attr).zip(Some(attr))) 25 | .filter_map(|(method, attr)| self.handler_path_from_attr(attr).map(|(path, uri_params)| (method, path, uri_params))) 26 | .collect(); 27 | 28 | let multi = routes.len() > 1; 29 | 30 | routes 31 | .into_iter() 32 | .filter_map(|(m, path, uri_params)| self.extract_route_info_from_method_macro(controller_path, m, path, uri_params, multi, &method.syn.sig)) 33 | .collect() 34 | } 35 | 36 | fn extract_route_info_from_method_macro( 37 | &self, 38 | controller_path: &str, 39 | method: OpenApiPathMethod, 40 | path: String, 41 | uri_params: Vec, 42 | multi: bool, 43 | sig: &Signature, 44 | ) -> Option { 45 | let mut full_path = format!("/{}{}", controller_path, path); 46 | if full_path.ends_with('/') { 47 | full_path = full_path[0..(full_path.len() - 1)].to_string(); 48 | } 49 | if !full_path.starts_with(self.args.scope.as_str()) { 50 | return None; 51 | } 52 | let operation_id = self.handler_operation_id_from_sig(sig); 53 | let operation_name = self.handler_operation_name_from_sig(sig, if multi { Some(method.to_str()) } else { None }); 54 | 55 | Some(RouteInfo { 56 | method, 57 | uri: full_path, 58 | uri_params, 59 | operation_id, 60 | operation_name, 61 | }) 62 | } 63 | 64 | fn handler_operation_name_from_sig(&self, sig: &Signature, prefix: Option<&str>) -> String { 65 | let name = sig.ident.to_string(); 66 | match prefix { 67 | Some(p) => format!("{}_{}", p, name), 68 | None => name, 69 | } 70 | .to_case((&self.args.operation_name_case).into()) 71 | } 72 | 73 | fn handler_method_from_attr(&self, attr: &Attribute) -> Option { 74 | let ident = attr.path.get_ident()?; 75 | OpenApiPathMethod::from_str(ident.to_string().as_str()) 76 | } 77 | 78 | fn handler_path_from_attr(&self, attr: &Attribute) -> Option<(String, Vec)> { 79 | if let Ok(Meta::List(meta)) = attr.parse_meta() { 80 | if let Some(NestedMeta::Lit(Lit::Str(l))) = meta.nested.first() { 81 | let mut chars: Vec = l.value().chars().collect(); 82 | let mut params: Vec = Vec::new(); 83 | 84 | let mut i = 0; 85 | while i < chars.len() { 86 | if chars[i] == '<' || chars[i] == '{' { 87 | chars[i] = '{'; 88 | let start = i; 89 | for j in start..chars.len() { 90 | if chars[j] == '>' || chars[j] == '}' { 91 | chars[j] = '}'; 92 | params.push(chars[(i + 1)..j].iter().collect()); 93 | i = j; 94 | break; 95 | } 96 | } 97 | } 98 | i += 1; 99 | } 100 | 101 | return Some((chars.into_iter().collect(), params)); 102 | } 103 | } 104 | None 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/generate/utils.rs: -------------------------------------------------------------------------------- 1 | use convert_case::{Case, Casing}; 2 | use syn::{Attribute, Lit, Meta, NestedMeta}; 3 | 4 | pub(crate) fn case_from_serde_rename_str(case_name: &str) -> Option { 5 | match case_name { 6 | "lowercase" => Some(Case::Lower), 7 | "UPPERCASE" => Some(Case::Upper), 8 | "PascalCase" => Some(Case::Pascal), 9 | "camelCase" => Some(Case::Camel), 10 | "snake_case" => Some(Case::Snake), 11 | "SCREAMING_SNAKE_CASE" => Some(Case::ScreamingSnake), 12 | "kebab-case" => Some(Case::Kebab), 13 | "SCREAMING-KEBAB-CASE" => Some(Case::Cobol), 14 | _ => None, 15 | } 16 | } 17 | 18 | pub(crate) fn get_serde_field(mut field_name: String, field_attributes: &[Attribute], container_attributes: &[Attribute]) -> Option { 19 | if find_macro_attribute_flag(field_attributes, "serde", "skip") || find_macro_attribute_flag(field_attributes, "serde", "skip_serializing") { 20 | return None; 21 | } 22 | if let Some(Lit::Str(rename)) = find_macro_attribute_named_value(field_attributes, "serde", "rename") { 23 | field_name = rename.value(); 24 | } else if let Some(Lit::Str(rename)) = find_macro_attribute_named_value(container_attributes, "serde", "rename_all") { 25 | if let Some(case) = case_from_serde_rename_str(rename.value().as_str()) { 26 | field_name = field_name.to_case(case); 27 | } 28 | } 29 | Some(field_name) 30 | } 31 | 32 | pub(crate) fn find_macro_attribute_flag(attrs: &[Attribute], macro_name: &str, value_name: &str) -> bool { 33 | for attr in attrs 34 | .iter() 35 | .filter(|a| a.path.get_ident().filter(|i| i.to_string().as_str() == macro_name).is_some()) 36 | { 37 | if let Ok(meta) = attr.parse_meta() { 38 | if find_macro_attribute_flag_from_meta(&meta, value_name) { 39 | return true; 40 | } 41 | } 42 | } 43 | false 44 | } 45 | 46 | pub(crate) fn find_macro_attribute_flag_from_meta(meta: &Meta, value_name: &str) -> bool { 47 | match meta { 48 | Meta::List(l) => { 49 | for n in &l.nested { 50 | match n { 51 | NestedMeta::Meta(nm) => { 52 | if find_macro_attribute_flag_from_meta(nm, value_name) { 53 | return true; 54 | } 55 | } 56 | NestedMeta::Lit(_) => {} 57 | } 58 | } 59 | } 60 | Meta::Path(p) => { 61 | if p.get_ident().map(|i| i.to_string()).filter(|s| s == value_name).is_some() { 62 | return true; 63 | } 64 | } 65 | _ => {} 66 | } 67 | false 68 | } 69 | 70 | pub(crate) fn find_macro_attribute_named_value(attrs: &[Attribute], macro_name: &str, value_name: &str) -> Option { 71 | for attr in attrs 72 | .iter() 73 | .filter(|a| a.path.get_ident().filter(|i| i.to_string().as_str() == macro_name).is_some()) 74 | { 75 | if let Ok(meta) = attr.parse_meta() { 76 | if let Some(s) = find_macro_attribute_value_from_meta(&meta, value_name) { 77 | return Some(s); 78 | } 79 | } 80 | } 81 | None 82 | } 83 | pub(crate) fn find_macro_attribute_value_from_meta(meta: &Meta, value_name: &str) -> Option { 84 | match meta { 85 | Meta::List(l) => { 86 | for n in &l.nested { 87 | match n { 88 | NestedMeta::Meta(nm) => { 89 | if let Some(s) = find_macro_attribute_value_from_meta(nm, value_name) { 90 | return Some(s); 91 | } 92 | } 93 | NestedMeta::Lit(l) => { 94 | println!(" Litteral meta : {:?}", l); 95 | } 96 | } 97 | } 98 | } 99 | Meta::NameValue(nv) => { 100 | if nv.path.get_ident().map(|i| i.to_string()).filter(|s| s == value_name).is_some() { 101 | return Some(nv.lit.clone()); 102 | } 103 | } 104 | _ => {} 105 | } 106 | None 107 | } 108 | -------------------------------------------------------------------------------- /saphir/examples/macro.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use saphir::{file::middleware::FileMiddlewareBuilder, prelude::*}; 3 | use serde_derive::{Deserialize, Serialize}; 4 | 5 | struct PrintGuard { 6 | inner: String, 7 | } 8 | 9 | #[guard] 10 | impl PrintGuard { 11 | pub fn new(inner: &str) -> Self { 12 | PrintGuard { inner: inner.to_string() } 13 | } 14 | 15 | async fn validate(&self, req: Request) -> Result { 16 | info!("{}", self.inner); 17 | Ok(req) 18 | } 19 | } 20 | 21 | #[derive(Serialize, Deserialize, Clone)] 22 | struct User { 23 | username: String, 24 | age: i64, 25 | } 26 | 27 | struct UserController {} 28 | 29 | #[controller(name = "users", version = 1, prefix = "api")] 30 | impl UserController { 31 | #[get("/")] 32 | async fn get_user(&self, user_id: String, action: Option) -> (u16, String) { 33 | (200, format!("user_id: {}, action: {:?}", user_id, action)) 34 | } 35 | 36 | #[post("/json")] 37 | #[validator(exclude("user"))] 38 | async fn post_user_json(&self, user: Json) -> (u16, Json) { 39 | (200, user) 40 | } 41 | 42 | #[get("/form")] 43 | #[post("/form")] 44 | #[validator(exclude("user"))] 45 | async fn user_form(&self, user: Form) -> (u16, Form) { 46 | (200, user) 47 | } 48 | 49 | #[cookies] 50 | #[post("/sync")] 51 | #[validator(exclude("req"))] 52 | fn get_user_sync(&self, mut req: Request>) -> (u16, Json) { 53 | let u = req.body_mut(); 54 | u.username = "Samuel".to_string(); 55 | (200, Json(u.clone())) 56 | } 57 | 58 | #[get("/")] 59 | #[guard(PrintGuard, init_expr = "UserController::BASE_PATH")] 60 | async fn list_user(&self, _req: Request) -> (u16, String) { 61 | (200, "Yo".to_string()) 62 | } 63 | 64 | #[post("/multi")] 65 | async fn multipart(&self, mul: Multipart) -> (u16, String) { 66 | let mut multipart_image_count = 0; 67 | while let Ok(Some(mut f)) = mul.next_field().await { 68 | if f.content_type() == &mime::IMAGE_PNG { 69 | let _ = f.save(format!("/tmp/{}.png", f.name())).await; 70 | multipart_image_count += 1; 71 | } 72 | } 73 | 74 | (200, format!("Multipart form data image saved on disk: {}", multipart_image_count)) 75 | } 76 | 77 | #[get("/file")] 78 | async fn file(&self, _req: Request>>) -> (u16, Option) { 79 | match File::open("/path/to/file").await { 80 | Ok(file) => (200, Some(file)), 81 | Err(_) => (500, None), 82 | } 83 | } 84 | } 85 | 86 | #[allow(dead_code)] 87 | struct ApiKeyMiddleware(String); 88 | 89 | #[middleware] 90 | impl ApiKeyMiddleware { 91 | pub fn new(api_key: &str) -> Self { 92 | ApiKeyMiddleware(api_key.to_string()) 93 | } 94 | 95 | async fn next(&self, ctx: HttpContext, chain: &dyn MiddlewareChain) -> Result { 96 | if let Some(Ok("Bearer secure-key")) = ctx 97 | .state 98 | .request_unchecked() 99 | .headers() 100 | .get(header::AUTHORIZATION) 101 | .map(|auth_value| auth_value.to_str()) 102 | { 103 | info!("Authenticated"); 104 | } else { 105 | info!("Not Authenticated"); 106 | } 107 | 108 | info!("Handler {} will be used", ctx.metadata.name.unwrap_or("unknown")); 109 | chain.next(ctx).await 110 | } 111 | } 112 | 113 | #[tokio::main] 114 | async fn main() -> Result<(), SaphirError> { 115 | env_logger::init(); 116 | 117 | let file_middleware = FileMiddlewareBuilder::new("op", "./saphir/examples/files_to_serve").build()?; 118 | let server = Server::builder() 119 | .configure_listener(|l| l.interface("127.0.0.1:3000").server_name("MacroExample").request_timeout(None)) 120 | .configure_middlewares(|m| { 121 | m.apply(ApiKeyMiddleware::new("secure-key"), vec!["/"], None) 122 | .apply(file_middleware, vec!["/op/"], None) 123 | }) 124 | .configure_router(|r| r.controller(UserController {})) 125 | .build(); 126 | 127 | server.run().await 128 | } 129 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/generate/controller_info.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::generate::{ 2 | crate_syn_browser::{Impl, ImplItemKind}, 3 | handler_info::HandlerInfo, 4 | Gen, 5 | }; 6 | use syn::{Lit, Meta, NestedMeta, Type}; 7 | 8 | #[derive(Clone, Debug, Default)] 9 | pub(crate) struct ControllerInfo { 10 | pub(crate) controller_name: String, 11 | pub(crate) name: String, 12 | pub(crate) version: Option, 13 | pub(crate) prefix: Option, 14 | pub(crate) handlers: Vec, 15 | } 16 | 17 | impl ControllerInfo { 18 | pub fn base_path(&self) -> String { 19 | let mut path = self.name.clone(); 20 | if let Some(ver) = &self.version { 21 | path = format!("v{}/{}", ver, path); 22 | } 23 | if let Some(prefix) = &self.prefix { 24 | path = format!("{}/{}", prefix, path); 25 | } 26 | path 27 | } 28 | } 29 | 30 | impl Gen { 31 | /// Retrieve ControllerInfo from an implementation block. 32 | /// Saphir does not currently support multiple implementation blocks for the 33 | /// same controller. 34 | pub(crate) fn extract_controller_info<'b>(&mut self, im: &'b Impl<'b>) -> Option { 35 | for attr in &im.syn.attrs { 36 | if let Some(first_seg) = attr.path.segments.first() { 37 | let t = im.syn.self_ty.as_ref(); 38 | if let Type::Path(p) = t { 39 | if let Some(struct_first_seg) = p.path.segments.first() { 40 | if first_seg.ident.eq("controller") { 41 | let controller_name = struct_first_seg.ident.to_string(); 42 | let name = controller_name.to_ascii_lowercase(); 43 | let name = &name[0..name.rfind("controller").unwrap_or(name.len())]; 44 | let mut name = name.to_string(); 45 | let mut prefix = None; 46 | let mut version = None; 47 | if let Ok(Meta::List(meta)) = attr.parse_meta() { 48 | for nested in meta.nested { 49 | if let NestedMeta::Meta(Meta::NameValue(nv)) = nested { 50 | if let Some(p) = nv.path.segments.first() { 51 | let value = match nv.lit { 52 | Lit::Str(s) => s.value(), 53 | Lit::Int(i) => i.to_string(), 54 | _ => continue, 55 | }; 56 | match p.ident.to_string().as_str() { 57 | "name" => name = value, 58 | "prefix" => prefix = Some(value), 59 | "version" => version = Some(value), 60 | _ => {} 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | let mut controller = ControllerInfo { 68 | controller_name, 69 | name, 70 | prefix, 71 | version, 72 | handlers: Vec::new(), 73 | }; 74 | let mut handlers = im 75 | .items() 76 | .iter() 77 | .filter_map(|i| match i.kind() { 78 | ImplItemKind::Method(m) => self.extract_handler_info(controller.base_path().as_str(), m), 79 | _ => None, 80 | }) 81 | .collect::>(); 82 | std::mem::swap(&mut controller.handlers, &mut handlers); 83 | return Some(controller); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | None 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/generate/crate_syn_browser/item.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use super::Module; 3 | use crate::openapi::generate::crate_syn_browser::UseScope; 4 | use lazycell::LazyCell; 5 | use std::fmt::{Debug, Formatter}; 6 | use syn::{ 7 | ImplItem as SynImplItem, ImplItemMethod as SynImplItemMethod, Item as SynItem, ItemEnum as SynItemEnum, ItemImpl as SynItemImpl, 8 | ItemStruct as SynItemStruct, ItemUse as SynItemUse, 9 | }; 10 | 11 | pub struct Item<'b> { 12 | pub scope: &'b dyn UseScope<'b>, 13 | pub item: &'b SynItem, 14 | kind: LazyCell>, 15 | } 16 | 17 | impl Debug for Item<'_> { 18 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 19 | write!(f, "{:?}", self.kind) 20 | } 21 | } 22 | 23 | impl<'b> Item<'b> { 24 | pub fn kind(&'b self) -> &'b ItemKind<'b> { 25 | self.kind.borrow().expect("Should be initialized by Item::init_new() after created with new()") 26 | } 27 | } 28 | 29 | #[derive(Debug)] 30 | pub enum ItemKind<'b> { 31 | Use(Use<'b>), 32 | Struct(Struct<'b>), 33 | Enum(Enum<'b>), 34 | Impl(Impl<'b>), 35 | Unsupported, 36 | } 37 | 38 | #[derive(Debug)] 39 | pub struct Use<'b> { 40 | pub item: &'b Item<'b>, 41 | pub syn: &'b SynItemUse, 42 | } 43 | 44 | #[derive(Debug)] 45 | pub struct Enum<'b> { 46 | pub item: &'b Item<'b>, 47 | pub syn: &'b SynItemEnum, 48 | } 49 | 50 | #[derive(Debug)] 51 | pub struct Struct<'b> { 52 | pub item: &'b Item<'b>, 53 | pub syn: &'b SynItemStruct, 54 | } 55 | 56 | #[derive(Debug)] 57 | pub struct Impl<'b> { 58 | pub item: &'b Item<'b>, 59 | pub syn: &'b SynItemImpl, 60 | items: LazyCell>>, 61 | } 62 | 63 | impl<'b> Impl<'b> { 64 | pub fn items(&'b self) -> &'b Vec> { 65 | self.items.borrow().expect("Impl should always be initialized") 66 | } 67 | } 68 | 69 | #[derive(Debug)] 70 | pub struct ImplItem<'b> { 71 | pub im: &'b Impl<'b>, 72 | pub syn: &'b SynImplItem, 73 | kind: LazyCell>, 74 | } 75 | 76 | impl<'b> ImplItem<'b> { 77 | pub fn kind(&'b self) -> &'b ImplItemKind<'b> { 78 | self.kind.borrow().expect("Impl item should be initialized by crate_syn_browser") 79 | } 80 | } 81 | 82 | impl<'b> Impl<'b> { 83 | pub(self) fn new(syn: &'b SynItemImpl, item: &'b Item<'b>) -> Self { 84 | Impl { 85 | item, 86 | syn, 87 | items: LazyCell::new(), 88 | } 89 | } 90 | 91 | pub(self) fn init_new(&'b self) { 92 | let items: Vec> = self 93 | .syn 94 | .items 95 | .iter() 96 | .map(|i| ImplItem { 97 | im: self, 98 | syn: i, 99 | kind: LazyCell::new(), 100 | }) 101 | .collect(); 102 | self.items.fill(items).expect("init_new must be called only once"); 103 | 104 | for (i, syn_item) in self.syn.items.iter().enumerate() { 105 | let impl_item = self 106 | .items 107 | .borrow() 108 | .expect("initialized above") 109 | .get(i) 110 | .expect("should be 1:1 with the items added above"); 111 | impl_item 112 | .kind 113 | .fill(match syn_item { 114 | SynImplItem::Method(syn) => ImplItemKind::Method(Method { syn, impl_item }), 115 | _ => ImplItemKind::Unsupported, 116 | }) 117 | .expect("should be filled only once") 118 | } 119 | } 120 | } 121 | 122 | #[derive(Debug)] 123 | pub enum ImplItemKind<'b> { 124 | Method(Method<'b>), 125 | Unsupported, 126 | } 127 | 128 | #[derive(Debug)] 129 | pub struct Method<'b> { 130 | pub impl_item: &'b ImplItem<'b>, 131 | pub syn: &'b SynImplItemMethod, 132 | } 133 | 134 | impl<'b> Item<'b> { 135 | pub fn new(scope: &'b Module<'b>, item: &'b SynItem) -> Self { 136 | Self { 137 | scope, 138 | item, 139 | kind: LazyCell::new(), 140 | } 141 | } 142 | 143 | pub fn init_new(&'b self) { 144 | self.kind 145 | .fill(match self.item { 146 | SynItem::Use(syn) => ItemKind::Use(Use { syn, item: self }), 147 | SynItem::Enum(syn) => ItemKind::Enum(Enum { syn, item: self }), 148 | SynItem::Struct(syn) => ItemKind::Struct(Struct { syn, item: self }), 149 | SynItem::Impl(syn) => ItemKind::Impl(Impl::new(syn, self)), 150 | _ => ItemKind::Unsupported, 151 | }) 152 | .expect("init_new should be called only once"); 153 | 154 | #[allow(clippy::single_match)] 155 | match self.kind() { 156 | ItemKind::Impl(i) => i.init_new(), 157 | _ => {} 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /saphir/src/file/content_range.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2018 Sean McArthur 2 | // Copyright (c) 2018 The hyperx Contributors 3 | // license: https://github.com/dekellum/hyperx/blob/master/LICENSE 4 | // source: https://github.com/dekellum/hyperx/blob/master/src/header/common/content_range.rs 5 | 6 | use crate::error::SaphirError; 7 | use std::{fmt::Display, str::FromStr}; 8 | 9 | /// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2) 10 | /// 11 | /// # ABNF 12 | /// 13 | /// ```text 14 | /// Content-Range = byte-content-range 15 | /// / other-content-range 16 | /// 17 | /// byte-content-range = bytes-unit SP 18 | /// ( byte-range-resp / unsatisfied-range ) 19 | /// 20 | /// byte-range-resp = byte-range "/" ( complete-length / "*" ) 21 | /// byte-range = first-byte-pos "-" last-byte-pos 22 | /// unsatisfied-range = "*/" complete-length 23 | /// 24 | /// complete-length = 1*DIGIT 25 | /// 26 | /// other-content-range = other-range-unit SP other-range-resp 27 | /// other-range-resp = *CHAR 28 | /// ``` 29 | #[derive(PartialEq, Eq, Clone, Debug)] 30 | pub enum ContentRange { 31 | /// Byte range 32 | Bytes { 33 | /// First and last bytes of the range, omitted if request could not be 34 | /// satisfied 35 | range: Option<(u64, u64)>, 36 | 37 | /// Total length of the instance, can be omitted if unknown 38 | instance_length: Option, 39 | }, 40 | 41 | /// Custom range, with unit not registered at IANA 42 | Unregistered { 43 | /// other-range-unit 44 | unit: String, 45 | 46 | /// other-range-resp 47 | resp: String, 48 | }, 49 | } 50 | 51 | fn split_in_two(s: &str, separator: char) -> Option<(&str, &str)> { 52 | let mut iter = s.splitn(2, separator); 53 | match (iter.next(), iter.next()) { 54 | (Some(a), Some(b)) => Some((a, b)), 55 | _ => None, 56 | } 57 | } 58 | 59 | impl FromStr for ContentRange { 60 | type Err = SaphirError; 61 | 62 | fn from_str(s: &str) -> Result { 63 | let res = match split_in_two(s, ' ') { 64 | Some(("bytes", resp)) => { 65 | let (range, instance_length) = split_in_two(resp, '/').ok_or_else(|| SaphirError::Other("Could not parse Content-Range".to_owned()))?; 66 | 67 | let instance_length = if instance_length == "*" { 68 | None 69 | } else { 70 | Some( 71 | instance_length 72 | .parse() 73 | .map_err(|_| SaphirError::Other("Could not parse Content-Range".to_owned()))?, 74 | ) 75 | }; 76 | 77 | let range = if range == "*" { 78 | None 79 | } else { 80 | let (first_byte, last_byte) = split_in_two(range, '-').ok_or_else(|| SaphirError::Other("Could not parse bytes in range".to_owned()))?; 81 | let first_byte = first_byte.parse().map_err(|_| SaphirError::Other("Could not parse byte in range".to_owned()))?; 82 | let last_byte = last_byte.parse().map_err(|_| SaphirError::Other("Could not parse byte in range".to_owned()))?; 83 | if last_byte < first_byte { 84 | return Err(SaphirError::Other("Byte order incorrect".to_owned())); 85 | } 86 | Some((first_byte, last_byte)) 87 | }; 88 | 89 | ContentRange::Bytes { range, instance_length } 90 | } 91 | Some((unit, resp)) => ContentRange::Unregistered { 92 | unit: unit.to_owned(), 93 | resp: resp.to_owned(), 94 | }, 95 | _ => return Err(SaphirError::Other("Range missing or incomplete".to_owned())), 96 | }; 97 | Ok(res) 98 | } 99 | } 100 | 101 | impl Display for ContentRange { 102 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 103 | match *self { 104 | ContentRange::Bytes { range, instance_length } => { 105 | write!(f, "bytes ")?; 106 | match range { 107 | Some((first_byte, last_byte)) => { 108 | write!(f, "{first_byte}-{last_byte}")?; 109 | } 110 | None => { 111 | write!(f, "*")?; 112 | } 113 | }; 114 | write!(f, "/")?; 115 | if let Some(v) = instance_length { 116 | write!(f, "{v}")?; 117 | } else { 118 | write!(f, "*")?; 119 | } 120 | 121 | Ok(()) 122 | } 123 | ContentRange::Unregistered { ref unit, ref resp } => write!(f, "{unit} {resp}"), 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /saphir_macro/src/controller/controller_attr.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::handler::{HandlerAttrs, HandlerRepr}; 2 | use proc_macro2::{Ident, TokenStream}; 3 | use syn::{AttributeArgs, Error, ItemImpl, Lit, Meta, MetaNameValue, NestedMeta, Result}; 4 | 5 | use quote::{quote, ToTokens}; 6 | 7 | #[derive(Debug)] 8 | pub struct ControllerAttr { 9 | pub ident: Ident, 10 | pub name: String, 11 | pub version: Option, 12 | pub prefix: Option, 13 | } 14 | 15 | impl ControllerAttr { 16 | pub fn new(args: AttributeArgs, input: &ItemImpl) -> Result { 17 | let mut name = None; 18 | let mut version = None; 19 | let mut prefix = None; 20 | 21 | let ident = crate::utils::parse_item_impl_ident(input)?; 22 | 23 | for m in args.into_iter().filter_map(|a| if let NestedMeta::Meta(m) = a { Some(m) } else { None }) { 24 | match m { 25 | Meta::Path(p) => { 26 | return Err(Error::new_spanned(p, "Unexpected Attribute on controller impl")); 27 | } 28 | Meta::List(l) => { 29 | return Err(Error::new_spanned(l, "Unexpected Attribute on controller impl")); 30 | } 31 | Meta::NameValue(MetaNameValue { path, lit, .. }) => match (path.segments.first().map(|p| p.ident.to_string()).as_deref(), lit) { 32 | (Some("name"), Lit::Str(bp)) => { 33 | name = Some(bp.value().trim_matches('/').to_string()); 34 | } 35 | (Some("version"), Lit::Str(v)) => { 36 | version = Some( 37 | v.value() 38 | .parse::() 39 | .map_err(|_| Error::new_spanned(v, "Invalid version, expected number between 1 & u16::MAX"))?, 40 | ); 41 | } 42 | (Some("version"), Lit::Int(v)) => { 43 | version = Some( 44 | v.base10_parse::() 45 | .map_err(|_| Error::new_spanned(v, "Invalid version, expected number between 1 & u16::MAX"))?, 46 | ); 47 | } 48 | (Some("prefix"), Lit::Str(p)) => { 49 | prefix = Some(p.value().trim_matches('/').to_string()); 50 | } 51 | _ => { 52 | return Err(Error::new_spanned(path, "Unexpected Param in controller macro")); 53 | } 54 | }, 55 | } 56 | } 57 | 58 | let name = name.unwrap_or_else(|| ident.to_string().to_lowercase().trim_end_matches("controller").to_string()); 59 | 60 | Ok(ControllerAttr { 61 | ident: ident.clone(), 62 | name, 63 | version, 64 | prefix, 65 | }) 66 | } 67 | } 68 | 69 | pub fn gen_controller_trait_implementation(attrs: &ControllerAttr, handlers: &[HandlerRepr]) -> TokenStream { 70 | let controller_base_path = gen_controller_base_path_const(attrs); 71 | let controller_handlers_fn = gen_controller_handlers_fn(attrs, handlers); 72 | 73 | let ident = &attrs.ident; 74 | let e = quote! { 75 | impl Controller for #ident { 76 | #controller_base_path 77 | 78 | #controller_handlers_fn 79 | } 80 | }; 81 | 82 | e 83 | } 84 | 85 | fn gen_controller_base_path_const(attr: &ControllerAttr) -> TokenStream { 86 | let mut path = "/".to_string(); 87 | 88 | if let Some(prefix) = attr.prefix.as_ref() { 89 | path.push_str(prefix); 90 | path.push('/'); 91 | } 92 | 93 | if let Some(version) = attr.version { 94 | path.push('v'); 95 | path.push_str(&version.to_string()); 96 | path.push('/'); 97 | } 98 | 99 | path.push_str(attr.name.as_str()); 100 | 101 | let e = quote! { 102 | const BASE_PATH: &'static str = #path; 103 | }; 104 | 105 | e 106 | } 107 | 108 | fn gen_controller_handlers_fn(attr: &ControllerAttr, handlers: &[HandlerRepr]) -> TokenStream { 109 | let mut handler_stream = TokenStream::new(); 110 | let ctrl_ident = &attr.ident; 111 | 112 | for handler in handlers { 113 | let HandlerAttrs { methods_paths, guards, .. } = &handler.attrs; 114 | let handler_ident = &handler.original_method.sig.ident; 115 | 116 | for (method, path) in methods_paths { 117 | let method = method.as_str(); 118 | let handler_name = handler_ident.to_string(); 119 | if guards.is_empty() { 120 | (quote! { 121 | .add_with_name(#handler_name, Method::from_str(#method).expect("Method was validated by the macro expansion"), #path, #ctrl_ident::#handler_ident) 122 | }) 123 | .to_tokens(&mut handler_stream); 124 | } else { 125 | let mut guard_stream = TokenStream::new(); 126 | 127 | for guard_def in guards { 128 | (quote! { 129 | .apply(#guard_def) 130 | }) 131 | .to_tokens(&mut guard_stream); 132 | } 133 | 134 | (quote! { 135 | .add_with_guards_and_name(#handler_name, Method::from_str(#method).expect("Method was validated the macro expansion"), #path, #ctrl_ident::#handler_ident, |g| { 136 | g #guard_stream 137 | }) 138 | }) 139 | .to_tokens(&mut handler_stream); 140 | } 141 | } 142 | } 143 | 144 | let quoted_h = quote! { 145 | fn handlers(&self) -> Vec> where Self: Sized { 146 | EndpointsBuilder::new() 147 | #handler_stream 148 | .build() 149 | } 150 | }; 151 | quoted_h 152 | } 153 | -------------------------------------------------------------------------------- /saphir_macro/src/guard/fun.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span}; 2 | use syn::{ 3 | AngleBracketedGenericArguments, Error, FnArg, GenericArgument, ImplItem, PatType, Path, PathArguments, Result, ReturnType, Signature, Type, TypePath, 4 | }; 5 | 6 | pub struct GuardFnDef { 7 | pub def: ImplItem, 8 | pub responder: Path, 9 | pub fn_ident: Ident, 10 | } 11 | 12 | impl GuardFnDef { 13 | pub fn new(g_fn: ImplItem) -> Result { 14 | let mut m = if let ImplItem::Method(m) = g_fn { 15 | m 16 | } else { 17 | return Err(Error::new_spanned(g_fn, "The token named validate is not method")); 18 | }; 19 | 20 | let responder = check_signature(&m.sig)?; 21 | 22 | let fn_ident = Ident::new(&format!("{}_wrapped", m.sig.ident), Span::call_site()); 23 | m.sig.ident = fn_ident.clone(); 24 | 25 | Ok(GuardFnDef { 26 | def: ImplItem::Method(m), 27 | responder, 28 | fn_ident, 29 | }) 30 | } 31 | } 32 | 33 | fn check_signature(m: &Signature) -> Result { 34 | if m.asyncness.is_none() { 35 | return Err(Error::new_spanned(m, "Invalid function signature, the guard function should be async")); 36 | } 37 | 38 | if m.inputs.len() != 2 { 39 | return Err(Error::new_spanned( 40 | m, 41 | "Invalid middleware function input parameters.\nExpected the following parameters:\n (&self, _: HttpContext, _: &dyn MiddlewareChain)", 42 | )); 43 | } 44 | 45 | let mut input_args = m.inputs.iter(); 46 | 47 | match input_args.next().expect("len was checked above") { 48 | FnArg::Receiver(_) => {} 49 | arg => { 50 | return Err(Error::new_spanned(arg, "Invalid 1st parameter, expected `&self`")); 51 | } 52 | } 53 | 54 | let arg2 = input_args.next().expect("len was checked above"); 55 | let passed = match arg2 { 56 | FnArg::Typed(PatType { ty, .. }) => { 57 | if let Type::Path(TypePath { 58 | path: Path { segments, .. }, .. 59 | }) = ty.as_ref() 60 | { 61 | segments 62 | .first() 63 | .ok_or_else(|| Error::new_spanned(ty, "Unexpected type"))? 64 | .ident 65 | .to_string() 66 | .eq("Request") 67 | } else { 68 | false 69 | } 70 | } 71 | _ => false, 72 | }; 73 | 74 | if !passed { 75 | return Err(Error::new_spanned(arg2, "Invalid 2nd parameter, expected `Request`")); 76 | } 77 | 78 | let resp = if let ReturnType::Type(_, ret) = &m.output { 79 | if let Type::Path(TypePath { 80 | path: Path { segments, .. }, .. 81 | }) = ret.as_ref() 82 | { 83 | let r = segments.first().ok_or_else(|| Error::new_spanned(segments, "Unexpected type"))?; 84 | if r.ident.to_string().ne("Result") { 85 | return Err(Error::new_spanned( 86 | r, 87 | format!( 88 | "Invalid return type for the validate fn, expected Result, got {}", 89 | r.ident 90 | ), 91 | )); 92 | } 93 | if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = &r.arguments { 94 | if args.len() != 2 { 95 | return Err(Error::new_spanned( 96 | args, 97 | "Unexpected return type for the validate fn, expected 2 type argument inside Result", 98 | )); 99 | } 100 | let mut args = args.iter(); 101 | 102 | let out1 = args.next().expect("len checked above"); 103 | if let GenericArgument::Type(Type::Path(TypePath { 104 | path: Path { segments, .. }, .. 105 | })) = out1 106 | { 107 | let segment_name = segments 108 | .first() 109 | .ok_or_else(|| Error::new_spanned(segments, "Unexpected type"))? 110 | .ident 111 | .to_string(); 112 | if segment_name.ne("Request") { 113 | return Err(Error::new_spanned( 114 | segments, 115 | format!( 116 | "Invalid return type for the validate fn, expected Result, got Result<{}, ..>", 117 | segment_name 118 | ), 119 | )); 120 | } 121 | } 122 | let out2 = args.next().expect("len checked above"); 123 | if let GenericArgument::Type(Type::Path(TypePath { path, .. })) = out2 { 124 | path.clone() 125 | } else { 126 | return Err(Error::new_spanned( 127 | &m.output, 128 | "Unexpected return type for the validate fn, expected Result", 129 | )); 130 | } 131 | } else { 132 | return Err(Error::new_spanned( 133 | &m.output, 134 | "Unexpected return type for the validate fn, expected Result", 135 | )); 136 | } 137 | } else { 138 | return Err(Error::new_spanned( 139 | &m.output, 140 | "Unexpected return type for the validate fn, expected Result", 141 | )); 142 | } 143 | } else { 144 | return Err(Error::new_spanned( 145 | &m.output, 146 | "Invalid return type for the validate fn, expected Result, got ()", 147 | )); 148 | }; 149 | 150 | Ok(resp) 151 | } 152 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/generate/type_info.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::generate::{ 2 | crate_syn_browser::UseScope, 3 | utils::{find_macro_attribute_flag, find_macro_attribute_named_value}, 4 | }; 5 | use syn::{Expr, GenericArgument, Item as SynItem, Lit, Path, PathArguments, Type}; 6 | 7 | /// Informations about a Rust Type required to create a corresponding 8 | /// OpenApiType 9 | #[derive(Clone, Debug)] 10 | pub(crate) struct TypeInfo { 11 | pub(crate) name: String, 12 | /// fully-qualified type path; None for anonymous type 13 | pub(crate) type_path: Option, 14 | pub(crate) is_type_serializable: bool, 15 | pub(crate) is_type_deserializable: bool, 16 | pub(crate) is_array: bool, 17 | pub(crate) is_optional: bool, 18 | pub(crate) is_dictionary: bool, 19 | pub(crate) min_array_len: Option, 20 | pub(crate) max_array_len: Option, 21 | pub(crate) mime: Option, 22 | pub(crate) rename: Option, 23 | } 24 | 25 | impl TypeInfo { 26 | /// Retrieve TypeInfo for a syn::Type found in a crate_syn_browser::File. 27 | pub fn new<'b>(scope: &'b dyn UseScope<'b>, t: &Type) -> Option { 28 | match t { 29 | Type::Path(p) => { 30 | return TypeInfo::new_from_path(scope, &p.path); 31 | } 32 | Type::Array(a) => { 33 | let len: Option = match &a.len { 34 | Expr::Lit(l) => match &l.lit { 35 | Lit::Int(i) => i.base10_parse().ok(), 36 | _ => None, 37 | }, 38 | _ => None, 39 | }; 40 | 41 | if let Some(mut type_info) = TypeInfo::new(scope, a.elem.as_ref()) { 42 | type_info.is_array = true; 43 | type_info.min_array_len = len; 44 | type_info.max_array_len = len; 45 | return Some(type_info); 46 | } 47 | } 48 | Type::Reference(tr) => return TypeInfo::new(scope, tr.elem.as_ref()), 49 | _ => {} 50 | }; 51 | None 52 | } 53 | 54 | pub fn new_from_path<'b>(scope: &'b dyn UseScope<'b>, path: &Path) -> Option { 55 | if let Some(s) = path.segments.last() { 56 | let name = s.ident.to_string(); 57 | if name == "Vec" || name == "Option" || name == "HashMap" { 58 | let ag = match &s.arguments { 59 | PathArguments::AngleBracketed(ag) => ag, 60 | _ => { 61 | println!( 62 | "{} need angle bracket type parameter. Maybe another type was aliased as {}, which is not supported.", 63 | name, name 64 | ); 65 | return None; 66 | } 67 | }; 68 | let t2 = match ag.args.iter().rfind(|a| match a { 69 | GenericArgument::Type(_) => true, 70 | _ => false, 71 | }) { 72 | Some(GenericArgument::Type(t)) => t, 73 | _ => { 74 | println!("{} should be provided a type in angle-bracketed format. Faulty type : {:?}", name, path); 75 | return None; 76 | } 77 | }; 78 | 79 | if let Some(mut type_info) = TypeInfo::new(scope, t2) { 80 | match name.as_str() { 81 | "Vec" => type_info.is_array = true, 82 | "Option" => type_info.is_optional = true, 83 | "HashMap" => type_info.is_dictionary = true, 84 | _ => unreachable!(), 85 | } 86 | return Some(type_info); 87 | } 88 | } else { 89 | let path = path.segments.iter().map(|s| s.ident.to_string()).collect::>().join("::"); 90 | let type_impl = scope.find_type_definition(path.as_str()).ok().flatten(); 91 | let type_path = type_impl.map(|i| i.scope.path().to_string()); 92 | let item_attrs = type_impl.map(|i| match i.item { 93 | SynItem::Struct(s) => &s.attrs, 94 | SynItem::Enum(e) => &e.attrs, 95 | _ => unreachable!(), 96 | }); 97 | let is_primitive = match name.as_str() { 98 | "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "f32" | "f64" | "bool" | "Boolean" 99 | | "string" | "String" => true, 100 | _ => false, 101 | }; 102 | let is_type_serializable = is_primitive 103 | | item_attrs 104 | .map(|attrs| find_macro_attribute_flag(attrs, "derive", "Serialize")) 105 | .unwrap_or_default(); 106 | let is_type_deserializable = is_primitive 107 | | item_attrs 108 | .map(|attrs| find_macro_attribute_flag(attrs, "derive", "Deserialize")) 109 | .unwrap_or_default(); 110 | let rename = item_attrs 111 | .and_then(|attrs| find_macro_attribute_named_value(attrs, "openapi", "name")) 112 | .and_then(|m| match m { 113 | Lit::Str(s) => Some(s.value()), 114 | _ => None, 115 | }); 116 | let mime = item_attrs 117 | .and_then(|attrs| find_macro_attribute_named_value(attrs, "openapi", "mime")) 118 | .and_then(|m| match m { 119 | Lit::Str(s) => Some(s.value()), 120 | _ => None, 121 | }); 122 | return Some(TypeInfo { 123 | name, 124 | type_path, 125 | is_type_serializable, 126 | is_type_deserializable, 127 | is_array: false, 128 | is_optional: false, 129 | is_dictionary: false, 130 | min_array_len: None, 131 | max_array_len: None, 132 | mime, 133 | rename, 134 | }); 135 | } 136 | } 137 | None 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /saphir/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Saphir is a fully async-await http server framework for rust 2 | //! The goal is to give low-level control to your web stack (as hyper does) 3 | //! without the time consuming task of doing everything from scratch. 4 | //! 5 | //! Just `use` the prelude module, and you're ready to go! 6 | //! 7 | //! # Quick Overview 8 | //! 9 | //! Saphir provide multiple functionality through features. To try it out 10 | //! without fuss, we suggest that use all the features: 11 | //! 12 | //! ```toml 13 | //! saphir = { version = "2.0.0", features = ["full"] } 14 | //! ``` 15 | //! 16 | //! Then bootstrapping the server is as easy as: 17 | //! 18 | //! ```rust 19 | //! use saphir::prelude::*; 20 | //! 21 | //! struct TestController {} 22 | //! 23 | //! #[controller] 24 | //! impl TestController { 25 | //! #[get("/{var}/print")] 26 | //! async fn print_test(&self, var: String) -> (u16, String) { 27 | //! (200, var) 28 | //! } 29 | //! } 30 | //! 31 | //! async fn test_handler(mut req: Request) -> (u16, Option) { 32 | //! (200, req.captures_mut().remove("variable")) 33 | //! } 34 | //! 35 | //! #[tokio::main] 36 | //! async fn main() -> Result<(), SaphirError> { 37 | //! env_logger::init(); 38 | //! 39 | //! let server = Server::builder() 40 | //! .configure_listener(|l| { 41 | //! l.interface("127.0.0.1:3000") 42 | //! }) 43 | //! .configure_router(|r| { 44 | //! r.route("/{variable}/print", Method::GET, test_handler) 45 | //! .controller(TestController {}) 46 | //! }) 47 | //! .build(); 48 | //! 49 | //! // Start server with 50 | //! // server.run().await 51 | //! # Ok(()) 52 | //! } 53 | //! ``` 54 | //! # Saphir's Features 55 | //! 56 | //! Even though we strongly recommend that you use at least the `macro` feature, 57 | //! Saphir will work without any of the following feature, Saphir's features 58 | //! don't rely on each other to work. 59 | //! 60 | //! - `macro` : Enable the `#[controller]` macro attribute for code generation, 61 | //! Recommended and active by default 62 | //! - `https` : Provide everything to allow Saphir server to listen an accept 63 | //! HTTPS traffic 64 | //! - `json` : Add the `Json` wrapper type to simplify working with json data 65 | //! - `form` : Add the `Form` wrapper type to simplify working with urlencoded 66 | //! data 67 | //! - `validate-requests` : Enable the `#[controller]` macro to generate validation 68 | //! code for all `Json` and `Form`request payloads using the 69 | //! [`validator`](https://github.com/Keats/validator) crate. 70 | //! 71 | //! *_More feature will be added in the future_* 72 | #![allow(clippy::match_like_matches_macro)] 73 | #![cfg_attr(docsrs, feature(doc_cfg))] 74 | #![allow(clippy::empty_docs)] 75 | 76 | #[macro_use] 77 | extern crate log; 78 | extern crate core; 79 | 80 | /// 81 | pub mod body; 82 | /// 83 | pub mod controller; 84 | /// 85 | pub mod cookie; 86 | /// Error definitions 87 | pub mod error; 88 | /// 89 | pub mod extension; 90 | /// 91 | #[cfg(feature = "file")] 92 | #[cfg_attr(docsrs, doc(cfg(feature = "file")))] 93 | pub mod file; 94 | /// 95 | pub mod guard; 96 | /// Definition of types which can handle an http request 97 | pub mod handler; 98 | /// Context enveloping every request <-> response 99 | pub mod http_context; 100 | /// Saphir macro for code generation 101 | #[cfg(feature = "macro")] 102 | #[cfg_attr(docsrs, doc(cfg(feature = "macro")))] 103 | pub mod macros; 104 | /// 105 | pub mod middleware; 106 | /// The async Multipart Form-Data representation 107 | #[cfg(feature = "multipart")] 108 | #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))] 109 | pub mod multipart; 110 | /// 111 | #[cfg(feature = "redirect")] 112 | #[cfg_attr(docsrs, doc(cfg(feature = "redirect")))] 113 | pub mod redirect; 114 | /// The Http Request type 115 | pub mod request; 116 | /// Definition of type which can map to a response 117 | pub mod responder; 118 | /// The Http Response type 119 | pub mod response; 120 | /// 121 | pub mod router; 122 | /// Server implementation and default runtime 123 | pub mod server; 124 | /// 125 | pub mod utils; 126 | /// 127 | pub use http; 128 | #[doc(hidden)] 129 | pub use hyper; 130 | #[cfg(feature = "tracing-instrument")] 131 | #[doc(hidden)] 132 | pub use tracing; 133 | 134 | /// Contains everything you need to bootstrap your http server 135 | /// 136 | /// ```rust 137 | /// use saphir::prelude::*; 138 | /// 139 | /// // implement magic 140 | /// ``` 141 | pub mod prelude { 142 | /// 143 | pub use crate::body::Body; 144 | /// 145 | pub use crate::body::Bytes; 146 | /// 147 | #[cfg(feature = "form")] 148 | #[cfg_attr(docsrs, doc(cfg(feature = "form")))] 149 | pub use crate::body::Form; 150 | /// 151 | #[cfg(feature = "json")] 152 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 153 | pub use crate::body::Json; 154 | /// 155 | pub use crate::controller::Controller; 156 | /// 157 | pub use crate::controller::ControllerEndpoint; 158 | /// 159 | pub use crate::controller::EndpointsBuilder; 160 | /// 161 | pub use crate::cookie::Cookie; 162 | /// 163 | pub use crate::cookie::CookieBuilder; 164 | /// 165 | pub use crate::cookie::CookieJar; 166 | /// 167 | pub use crate::error::SaphirError; 168 | /// 169 | pub use crate::extension::Ext; 170 | /// 171 | pub use crate::extension::Extensions; 172 | /// 173 | #[cfg(feature = "file")] 174 | #[cfg_attr(docsrs, doc(cfg(feature = "file")))] 175 | pub use crate::file::File; 176 | /// 177 | pub use crate::guard::Guard; 178 | /// 179 | pub use crate::handler::Handler; 180 | #[cfg(feature = "operation")] 181 | #[cfg_attr(docsrs, doc(cfg(feature = "operation")))] 182 | pub use crate::http_context::operation::OperationId; 183 | /// 184 | pub use crate::http_context::HttpContext; 185 | /// 186 | #[cfg(feature = "macro")] 187 | #[cfg_attr(docsrs, doc(cfg(feature = "macro")))] 188 | pub use crate::macros::*; 189 | /// 190 | pub use crate::middleware::Middleware; 191 | /// 192 | pub use crate::middleware::MiddlewareChain; 193 | /// 194 | #[cfg(feature = "multipart")] 195 | #[cfg_attr(docsrs, doc(cfg(feature = "multipart")))] 196 | pub use crate::multipart::Multipart; 197 | /// 198 | #[cfg(feature = "redirect")] 199 | #[cfg_attr(docsrs, doc(cfg(feature = "redirect")))] 200 | pub use crate::redirect::Redirect; 201 | /// 202 | pub use crate::request::FromRequest; 203 | /// 204 | pub use crate::request::Request; 205 | /// 206 | pub use crate::responder::Responder; 207 | /// 208 | pub use crate::response::Builder; 209 | /// 210 | pub use crate::response::Response; 211 | /// 212 | pub use crate::server::Server; 213 | /// 214 | pub use crate::server::Stack; 215 | /// 216 | pub use http::header; 217 | /// 218 | pub use http::Method; 219 | /// 220 | pub use http::StatusCode; 221 | /// 222 | pub use http::Uri; 223 | /// 224 | pub use http::Version; 225 | } 226 | -------------------------------------------------------------------------------- /saphir_cli/src/openapi/generate/handler_info.rs: -------------------------------------------------------------------------------- 1 | use crate::openapi::{ 2 | generate::{crate_syn_browser::Method, response_info::ResponseInfo, route_info::RouteInfo, type_info::TypeInfo, BodyParamInfo, Gen, RouteParametersInfo}, 3 | schema::{OpenApiMimeType, OpenApiParameter, OpenApiParameterLocation, OpenApiSchema, OpenApiType}, 4 | }; 5 | use syn::{FnArg, GenericArgument, ImplItemMethod, Pat, PathArguments, Type}; 6 | 7 | #[derive(Clone, Debug, Default)] 8 | pub(crate) struct HandlerInfo { 9 | pub(crate) use_cookies: bool, 10 | pub(crate) parameters: Vec, 11 | pub(crate) body_info: Option, 12 | pub(crate) routes: Vec, 13 | pub(crate) responses: Vec, 14 | } 15 | 16 | impl Gen { 17 | pub(crate) fn extract_handler_info<'b>(&mut self, controller_path: &str, method: &'b Method<'b>) -> Option { 18 | let mut consume_cookies: bool = self.handler_has_cookies(method.syn); 19 | 20 | let routes = self.extract_routes_info_from_method_macro(method, controller_path); 21 | 22 | if routes.is_empty() { 23 | return None; 24 | } 25 | 26 | let parameters_info = self.parse_handler_parameters(method, &routes[0].uri_params); 27 | if parameters_info.has_cookies_param { 28 | consume_cookies = true; 29 | } 30 | 31 | let responses = self.extract_response_info(method); 32 | 33 | Some(HandlerInfo { 34 | use_cookies: consume_cookies, 35 | parameters: parameters_info.parameters.clone(), 36 | body_info: parameters_info.body_info, 37 | routes, 38 | responses, 39 | }) 40 | } 41 | 42 | fn handler_has_cookies(&self, m: &ImplItemMethod) -> bool { 43 | for attr in &m.attrs { 44 | if let Some(i) = attr.path.get_ident() { 45 | if i.to_string().as_str() == "cookies" { 46 | return true; 47 | } 48 | } 49 | } 50 | false 51 | } 52 | 53 | /// TODO: better typing for parameters. 54 | /// implement a ParameterInfo struct with typing for param, fill 55 | /// HandlerInfo with this, separate the discovery of BodyInfo and 56 | /// cookies usage from parameters. 57 | fn parse_handler_parameters<'b>(&self, method: &'b Method<'b>, uri_params: &[String]) -> RouteParametersInfo { 58 | let mut parameters = Vec::new(); 59 | let mut has_cookies_param = false; 60 | let mut body_type = None; 61 | for param in method.syn.sig.inputs.iter().filter_map(|i| match i { 62 | FnArg::Typed(p) => Some(p), 63 | _ => None, 64 | }) { 65 | let param_name = match param.pat.as_ref() { 66 | Pat::Ident(i) => i.ident.to_string(), 67 | _ => continue, 68 | }; 69 | 70 | let (param_type, optional) = match param.ty.as_ref() { 71 | Type::Path(p) => { 72 | if let Some(s1) = p.path.segments.last() { 73 | let mut param_type = s1.ident.to_string(); 74 | if param_type.as_str() == "Ext" { 75 | continue; 76 | } 77 | if param_type.as_str() == "CookieJar" { 78 | has_cookies_param = true; 79 | continue; 80 | } 81 | if param_type.as_str() == "Request" { 82 | if let PathArguments::AngleBracketed(ab) = &s1.arguments { 83 | if let Some(GenericArgument::Type(Type::Path(body_path))) = ab.args.first() { 84 | if let Some(seg) = body_path.path.segments.first() { 85 | body_type = Some(seg); 86 | } 87 | } 88 | } 89 | continue; 90 | } 91 | if param_type.as_str() == "Json" || param_type.as_str() == "Form" { 92 | body_type = Some(s1); 93 | continue; 94 | } 95 | let optional = param_type.as_str() == "Option"; 96 | if optional { 97 | param_type = "String".to_string(); 98 | if let PathArguments::AngleBracketed(ab) = &s1.arguments { 99 | if let Some(GenericArgument::Type(Type::Path(p))) = ab.args.first() { 100 | if let Some(i) = p.path.get_ident() { 101 | param_type = i.to_string(); 102 | } 103 | } 104 | } 105 | } 106 | 107 | let api_type = OpenApiType::from_rust_type_str(param_type.as_str()); 108 | (api_type.unwrap_or_else(OpenApiType::string), optional) 109 | } else { 110 | (OpenApiType::string(), false) 111 | } 112 | } 113 | _ => (OpenApiType::string(), false), 114 | }; 115 | 116 | let location = if uri_params.contains(¶m_name) { 117 | OpenApiParameterLocation::Path 118 | } else { 119 | OpenApiParameterLocation::Query 120 | }; 121 | parameters.push(OpenApiParameter { 122 | name: param_name, 123 | required: !optional, 124 | nullable: optional, 125 | location, 126 | schema: OpenApiSchema::Inline(param_type), 127 | ..Default::default() 128 | }) 129 | } 130 | 131 | let mut body_info: Option = None; 132 | if let Some(body) = body_type { 133 | let body_type = body.ident.to_string(); 134 | let openapi_type = match body_type.as_str() { 135 | "Json" => OpenApiMimeType::Json, 136 | "Form" => OpenApiMimeType::Form, 137 | _ => OpenApiMimeType::Any, 138 | }; 139 | match body_type.as_str() { 140 | "Json" | "Form" => { 141 | if let PathArguments::AngleBracketed(ag) = &body.arguments { 142 | if let Some(GenericArgument::Type(t)) = ag.args.first() { 143 | if let Some(type_info) = TypeInfo::new(method.impl_item.im.item.scope, t) { 144 | body_info = Some(BodyParamInfo { openapi_type, type_info }); 145 | } 146 | } 147 | } 148 | } 149 | _ => {} 150 | }; 151 | } 152 | 153 | RouteParametersInfo { 154 | parameters, 155 | has_cookies_param, 156 | body_info, 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /saphir/src/responder.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::let_and_return)] 2 | use crate::{http_context::HttpContext, response::Builder}; 3 | use http::StatusCode; 4 | 5 | macro_rules! impl_status_responder { 6 | ( $( $x:ty ),+ ) => { 7 | $( 8 | impl Responder for $x { 9 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder { 10 | builder.status(self as u16) 11 | } 12 | } 13 | )+ 14 | } 15 | } 16 | 17 | macro_rules! impl_body_responder { 18 | ( $( $x:ty ),+ ) => { 19 | $( 20 | impl Responder for $x { 21 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder { 22 | builder.body(self) 23 | } 24 | } 25 | )+ 26 | } 27 | } 28 | 29 | macro_rules! impl_plain_body_responder { 30 | ( $( $x:ty ),+ ) => { 31 | $( 32 | impl Responder for $x { 33 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder { 34 | builder.header(http::header::CONTENT_TYPE, "text/plain").body(self) 35 | } 36 | } 37 | )+ 38 | } 39 | } 40 | 41 | macro_rules! impl_tuple_responder { 42 | 43 | ( $($idx:tt -> $T:ident),+ ) => { 44 | 45 | impl<$($T:Responder),+> Responder for ($($T),+) { 46 | fn respond_with_builder(self, builder: Builder, ctx: &HttpContext) -> Builder { 47 | $(let builder = self.$idx.respond_with_builder(builder, ctx);)+ 48 | builder 49 | } 50 | } 51 | } 52 | } 53 | 54 | /// Responder defines what type can generate a response 55 | pub trait Responder { 56 | /// Consume self into a builder 57 | /// 58 | /// ```rust 59 | /// # use saphir::prelude::*; 60 | /// struct CustomResponder(String); 61 | /// 62 | /// impl Responder for CustomResponder { 63 | /// fn respond_with_builder(self, builder: Builder, ctx: &HttpContext) -> Builder { 64 | /// // Put the string as the response body 65 | /// builder.body(self.0) 66 | /// } 67 | /// } 68 | /// ``` 69 | fn respond_with_builder(self, builder: Builder, ctx: &HttpContext) -> Builder; 70 | } 71 | 72 | impl Responder for Vec 73 | where 74 | T: Responder, 75 | { 76 | fn respond_with_builder(self, mut builder: Builder, ctx: &HttpContext) -> Builder { 77 | for responder in self { 78 | builder = responder.respond_with_builder(builder, ctx); 79 | } 80 | builder 81 | } 82 | } 83 | 84 | impl Responder for &'static [T] 85 | where 86 | T: Responder + Clone, 87 | { 88 | fn respond_with_builder(self, mut builder: Builder, ctx: &HttpContext) -> Builder { 89 | for responder in self { 90 | builder = responder.clone().respond_with_builder(builder, ctx); 91 | } 92 | builder 93 | } 94 | } 95 | 96 | impl Responder for StatusCode { 97 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder { 98 | builder.status(self) 99 | } 100 | } 101 | 102 | impl Responder for () { 103 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder { 104 | builder.status(200) 105 | } 106 | } 107 | 108 | impl Responder for Option { 109 | fn respond_with_builder(self, builder: Builder, ctx: &HttpContext) -> Builder { 110 | if let Some(r) = self { 111 | r.respond_with_builder(builder, ctx).status_if_not_set(200) 112 | } else { 113 | builder.status_if_not_set(404) 114 | } 115 | } 116 | } 117 | 118 | impl Responder for Result { 119 | fn respond_with_builder(self, builder: Builder, ctx: &HttpContext) -> Builder { 120 | match self { 121 | Ok(r) => r.respond_with_builder(builder, ctx).status_if_not_set(200), 122 | Err(r) => r.respond_with_builder(builder, ctx).status_if_not_set(500), 123 | } 124 | } 125 | } 126 | 127 | impl Responder for hyper::Error { 128 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder { 129 | builder.status(500) 130 | } 131 | } 132 | 133 | impl Responder for Builder { 134 | fn respond_with_builder(self, _builder: Builder, _ctx: &HttpContext) -> Builder { 135 | self 136 | } 137 | } 138 | 139 | #[cfg(feature = "json")] 140 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 141 | mod json { 142 | use super::*; 143 | use crate::body::Json; 144 | use serde::Serialize; 145 | 146 | impl Responder for Json { 147 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder { 148 | builder 149 | .json(&self.0) 150 | .unwrap_or_else(|boxed| (*boxed).0.status(500).body("Unable to serialize json data")) 151 | } 152 | } 153 | } 154 | 155 | #[cfg(feature = "form")] 156 | #[cfg_attr(docsrs, doc(cfg(feature = "form")))] 157 | mod form { 158 | use super::*; 159 | use crate::body::Form; 160 | use serde::Serialize; 161 | 162 | impl Responder for Form { 163 | fn respond_with_builder(self, builder: Builder, _ctx: &HttpContext) -> Builder { 164 | builder 165 | .form(&self.0) 166 | .unwrap_or_else(|boxed| (*boxed).0.status(500).body("Unable to serialize form data")) 167 | } 168 | } 169 | } 170 | 171 | impl_status_responder!(u16, i16, u32, i32, u64, i64, usize, isize); 172 | impl_plain_body_responder!(String, &'static str); 173 | impl_body_responder!(Vec, &'static [u8], hyper::body::Bytes); 174 | impl_tuple_responder!(0->A, 1->B); 175 | impl_tuple_responder!(0->A, 1->B, 2->C); 176 | impl_tuple_responder!(0->A, 1->B, 2->C, 3->D); 177 | impl_tuple_responder!(0->A, 1->B, 2->C, 3->D, 4->E); 178 | impl_tuple_responder!(0->A, 1->B, 2->C, 3->D, 4->E, 5->F); 179 | 180 | /// Trait used by the server, not meant for manual implementation 181 | pub trait DynResponder { 182 | #[doc(hidden)] 183 | fn dyn_respond(&mut self, builder: Builder, ctx: &HttpContext) -> Builder; 184 | } 185 | 186 | impl DynResponder for Option 187 | where 188 | T: Responder, 189 | { 190 | fn dyn_respond(&mut self, builder: Builder, ctx: &HttpContext) -> Builder { 191 | self.take().ok_or(500).respond_with_builder(builder, ctx) 192 | } 193 | } 194 | 195 | #[cfg(feature = "tracing-instrument")] 196 | #[doc(hidden)] 197 | pub mod spanned { 198 | use super::*; 199 | 200 | #[derive(Debug)] 201 | pub struct SpannedResponder { 202 | pub responder: R, 203 | span: tracing::span::Span, 204 | } 205 | 206 | impl SpannedResponder { 207 | #[doc(hidden)] 208 | pub fn new(responder: R, span: tracing::span::Span) -> Self { 209 | Self { responder, span } 210 | } 211 | } 212 | 213 | impl Responder for SpannedResponder 214 | where 215 | R: Responder, 216 | { 217 | fn respond_with_builder(self, builder: Builder, ctx: &HttpContext) -> Builder { 218 | self.responder.respond_with_builder(builder, ctx).span(self.span) 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /saphir/src/controller.rs: -------------------------------------------------------------------------------- 1 | //! Controllers are responsible for handling requests and returning responses to 2 | //! the client. 3 | //! 4 | //! More specifically a Controller defines a list of endpoint (Handlers) that 5 | //! handle a request and return a Future of a 6 | //! [`Responder`](crate::responder::Responder). The Responder is responsible for 7 | //! the [`Response`](crate::response::Response) being generated. 8 | //! 9 | //! To create a controller, simply implement the 10 | //! [Controller](trait.Controller.html) trait on a struct: 11 | //! ```rust 12 | //! use saphir::prelude::*; 13 | //! 14 | //! struct BasicController; 15 | //! 16 | //! impl Controller for BasicController { 17 | //! const BASE_PATH: &'static str = "/basic"; 18 | //! 19 | //! fn handlers(&self) -> Vec> 20 | //! where 21 | //! Self: Sized { 22 | //! EndpointsBuilder::new() 23 | //! .add(Method::GET, "/healthz", BasicController::healthz) 24 | //! .build() 25 | //! } 26 | //! } 27 | //! 28 | //! impl BasicController { 29 | //! async fn healthz(&self, req: Request) -> impl Responder {200} 30 | //! } 31 | //! ``` 32 | 33 | use crate::{ 34 | body::Body, 35 | guard::{Builder as GuardBuilder, GuardChain, GuardChainEnd}, 36 | request::Request, 37 | responder::{DynResponder, Responder}, 38 | }; 39 | use futures::future::BoxFuture; 40 | use futures_util::future::{Future, FutureExt}; 41 | use http::Method; 42 | 43 | /// Type definition to represent a endpoint within a controller 44 | pub type ControllerEndpoint = ( 45 | Option<&'static str>, 46 | Method, 47 | &'static str, 48 | Box + Send + Sync>, 49 | Box, 50 | ); 51 | 52 | /// Trait that defines how a controller handles its requests 53 | pub trait Controller { 54 | /// Defines the base path from which requests are to be handled by this 55 | /// controller 56 | const BASE_PATH: &'static str; 57 | 58 | /// Returns a list of [`ControllerEndpoint`](type.ControllerEndpoint.html) 59 | /// 60 | /// Each [`ControllerEndpoint`](type.ControllerEndpoint.html) is then added 61 | /// to the router, which will dispatch requests accordingly 62 | fn handlers(&self) -> Vec> 63 | where 64 | Self: Sized; 65 | } 66 | 67 | /// Trait that defines a handler within a controller. 68 | /// This trait is not meant to be implemented manually as there is a blanket 69 | /// implementation for Async Fns 70 | pub trait ControllerHandler { 71 | /// An instance of a [`Responder`](../responder/trait.Responder.html) being 72 | /// returned by the handler 73 | type Responder: Responder; 74 | /// 75 | type Future: Future; 76 | 77 | /// Handle the request dispatched from the 78 | /// [`Router`](../router/struct.Router.html) 79 | fn handle(&self, controller: &'static C, req: Request) -> Self::Future; 80 | } 81 | 82 | /// 83 | pub trait DynControllerHandler { 84 | /// 85 | fn dyn_handle(&self, controller: &'static C, req: Request) -> BoxFuture<'static, Box>; 86 | } 87 | 88 | /// Builder to simplify returning a list of endpoints in the `handlers` method 89 | /// of the controller trait 90 | #[derive(Default)] 91 | pub struct EndpointsBuilder { 92 | handlers: Vec>, 93 | } 94 | 95 | impl EndpointsBuilder { 96 | /// Create a new endpoint builder 97 | #[inline] 98 | pub fn new() -> Self { 99 | Self { handlers: Default::default() } 100 | } 101 | 102 | /// Add a endpoint the the builder 103 | /// 104 | /// ```rust 105 | /// # use saphir::prelude::*; 106 | /// 107 | /// # struct BasicController; 108 | /// 109 | /// # impl Controller for BasicController { 110 | /// # const BASE_PATH: &'static str = "/basic"; 111 | /// # 112 | /// # fn handlers(&self) -> Vec> 113 | /// # where 114 | /// # Self: Sized { 115 | /// # EndpointsBuilder::new() 116 | /// # .add(Method::GET, "/healthz", BasicController::healthz) 117 | /// # .build() 118 | /// # } 119 | /// # } 120 | /// # 121 | /// impl BasicController { 122 | /// async fn healthz(&self, req: Request) -> impl Responder {200} 123 | /// } 124 | /// 125 | /// let b: EndpointsBuilder = EndpointsBuilder::new().add(Method::GET, "/healthz", BasicController::healthz); 126 | /// ``` 127 | #[inline] 128 | pub fn add(mut self, method: Method, route: &'static str, handler: H) -> Self 129 | where 130 | H: 'static + DynControllerHandler + Send + Sync, 131 | { 132 | self.handlers.push((None, method, route, Box::new(handler), GuardBuilder::default().build())); 133 | self 134 | } 135 | 136 | /// Add a guarded endpoint the the builder 137 | #[inline] 138 | pub fn add_with_guards(mut self, method: Method, route: &'static str, handler: H, guards: F) -> Self 139 | where 140 | H: 'static + DynControllerHandler + Send + Sync, 141 | F: FnOnce(GuardBuilder) -> GuardBuilder, 142 | Chain: GuardChain + 'static, 143 | { 144 | self.handlers 145 | .push((None, method, route, Box::new(handler), guards(GuardBuilder::default()).build())); 146 | self 147 | } 148 | 149 | /// Add but with a handler name 150 | #[inline] 151 | pub fn add_with_name(mut self, handler_name: &'static str, method: Method, route: &'static str, handler: H) -> Self 152 | where 153 | H: 'static + DynControllerHandler + Send + Sync, 154 | { 155 | self.handlers 156 | .push((Some(handler_name), method, route, Box::new(handler), GuardBuilder::default().build())); 157 | self 158 | } 159 | 160 | /// Add with guard but with a handler name 161 | #[inline] 162 | pub fn add_with_guards_and_name(mut self, handler_name: &'static str, method: Method, route: &'static str, handler: H, guards: F) -> Self 163 | where 164 | H: 'static + DynControllerHandler + Send + Sync, 165 | F: FnOnce(GuardBuilder) -> GuardBuilder, 166 | Chain: GuardChain + 'static, 167 | { 168 | self.handlers 169 | .push((Some(handler_name), method, route, Box::new(handler), guards(GuardBuilder::default()).build())); 170 | self 171 | } 172 | 173 | /// Finish the builder into a `Vec>` 174 | #[inline] 175 | pub fn build(self) -> Vec> { 176 | self.handlers 177 | } 178 | } 179 | 180 | impl ControllerHandler for Fun 181 | where 182 | C: 'static, 183 | Fun: Fn(&'static C, Request) -> Fut, 184 | Fut: 'static + Future + Send, 185 | R: Responder, 186 | { 187 | type Future = Box + Unpin + Send>; 188 | type Responder = R; 189 | 190 | #[inline] 191 | fn handle(&self, controller: &'static C, req: Request) -> Self::Future { 192 | Box::new(Box::pin((*self)(controller, req))) 193 | } 194 | } 195 | 196 | impl DynControllerHandler for H 197 | where 198 | R: 'static + Responder + Send, 199 | Fut: 'static + Future + Unpin + Send, 200 | H: ControllerHandler, 201 | { 202 | #[inline] 203 | fn dyn_handle(&self, controller: &'static C, req: Request) -> BoxFuture<'static, Box> { 204 | self.handle(controller, req).map(|r| Box::new(Some(r)) as Box).boxed() 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /saphir/src/middleware.rs: -------------------------------------------------------------------------------- 1 | //! A middleware is an object being called before the request is processed by 2 | //! the router, allowing to continue or stop the processing of a given request 3 | //! by calling / omitting next. 4 | //! 5 | //! ```text 6 | //! chain.next(_) chain.next(_) 7 | //! | | 8 | //! | | 9 | //! +---------+ | +---------+ | +---------+ 10 | //! | +--+->+ +--+->+ | 11 | //! | Middle | | Middle | | Router | 12 | //! | ware1 | | ware2 | | | 13 | //! | +<----+ +<----+ | 14 | //! +---------+ +---------+ +---------+ 15 | //! ``` 16 | //! 17 | //! Once the request is fully processed by the stack or whenever a middleware 18 | //! returns an error, the request is terminated and the response is generated, 19 | //! the response then becomes available to the middleware 20 | //! 21 | //! A middleware is defined as the following: 22 | //! 23 | //! ```rust 24 | //! # use saphir::prelude::*; 25 | //! # struct CustomData; 26 | //! # 27 | //! async fn example_middleware(data: &CustomData, ctx: HttpContext, chain: &dyn MiddlewareChain) -> Result { 28 | //! // Do work before the request is handled by the router 29 | //! 30 | //! let ctx = chain.next(ctx).await?; 31 | //! 32 | //! // Do work with the response 33 | //! 34 | //! Ok(ctx) 35 | //! } 36 | //! ``` 37 | //! 38 | //! *SAFETY NOTICE* 39 | //! 40 | //! Inside the middleware chain we need a little bit of unsafe code. This code 41 | //! allow us to consider the futures generated by the middlewares as 'static. 42 | //! This is considered safe since all middleware data lives within the server 43 | //! stack which has a static lifetime over your application. We plan to remove 44 | //! this unsafe code as soon as we find another solution to it. 45 | 46 | use crate::{ 47 | error::{InternalError, SaphirError}, 48 | http_context::HttpContext, 49 | utils::UriPathMatcher, 50 | }; 51 | use futures::{future::BoxFuture, FutureExt}; 52 | use futures_util::future::Future; 53 | 54 | pub trait Middleware { 55 | fn next(&'static self, ctx: HttpContext, chain: &'static dyn MiddlewareChain) -> BoxFuture<'static, Result>; 56 | } 57 | 58 | impl Middleware for Fun 59 | where 60 | Fun: Fn(HttpContext, &'static dyn MiddlewareChain) -> Fut, 61 | Fut: 'static + Future> + Send, 62 | { 63 | #[inline] 64 | fn next(&'static self, ctx: HttpContext, chain: &'static dyn MiddlewareChain) -> BoxFuture<'static, Result> { 65 | (*self)(ctx, chain).boxed() 66 | } 67 | } 68 | 69 | /// Builder to apply middleware onto the http stack 70 | pub struct Builder { 71 | chain: Chain, 72 | } 73 | 74 | impl Default for Builder { 75 | fn default() -> Self { 76 | Self { chain: MiddleChainEnd } 77 | } 78 | } 79 | 80 | impl Builder { 81 | /// Method to apply a new middleware onto the stack where the `include_path` 82 | /// vec are all path affected by the middleware, and `exclude_path` are 83 | /// exclusion amongst the included paths. 84 | /// 85 | /// ```rust 86 | /// use saphir::middleware::Builder as MBuilder; 87 | /// # use saphir::prelude::*; 88 | /// 89 | /// # async fn log_middleware( 90 | /// # ctx: HttpContext, 91 | /// # chain: &dyn MiddlewareChain, 92 | /// # ) -> Result { 93 | /// # println!("new request on path: {}", ctx.state.request_unchecked().uri().path()); 94 | /// # let ctx = chain.next(ctx).await?; 95 | /// # println!("new response with status: {}", ctx.state.response_unchecked().status()); 96 | /// # Ok(ctx) 97 | /// # } 98 | /// # 99 | /// let builder = MBuilder::default().apply(log_middleware, vec!["/"], None); 100 | /// ``` 101 | pub fn apply<'a, Mid, E>(self, mid: Mid, include_path: Vec<&str>, exclude_path: E) -> Builder> 102 | where 103 | Mid: 'static + Middleware + Sync + Send, 104 | E: Into>>, 105 | { 106 | let rule = Rule::new(include_path, exclude_path.into()); 107 | Builder { 108 | chain: MiddlewareChainLink { rule, mid, rest: self.chain }, 109 | } 110 | } 111 | 112 | pub(crate) fn build(self) -> Box { 113 | Box::new(self.chain) 114 | } 115 | } 116 | 117 | pub(crate) struct Rule { 118 | included_path: Vec, 119 | excluded_path: Option>, 120 | } 121 | 122 | impl Rule { 123 | #[doc(hidden)] 124 | pub fn new(include_path: Vec<&str>, exclude_path: Option>) -> Self { 125 | Rule { 126 | included_path: include_path 127 | .iter() 128 | .filter_map(|p| { 129 | UriPathMatcher::new(p) 130 | .map_err(|e| error!("Unable to construct included middleware route: {}", e)) 131 | .ok() 132 | }) 133 | .collect(), 134 | excluded_path: exclude_path.map(|ex| { 135 | ex.iter() 136 | .filter_map(|p| { 137 | UriPathMatcher::new(p) 138 | .map_err(|e| error!("Unable to construct excluded middleware route: {}", e)) 139 | .ok() 140 | }) 141 | .collect() 142 | }), 143 | } 144 | } 145 | 146 | #[doc(hidden)] 147 | pub fn validate_path(&self, path: &str) -> bool { 148 | if self.included_path.iter().any(|m_p| m_p.match_non_exhaustive(path)) { 149 | if let Some(ref excluded_path) = self.excluded_path { 150 | return !excluded_path.iter().any(|m_e_p| m_e_p.match_non_exhaustive(path)); 151 | } else { 152 | return true; 153 | } 154 | } 155 | 156 | false 157 | } 158 | } 159 | 160 | #[doc(hidden)] 161 | pub trait MiddlewareChain: Sync + Send { 162 | fn next(&self, ctx: HttpContext) -> BoxFuture<'static, Result>; 163 | } 164 | 165 | #[doc(hidden)] 166 | pub struct MiddleChainEnd; 167 | 168 | impl MiddlewareChain for MiddleChainEnd { 169 | #[doc(hidden)] 170 | #[allow(unused_mut)] 171 | #[inline] 172 | fn next(&self, mut ctx: HttpContext) -> BoxFuture<'static, Result> { 173 | async { 174 | let router = ctx.router.take().ok_or(SaphirError::Internal(InternalError::Stack))?; 175 | router.dispatch(ctx).await 176 | } 177 | .boxed() 178 | } 179 | } 180 | 181 | #[doc(hidden)] 182 | pub struct MiddlewareChainLink { 183 | rule: Rule, 184 | mid: Mid, 185 | rest: Rest, 186 | } 187 | 188 | #[doc(hidden)] 189 | impl MiddlewareChain for MiddlewareChainLink 190 | where 191 | Mid: Middleware + Sync + Send + 'static, 192 | Rest: MiddlewareChain, 193 | { 194 | #[doc(hidden)] 195 | #[allow(clippy::transmute_ptr_to_ptr)] 196 | #[inline] 197 | fn next(&self, ctx: HttpContext) -> BoxFuture<'static, Result> { 198 | // # SAFETY # 199 | // The middleware chain and data are initialized in static memory when calling 200 | // run on Server. 201 | let (mid, rest) = unsafe { 202 | ( 203 | std::mem::transmute::<&'_ Mid, &'static Mid>(&self.mid), 204 | std::mem::transmute::<&'_ dyn MiddlewareChain, &'static dyn MiddlewareChain>(&self.rest), 205 | ) 206 | }; 207 | 208 | if ctx.state.request().filter(|req| self.rule.validate_path(req.uri().path())).is_some() { 209 | mid.next(ctx, rest) 210 | } else { 211 | rest.next(ctx) 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /saphir/src/file/range_requests.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Weihang Lo 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use crate::{ 10 | file::{ 11 | content_range::ContentRange, 12 | etag::{EntityTag, SystemTimeExt}, 13 | range::Range, 14 | }, 15 | request::Request, 16 | }; 17 | use std::time::SystemTime; 18 | use time::{format_description::well_known::Rfc2822, OffsetDateTime, UtcOffset}; 19 | 20 | /// Check if given value from `If-Range` header field is fresh. 21 | /// 22 | /// According to RFC7232, to validate `If-Range` header, the implementation 23 | /// must use a strong comparison. 24 | pub fn is_range_fresh(req: &Request, etag: &EntityTag, last_modified: &SystemTime) -> bool { 25 | // Ignore `If-Range` if `Range` header is not present. 26 | if !req.headers().contains_key(http::header::RANGE) { 27 | return false; 28 | } 29 | if let Some(if_range) = req.headers().get(http::header::IF_RANGE).and_then(|header| header.to_str().ok()) { 30 | if if_range.starts_with('"') || if_range.starts_with("W/\"") { 31 | return etag.strong_eq(EntityTag::parse(if_range)); 32 | } 33 | 34 | if let Ok(date) = OffsetDateTime::parse(if_range, &Rfc2822).map(|date| date.to_offset(UtcOffset::UTC)) { 35 | return last_modified.timestamp() == date.unix_timestamp() as u64; 36 | } 37 | } 38 | // Always be fresh if there is no validators 39 | true 40 | } 41 | 42 | /// Convert `Range` header field in incoming request to `Content-Range` header 43 | /// field for response. 44 | /// 45 | /// Here are all situations mapped to returning `Option`: 46 | /// 47 | /// - None byte-range -> None 48 | /// - One satisfiable byte-range -> Some 49 | /// - One not satisfiable byte-range -> None 50 | /// - Two or more byte-ranges -> None 51 | /// 52 | /// Note that invalid and multiple byte-range are treaded as an unsatisfiable 53 | /// range. 54 | pub fn is_satisfiable_range(range: &Range, instance_length: u64) -> Option { 55 | match *range { 56 | // Try to extract byte range specs from range-unit. 57 | Range::Bytes(ref byte_range_specs) => Some(byte_range_specs), 58 | _ => None, 59 | } 60 | .and_then(|specs| if specs.len() == 1 { Some(specs[0].to_owned()) } else { None }) 61 | .and_then(|spec| spec.to_satisfiable_range(instance_length)) 62 | .map(|range| ContentRange::Bytes { 63 | range: Some(range), 64 | instance_length: Some(instance_length), 65 | }) 66 | } 67 | 68 | /// Extract range from `ContentRange` header field. 69 | pub fn extract_range(content_range: &ContentRange) -> Option<(u64, u64)> { 70 | match *content_range { 71 | ContentRange::Bytes { range, .. } => range, 72 | _ => None, 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod t_range { 78 | use super::*; 79 | use crate::{ 80 | file::{conditional_request::format_systemtime, etag::EntityTag}, 81 | prelude::Body, 82 | }; 83 | use http::{request::Builder, Method}; 84 | use std::time::{Duration, SystemTime}; 85 | 86 | #[test] 87 | fn no_range_header() { 88 | // Ignore range freshness validation. Return ture. 89 | let req = init_request(); 90 | let last_modified = SystemTime::now(); 91 | let etag = EntityTag::Strong("".to_owned()); 92 | 93 | assert!(!is_range_fresh( 94 | &Request::new(req.header(http::header::IF_RANGE, etag.get_tag()).body(Body::empty()).unwrap(), None), 95 | &etag, 96 | &last_modified 97 | )); 98 | } 99 | 100 | #[test] 101 | fn no_if_range_header() { 102 | // Ignore if-range freshness validation. Return true. 103 | let req = init_request(); 104 | let range = Range::Bytes(vec![]); 105 | let last_modified = SystemTime::now(); 106 | let etag = EntityTag::Strong("".to_owned()); 107 | // Always be fresh if there is no validators 108 | assert!(is_range_fresh( 109 | &Request::new(req.header(http::header::RANGE, range.to_string()).body(Body::empty()).unwrap(), None), 110 | &etag, 111 | &last_modified 112 | )); 113 | } 114 | 115 | #[test] 116 | fn weak_validator_as_falsy() { 117 | let req = init_request(); 118 | let range = Range::Bytes(vec![]); 119 | 120 | let last_modified = SystemTime::now(); 121 | let etag = EntityTag::Weak("im_weak".to_owned()); 122 | assert!(!is_range_fresh( 123 | &Request::new( 124 | req.header(http::header::IF_RANGE, etag.get_tag()) 125 | .header(http::header::RANGE, range.to_string()) 126 | .body(Body::empty()) 127 | .unwrap(), 128 | None 129 | ), 130 | &etag, 131 | &last_modified 132 | )); 133 | } 134 | 135 | #[test] 136 | fn only_accept_exact_match_mtime() { 137 | let mut req = init_request(); 138 | let etag = EntityTag::Strong("".to_owned()); 139 | let date = SystemTime::now(); 140 | 141 | req = req.header(http::header::RANGE, Range::Bytes(vec![]).to_string()); 142 | 143 | // Same date. 144 | assert!(is_range_fresh( 145 | &Request::new(req.header(http::header::IF_RANGE, format_systemtime(date)).body(Body::empty()).unwrap(), None), 146 | &etag, 147 | &date 148 | )); 149 | 150 | req = init_request(); 151 | req = req.header(http::header::RANGE, Range::Bytes(vec![]).to_string()); 152 | 153 | // Before 10 sec. 154 | let past = date - Duration::from_secs(10); 155 | assert!(!is_range_fresh( 156 | &Request::new(req.header(http::header::IF_RANGE, format_systemtime(past)).body(Body::empty()).unwrap(), None), 157 | &etag, 158 | &date 159 | )); 160 | 161 | req = init_request(); 162 | req = req.header(http::header::RANGE, Range::Bytes(vec![]).to_string()); 163 | 164 | // After 10 sec. 165 | let future = date + Duration::from_secs(10); 166 | assert!(!is_range_fresh( 167 | &Request::new(req.header(http::header::IF_RANGE, format_systemtime(future)).body(Body::empty()).unwrap(), None), 168 | &etag, 169 | &date 170 | )); 171 | } 172 | 173 | #[test] 174 | fn strong_validator() { 175 | let mut req = init_request(); 176 | req = req.header(http::header::RANGE, Range::Bytes(vec![]).to_string()); 177 | 178 | let last_modified = SystemTime::now(); 179 | let etag = EntityTag::Strong("im_strong".to_owned()); 180 | req = req.header(http::header::IF_RANGE, etag.get_tag()); 181 | let req = Request::new(req.body(Body::empty()).unwrap(), None); 182 | assert!(is_range_fresh(&req, &etag, &last_modified)); 183 | } 184 | 185 | fn init_request() -> Builder { 186 | Builder::new().method(Method::GET) 187 | } 188 | } 189 | 190 | #[cfg(test)] 191 | mod t_satisfiable { 192 | use super::*; 193 | use crate::file::range::ByteRangeSpec; 194 | 195 | pub fn bytes(from: u64, to: u64) -> Range { 196 | Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)]) 197 | } 198 | 199 | /// Get byte range header with multiple subranges 200 | /// ("bytes=from1-to1,from2-to2,fromX-toX") 201 | pub fn bytes_multi(ranges: Vec<(u64, u64)>) -> Range { 202 | Range::Bytes(ranges.iter().map(|r| ByteRangeSpec::FromTo(r.0, r.1)).collect()) 203 | } 204 | 205 | #[test] 206 | fn zero_byte_range() { 207 | let range = &Range::Unregistered("".to_owned(), "".to_owned()); 208 | assert!(is_satisfiable_range(range, 10).is_none()); 209 | } 210 | 211 | #[test] 212 | fn one_satisfiable_byte_range() { 213 | let range = &bytes(0, 10); 214 | assert!(is_satisfiable_range(range, 10).is_some()); 215 | } 216 | 217 | #[test] 218 | fn one_unsatisfiable_byte_range() { 219 | let range = &bytes(20, 10); 220 | assert!(is_satisfiable_range(range, 10).is_none()); 221 | } 222 | 223 | #[test] 224 | fn multiple_byte_ranges() { 225 | let range = &bytes_multi(vec![(0, 5), (5, 6)]); 226 | assert!(is_satisfiable_range(range, 10).is_none()); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /saphir/examples/basic.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | use futures::{ 5 | future::{BoxFuture, Ready}, 6 | FutureExt, 7 | }; 8 | use saphir::prelude::*; 9 | use serde_derive::{Deserialize, Serialize}; 10 | #[cfg(feature = "operation")] 11 | use tokio::sync::RwLock; 12 | 13 | // == controller == // 14 | 15 | struct MagicController { 16 | label: String, 17 | } 18 | 19 | impl MagicController { 20 | pub fn new>(label: S) -> Self { 21 | Self { label: label.into() } 22 | } 23 | } 24 | 25 | impl Controller for MagicController { 26 | const BASE_PATH: &'static str = "/magic"; 27 | 28 | fn handlers(&self) -> Vec> 29 | where 30 | Self: Sized, 31 | { 32 | let b = EndpointsBuilder::new(); 33 | 34 | #[cfg(feature = "json")] 35 | let b = b.add(Method::POST, "/json", MagicController::user_json); 36 | #[cfg(feature = "json")] 37 | let b = b.add(Method::GET, "/json", MagicController::get_user_json); 38 | 39 | #[cfg(feature = "form")] 40 | let b = b.add(Method::POST, "/form", MagicController::user_form); 41 | #[cfg(feature = "form")] 42 | let b = b.add(Method::GET, "/form", MagicController::get_user_form); 43 | 44 | b.add(Method::GET, "/delay/{delay}", MagicController::magic_delay) 45 | .add_with_guards(Method::GET, "/guarded/{delay}", MagicController::magic_delay, |g| g.apply(numeric_delay_guard)) 46 | .add(Method::GET, "/", magic_handler) 47 | .add(Method::POST, "/", MagicController::read_body) 48 | .add(Method::GET, "/match/*/**", MagicController::match_any_route) 49 | .build() 50 | } 51 | } 52 | 53 | impl MagicController { 54 | async fn magic_delay(&self, req: Request) -> (u16, String) { 55 | if let Some(delay) = req.captures().get("delay").and_then(|t| t.parse::().ok()) { 56 | tokio::time::sleep(tokio::time::Duration::from_secs(delay)).await; 57 | (200, format!("Delayed of {} secs: {}", delay, self.label)) 58 | } else { 59 | (400, "Invalid timeout".to_owned()) 60 | } 61 | } 62 | 63 | async fn read_body(&self, mut req: Request) -> (u16, String) { 64 | let body = req.body_mut().take_as::().await.unwrap(); 65 | (200, body) 66 | } 67 | 68 | async fn match_any_route(&self, req: Request) -> (u16, String) { 69 | (200, req.uri().path().to_string()) 70 | } 71 | 72 | #[cfg(feature = "json")] 73 | async fn user_json(&self, mut req: Request) -> (u16, String) { 74 | if let Ok(user) = req.body_mut().take_as::>().await { 75 | (200, format!("New user with username: {} and age: {} read from JSON", user.username, user.age)) 76 | } else { 77 | (400, "Bad user format".to_string()) 78 | } 79 | } 80 | 81 | #[cfg(feature = "form")] 82 | async fn user_form(&self, mut req: Request) -> (u16, String) { 83 | if let Ok(user) = req.body_mut().take_as::>().await { 84 | ( 85 | 200, 86 | format!("New user with username: {} and age: {} read from Form data", user.username, user.age), 87 | ) 88 | } else { 89 | (400, "Bad user format".to_string()) 90 | } 91 | } 92 | 93 | #[cfg(feature = "json")] 94 | async fn get_user_json(&self, _req: Request) -> (u16, Json) { 95 | ( 96 | 200, 97 | Json(User { 98 | username: "john.doe@example.net".to_string(), 99 | age: 42, 100 | }), 101 | ) 102 | } 103 | 104 | #[cfg(feature = "form")] 105 | async fn get_user_form(&self, _req: Request) -> (u16, Form) { 106 | ( 107 | 200, 108 | Form(User { 109 | username: "john.doe@example.net".to_string(), 110 | age: 42, 111 | }), 112 | ) 113 | } 114 | } 115 | 116 | fn magic_handler(controller: &MagicController, _: Request) -> Ready<(u16, String)> { 117 | futures::future::ready((200, controller.label.clone())) 118 | } 119 | 120 | #[derive(Serialize, Deserialize)] 121 | struct User { 122 | username: String, 123 | age: i64, 124 | } 125 | 126 | // == middleware == // 127 | 128 | struct StatsData { 129 | #[cfg(feature = "operation")] 130 | entered: RwLock, 131 | #[cfg(feature = "operation")] 132 | exited: RwLock, 133 | } 134 | 135 | impl StatsData { 136 | fn new() -> Self { 137 | Self { 138 | #[cfg(feature = "operation")] 139 | entered: RwLock::new(0), 140 | #[cfg(feature = "operation")] 141 | exited: RwLock::new(0), 142 | } 143 | } 144 | 145 | async fn stats(&self, ctx: HttpContext, chain: &dyn MiddlewareChain) -> Result { 146 | #[cfg(feature = "operation")] 147 | { 148 | let mut entered = self.entered.write().await; 149 | let exited = self.exited.read().await; 150 | *entered += 1; 151 | info!( 152 | "[Operation: {}] entered stats middleware! Current data: entered={} ; exited={}", 153 | &ctx.operation_id, *entered, *exited 154 | ); 155 | } 156 | 157 | let ctx = chain.next(ctx).await?; 158 | 159 | #[cfg(feature = "operation")] 160 | { 161 | let mut exited = self.exited.write().await; 162 | let entered = self.entered.read().await; 163 | *exited += 1; 164 | info!( 165 | "[Operation: {}] exited stats middleware! Current data: entered={} ; exited={}", 166 | &ctx.operation_id, *entered, *exited 167 | ); 168 | } 169 | 170 | Ok(ctx) 171 | } 172 | } 173 | 174 | impl Middleware for StatsData { 175 | fn next(&'static self, ctx: HttpContext, chain: &'static dyn MiddlewareChain) -> BoxFuture<'static, Result> { 176 | self.stats(ctx, chain).boxed() 177 | } 178 | } 179 | 180 | async fn log_middleware(ctx: HttpContext, chain: &'static dyn MiddlewareChain) -> Result { 181 | info!("new request on path: {}", ctx.state.request_unchecked().uri().path()); 182 | let ctx = chain.next(ctx).await?; 183 | info!("new response with status: {}", ctx.state.response_unchecked().status()); 184 | Ok(ctx) 185 | } 186 | 187 | // == handlers with no controller == // 188 | 189 | async fn test_handler(mut req: Request) -> (u16, Option) { 190 | (200, req.captures_mut().remove("variable")) 191 | } 192 | 193 | async fn hello_world(_: Request) -> (u16, &'static str) { 194 | (200, "Hello, World!") 195 | } 196 | 197 | // == guards == // 198 | 199 | struct ForbidderData { 200 | forbidden: &'static str, 201 | } 202 | 203 | impl ForbidderData { 204 | fn filter_forbidden<'a>(&self, v: &'a str) -> Option<&'a str> { 205 | if v == self.forbidden { 206 | Some(v) 207 | } else { 208 | None 209 | } 210 | } 211 | 212 | async fn forbidden_guard(&self, req: Request) -> Result, u16> { 213 | if req.captures().get("variable").and_then(|v| self.filter_forbidden(v)).is_some() { 214 | Err(403) 215 | } else { 216 | Ok(req) 217 | } 218 | } 219 | } 220 | 221 | impl Guard for ForbidderData { 222 | type Future = BoxFuture<'static, Result>; 223 | type Responder = u16; 224 | 225 | fn validate(&'static self, req: Request>) -> Self::Future { 226 | self.forbidden_guard(req).boxed() 227 | } 228 | } 229 | 230 | async fn numeric_delay_guard(req: Request) -> Result, &'static str> { 231 | if req.captures().get("delay").and_then(|v| v.parse::().ok()).is_some() { 232 | Ok(req) 233 | } else { 234 | Err("Guard blocked request: delay is not a valid number.") 235 | } 236 | } 237 | 238 | #[tokio::main] 239 | async fn main() -> Result<(), SaphirError> { 240 | env_logger::init(); 241 | 242 | let server = Server::builder() 243 | .configure_listener(|l| l.interface("127.0.0.1:3000").server_name("BasicExample")) 244 | .configure_router(|r| { 245 | r.route("/", Method::GET, hello_world) 246 | .route("/{variable}/print", Method::GET, test_handler) 247 | .route_with_guards("/{variable}/guarded_print", Method::GET, test_handler, |g| { 248 | g.apply(ForbidderData { forbidden: "forbidden" }).apply(ForbidderData { forbidden: "password" }) 249 | }) 250 | .controller(MagicController::new("Just Like Magic!")) 251 | }) 252 | .configure_middlewares(|m| m.apply(log_middleware, vec!["/**/*.html"], None).apply(StatsData::new(), vec!["/"], None)) 253 | .build(); 254 | 255 | server.run().await 256 | } 257 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /saphir/src/file/cache.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::SaphirError, 3 | file::{compress_file, Compression, Encoder, File, FileCursor, FileInfo, FileStream, SaphirFile, MAX_BUFFER}, 4 | }; 5 | use futures::{ 6 | io::{AsyncRead, AsyncSeek, Cursor}, 7 | AsyncReadExt, AsyncSeekExt, Future, 8 | }; 9 | use mime::Mime; 10 | use std::{ 11 | collections::HashMap, 12 | io, 13 | io::SeekFrom, 14 | path::{Path, PathBuf}, 15 | pin::Pin, 16 | sync::Arc, 17 | task::{Context, Poll}, 18 | }; 19 | use tokio::sync::RwLock; 20 | 21 | #[derive(Default)] 22 | struct CacheInner { 23 | pub cache: HashMap<(String, Compression), Vec>, 24 | pub size: u64, 25 | } 26 | 27 | #[derive(Clone)] 28 | pub struct FileCache { 29 | inner: Arc>, 30 | max_file_size: u64, 31 | max_capacity: u64, 32 | } 33 | 34 | impl FileCache { 35 | pub fn new(max_file_size: u64, max_capacity: u64) -> Self { 36 | FileCache { 37 | inner: Arc::new(RwLock::new(Default::default())), 38 | max_file_size, 39 | max_capacity, 40 | } 41 | } 42 | 43 | pub async fn get(&self, key: (String, Compression)) -> Option { 44 | if let Some(file) = self.inner.read().await.cache.get(&key) { 45 | let path = PathBuf::from(&key.0); 46 | Some(CachedFile { 47 | key, 48 | inner: self.inner.clone(), 49 | path, 50 | mime: None, 51 | position: 0, 52 | get_file_future: None, 53 | size: file.len() as u64, 54 | }) 55 | } else { 56 | None 57 | } 58 | } 59 | 60 | pub async fn insert(&mut self, key: (String, Compression), value: Vec) { 61 | let mut inner = self.inner.write().await; 62 | inner.size += value.len() as u64; 63 | inner.cache.insert(key, value); 64 | } 65 | 66 | pub async fn get_size(&self) -> u64 { 67 | self.inner.read().await.size 68 | } 69 | 70 | pub async fn open_file(&mut self, path: &Path, compression: Compression) -> Result { 71 | let path_str = path.to_str().unwrap_or_default(); 72 | if let Some(cached_file) = self.get((path_str.to_string(), compression)).await { 73 | Ok(FileStream::new(cached_file)) 74 | } else { 75 | let file: Pin> = match self.get((path_str.to_string(), Compression::Raw)).await { 76 | Some(file) => Box::pin(file), 77 | None => Box::pin(File::open(path_str).await?), 78 | }; 79 | let file_size = file.get_size(); 80 | let mime = file.get_mime().cloned(); 81 | let compressed_file = compress_file(file, Encoder::None, compression).await?; 82 | if file_size + self.get_size().await <= self.max_capacity && file_size <= self.max_file_size { 83 | Ok(FileStream::new(FileCacher::new( 84 | (path_str.to_string(), compression), 85 | Box::pin(FileCursor::new(compressed_file, mime, path.to_owned())) as Pin>, 86 | self.clone(), 87 | ))) 88 | } else { 89 | Ok(FileStream::new(FileCursor::new(compressed_file, mime, path.to_owned()))) 90 | } 91 | } 92 | } 93 | 94 | pub async fn open_file_with_range(&mut self, path: &Path, range: (u64, u64)) -> Result { 95 | let path_str = path.to_str().unwrap_or_default(); 96 | if let Some(cached_file) = self.get((path_str.to_string(), Compression::Raw)).await { 97 | let mut file_stream = FileStream::new(cached_file); 98 | file_stream.set_range(range).await?; 99 | Ok(file_stream) 100 | } else { 101 | let mut file_stream = FileStream::new(File::open(path_str).await?); 102 | file_stream.set_range(range).await?; 103 | Ok(file_stream) 104 | } 105 | } 106 | } 107 | 108 | type ReadFileFuture = Pin>> + Send + Sync>>; 109 | 110 | pub struct CachedFile { 111 | key: (String, Compression), 112 | inner: Arc>, 113 | path: PathBuf, 114 | mime: Option, 115 | position: usize, 116 | get_file_future: Option, 117 | size: u64, 118 | } 119 | 120 | impl CachedFile { 121 | async fn read_async(key: (String, Compression), inner: Arc>, position: usize, len: usize) -> io::Result> { 122 | match inner.read().await.cache.get(&key) { 123 | Some(bytes) => { 124 | let mut vec = vec![0; len]; 125 | let mut cursor = Cursor::new(bytes); 126 | cursor.seek(SeekFrom::Start(position as u64)).await?; 127 | match cursor.read(vec.as_mut_slice()).await { 128 | Ok(size) => Ok(vec[..size].to_vec()), 129 | Err(e) => Err(e), 130 | } 131 | } 132 | 133 | None => Err(io::Error::from(io::ErrorKind::BrokenPipe)), 134 | } 135 | } 136 | } 137 | 138 | impl AsyncSeek for CachedFile { 139 | fn poll_seek(mut self: Pin<&mut Self>, _cx: &mut Context<'_>, position: SeekFrom) -> Poll> { 140 | match position { 141 | SeekFrom::Start(i) => { 142 | if i < self.size { 143 | self.position = i as usize; 144 | Poll::Ready(Ok(i)) 145 | } else { 146 | Poll::Ready(Err(io::Error::from(io::ErrorKind::InvalidInput))) 147 | } 148 | } 149 | 150 | SeekFrom::Current(i) => { 151 | if (i + self.position as i64) >= 0 { 152 | self.position += i as usize; 153 | Poll::Ready(Ok(self.position as u64)) 154 | } else { 155 | Poll::Ready(Err(io::Error::from(io::ErrorKind::InvalidInput))) 156 | } 157 | } 158 | 159 | SeekFrom::End(i) => { 160 | if self.size as i64 + i >= 0 { 161 | self.position = (self.size as i64 + i) as usize; 162 | Poll::Ready(Ok(self.position as u64)) 163 | } else { 164 | Poll::Ready(Err(io::Error::from(io::ErrorKind::InvalidInput))) 165 | } 166 | } 167 | } 168 | } 169 | } 170 | 171 | impl FileInfo for CachedFile { 172 | fn get_path(&self) -> &PathBuf { 173 | &self.path 174 | } 175 | 176 | fn get_mime(&self) -> Option<&Mime> { 177 | self.mime.as_ref() 178 | } 179 | 180 | fn get_size(&self) -> u64 { 181 | self.size 182 | } 183 | } 184 | 185 | impl AsyncRead for CachedFile { 186 | fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll> { 187 | let mut current_fut = self.get_file_future.take(); 188 | 189 | let res = if let Some(current) = current_fut.as_mut() { 190 | current.as_mut().poll(cx) 191 | } else { 192 | let mut current = Box::pin(Self::read_async(self.key.clone(), self.inner.clone(), self.position, buf.len())); 193 | let res = current.as_mut().poll(cx); 194 | current_fut = Some(current); 195 | res 196 | }; 197 | 198 | match res { 199 | Poll::Ready(res) => Poll::Ready(res.and_then(|bytes| { 200 | let len = bytes.len(); 201 | if len > 0 { 202 | self.position += len; 203 | let mut b = bytes.as_slice(); 204 | std::io::Read::read(&mut b, buf) 205 | } else { 206 | Ok(0) 207 | } 208 | })), 209 | 210 | Poll::Pending => { 211 | self.get_file_future = current_fut; 212 | Poll::Pending 213 | } 214 | } 215 | } 216 | } 217 | 218 | pub struct FileCacher { 219 | key: (String, Compression), 220 | inner: Pin>, 221 | buff: Vec, 222 | cache: FileCache, 223 | } 224 | 225 | impl FileCacher { 226 | pub fn new(key: (String, Compression), inner: Pin>, cache: FileCache) -> Self { 227 | FileCacher { 228 | key, 229 | inner, 230 | buff: Vec::with_capacity(MAX_BUFFER), 231 | cache, 232 | } 233 | } 234 | 235 | fn save_file_to_cache(&mut self) { 236 | let key = std::mem::take(&mut self.key); 237 | let buff = std::mem::take(&mut self.buff); 238 | let mut cache = self.cache.clone(); 239 | tokio::spawn(async move { 240 | cache.insert(key, buff).await; 241 | }); 242 | } 243 | } 244 | 245 | impl Drop for FileCacher { 246 | fn drop(&mut self) { 247 | self.save_file_to_cache(); 248 | } 249 | } 250 | 251 | impl AsyncRead for FileCacher { 252 | fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut [u8]) -> Poll> { 253 | match self.inner.as_mut().poll_read(cx, buf) { 254 | Poll::Ready(Ok(bytes)) => { 255 | if bytes > 0 { 256 | self.buff.extend_from_slice(&buf[0..bytes]); 257 | } 258 | Poll::Ready(Ok(bytes)) 259 | } 260 | Poll::Ready(Err(e)) => Poll::Ready(Err(e)), 261 | Poll::Pending => Poll::Pending, 262 | } 263 | } 264 | } 265 | 266 | impl AsyncSeek for FileCacher { 267 | fn poll_seek(mut self: Pin<&mut Self>, cx: &mut Context<'_>, pos: SeekFrom) -> Poll> { 268 | self.inner.as_mut().poll_seek(cx, pos) 269 | } 270 | } 271 | 272 | impl FileInfo for FileCacher { 273 | fn get_path(&self) -> &PathBuf { 274 | self.inner.get_path() 275 | } 276 | 277 | fn get_mime(&self) -> Option<&Mime> { 278 | self.inner.get_mime() 279 | } 280 | 281 | fn get_size(&self) -> u64 { 282 | self.inner.get_size() 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /saphir/src/macros.rs: -------------------------------------------------------------------------------- 1 | //! Saphir provides a proc_macro attribute and multiple function attributes. 2 | //! 3 | //! # The `#[controller]` Macro 4 | //! 5 | //! This macro is an attribute macro that need to be place on the `impl block` 6 | //! of a Saphir controller. It has 3 optionnal parameters: 7 | //! - `prefix="
"` : This will prefix any controller route by the specified
  8 | //!   route prefix
  9 | //! - `version=`  : This will insert the `/v#` path segment between the
 10 | //!   prefix and the base controller route
 11 | //! - `name=""`  : This will route the controller at /.
 12 | //!
 13 | //! If none of these are used, the controller will be routed at its own name, in
 14 | //! lowercase, with the controller keyword trimmed.
 15 | //!
 16 | //! # Function Attributes
 17 | //! We also parse several function attributes that can be placed above a
 18 | //! controller function (endpoint).
 19 | //!
 20 | //! ## The `#[("/")]` Attribute
 21 | //! This one is the attribute to add a endpoint to your controller, simply add a
 22 | //! method and a path above your endpoint function, and there ya go.
 23 | //! E.g. `#[get("/users/")]` would route its function to
 24 | //! /users/ with the HTTP method GET accepted.
 25 | //!
 26 | //! Path segments wrapped between '<' and '>', e.g. , are considered
 27 | //! parameters and mapped to the function parameter of the same name.
 28 | //!
 29 | //! The following parameters types are supported:
 30 | //!  - `CookieJar`: Collection of all the cookies in the request
 31 | //!  - `Json`: The request body interpreted in Json. If the request body is not
 32 | //!    valid Json, a 400 Bad Request response is returned.
 33 | //!  - `Form`: The request body interpreted as a standard form.
 34 | //!    (application/x-www-form-urlencoded) If the request body is not a valid
 35 | //!    Form, a 400 Bad Request response is returned.
 36 | //!  - `Multipart`: The request body interpreted as multipart form data
 37 | //!    (multipart/form-data) If the request body is not a valid multipart form,
 38 | //!    a 400 Bad Request response is returned.
 39 | //!  - `Ext`: Retrieve the MyExtensionType from the request
 40 | //!    extensions. Request extensions are data that you can attach to the
 41 | //!    request within Middlewares and Guards.
 42 | //!  - `Extensions`: Collection of all the extensions attached to the request.
 43 | //!    This is the whole owned collection, so it cannot be used in conjunction
 44 | //!    with single Ext parameters.
 45 | //!  - `Request`: The whole owned Saphir request. This is the whole owned
 46 | //!    request, so it cannot be used in conjunction of any of the above. (All of
 47 | //!    the above can be retrieved from this request)
 48 | //!  - `Option`: Any body parameter, path parameter or query string parameter
 49 | //!    (see below) can be marked as optionnal.
 50 | //!  - ``: Any other unhandled parameter type is considered a query string
 51 | //!    parameter. T must implement FromStr.
 52 | //!
 53 | //! We support even custom methods, and for convinience, `#[any(/your/path)]`
 54 | //! will be treated as : _any method_ being accepted.
 55 | //!
 56 | //! ## The `#[openapi(...)]` Attribute
 57 | //! This attribute can be added to a controller function (endpoint) to add
 58 | //! informations about the endpoint for OpenAPI generation through saphir's
 59 | //! CLI.
 60 | //! This attribute can be present multiple times and can include any number of
 61 | //! `return`, `return_override` and `params` parameters:
 62 | //!
 63 | //! ### The `return(...)` openapi parameter
 64 | //! **Syntax: `return(code = , type = ""[, mime = ])`**
 65 | //!
 66 | //! Specify a possible return code & type, and optionally a mime type.
 67 | //! The type must be a valid type path included (`use`) in the file.
 68 | //! E.g. `#[openapi(return(code = 200, type = "Json")]`
 69 | //!
 70 | //! `type` support infering the mimetype of built-in responders such as
 71 | //! `Json` and `Form`, so the following are ecquivalent :
 72 | //! - `#[openapi(return(code = 200, type = "Json")]`
 73 | //! - `#[openapi(return(code = 200, type = "self::MyType", mime = "json")]`
 74 | //! - `#[openapi(return(code = 200, type = "MyType", mime =
 75 | //!   "application/json")]`
 76 | //!
 77 | //! `type` can also be a string describing a raw object, for example :
 78 | //! `#[openapi(return(code = 200, type = "[{code: String, name: String}]))",
 79 | //! mime = "json"))]`
 80 | //!
 81 | //! You can also specify multiples codes that would return a similar type.
 82 | //! For example, if you have a type `MyJsonError` rendering an error as a json
 83 | //! payload, and your endpoint can return a 404 and a 500 in such a format,
 84 | //! you could write it as such :
 85 | //! `#[openapi(return(type = "MyJsonError", mime = "json", code = 404, code =
 86 | //! 500))]`
 87 | //!
 88 | //!
 89 | //! ### The `return_override(...)` openapi parameter
 90 | //! **Syntax: `return_override(type = "", code = [, mime =
 91 | //! ])`**
 92 | //!
 93 | //! Saphir provide some default API information for built-in types.
 94 | //! For example, a `Result::Ok` result has a status code of 200 by default, a
 95 | //! `Result::Err` a status code of 500, and a `Option::None` a status code of
 96 | //! 404. So, the following handler :
 97 | //! ```rust
 98 | //! # #[macro_use] extern crate saphir_macro;
 99 | //! # use crate::saphir::prelude::*;
100 | //! #
101 | //! # fn main() {}
102 | //! #
103 | //! # enum MyError {
104 | //! #     Unknown
105 | //! # }
106 | //! # impl Responder for MyError {
107 | //! #    fn respond_with_builder(self,builder: Builder,ctx: &HttpContext) -> Builder {
108 | //! #        unimplemented!()
109 | //! #    }
110 | //! # }
111 | //! #
112 | //! # struct MyController {}
113 | //! # #[controller(name = "my-controller")]
114 | //! # impl MyController {
115 | //! #[get("/")]
116 | //! async fn my_handler(&self) -> Result, MyError> { /*...*/ Ok(None) }
117 | //! # }
118 | //! ```
119 | //! will generate by default the same documentation as if it was written as
120 | //! such:
121 | //! ```rust
122 | //! # #[macro_use] extern crate saphir_macro;
123 | //! # use crate::saphir::prelude::*;
124 | //! #
125 | //! # fn main() {}
126 | //! #
127 | //! # enum MyError {
128 | //! #     Unknown
129 | //! # }
130 | //! # impl Responder for MyError {
131 | //! #    fn respond_with_builder(self,builder: Builder,ctx: &HttpContext) -> Builder {
132 | //! #        unimplemented!()
133 | //! #    }
134 | //! # }
135 | //! #
136 | //! # struct MyController {}
137 | //! # #[controller(name = "my-controller")]
138 | //! # impl MyController {
139 | //! #[get("/")]
140 | //! #[openapi(return(code = 200, type = "String", mime = "text/plain"))]
141 | //! #[openapi(return(code = 404, type = ""), return(code = 500, type = "MyError"))]
142 | //! async fn my_handler(&self) -> Result, MyError> { /*...*/ Ok(None) }
143 | //! # }
144 | //! ```
145 | //!
146 | //! If you want to start with these defaults and override the return of a single
147 | //! type in the composed result, for example specifying that `MyError` is
148 | //! rendered as a json document, then you can use `return_override` like this :
149 | //! ```rust
150 | //! # #[macro_use] extern crate saphir_macro;
151 | //! # use crate::saphir::prelude::*;
152 | //! #
153 | //! # fn main() {}
154 | //! #
155 | //! # enum MyError {
156 | //! #     Unknown
157 | //! # }
158 | //! # impl Responder for MyError {
159 | //! #    fn respond_with_builder(self,builder: Builder,ctx: &HttpContext) -> Builder {
160 | //! #        unimplemented!()
161 | //! #    }
162 | //! # }
163 | //! #
164 | //! # struct MyController {}
165 | //! # #[controller(name = "my-controller")]
166 | //! # impl MyController {
167 | //! #[get("/")]
168 | //! #[openapi(return_override(type = "MyError", mime = "application/json"))]
169 | //! async fn my_handler(&self) -> Result, MyError> { /*...*/ Ok(None) }
170 | //! # }
171 | //! ```
172 | //!
173 | //! ## The `#[cookies]` Attribute
174 | //! This will ensure cookies are parsed in the request before the endpoint
175 | //! function is called, cookies can than be accessed with
176 | //! `req.cookies().get("")`.
177 | //!
178 | //! ## The `#[guard]` Attribute
179 | //! This will add a request guard before your endpoint. It has two parameters:
180 | //! - `fn="path::to::your::guard_fn"` : *REQUIRED* This is used to specify what
181 | //!   guard function is to be called before your endpoint
182 | //! - `data="path::to::initializer"`  : _Optional_ This is used to instantiate
183 | //!   the data that will be passed to the guard function. this function takes a
184 | //!   reference of the controller type it is used in.
185 | //!
186 | //! ## The `#[validate(...)` Attribute
187 | //! **Syntax: `#[validate(exclude("excluded_param_1", "excluded_param_2"))]`**
188 | //!
189 | //! When using the `validate-requests` feature flag, saphir will generate
190 | //! validation code for all `Json` and `Form` request payloads using the [`validator`](https://github.com/Keats/validator) crate.
191 | //! Any `T` which does not implement the `validator::Validate` trait will cause
192 | //! compilation error.
193 | //! This macro attribute can be used to exclude validation on certain request
194 | //! parameters.
195 | //! Example:
196 | //! ```rust
197 | //! # #[macro_use] extern crate saphir_macro;
198 | //! # use crate::saphir::prelude::*;
199 | //! # use serde::Deserialize;
200 | //! #
201 | //! # fn main() {}
202 | //! #
203 | //! # enum MyError {
204 | //! #     Unknown
205 | //! # }
206 | //! # impl Responder for MyError {
207 | //! #    fn respond_with_builder(self,builder: Builder,ctx: &HttpContext) -> Builder {
208 | //! #        unimplemented!()
209 | //! #    }
210 | //! # }
211 | //! #
212 | //! #[derive(Deserialize)]
213 | //! struct MyPayload {
214 | //!    a: String,
215 | //! }
216 | //!
217 | //! struct MyController {}
218 | //!
219 | //! #[controller(name = "my-controller")]
220 | //! impl MyController {
221 | //!     #[post("/")]
222 | //!     #[validator(exclude("req"))]
223 | //!     async fn my_handler(&self, req: Json) -> Result<(), MyError> { /*...*/ Ok(()) }
224 | //! }
225 | //! ```
226 | //!
227 | //! # Type Attributes (Struct & Enum)
228 | //! These attributes can be added on top of a `struct` or `enum` definition.
229 | //!
230 | //! ## The `#[openapi(mime = )]` Attribute
231 | //! This attribute specify the OpenAPI mimetype for this type.
232 | 
233 | pub use futures::future::{BoxFuture, FutureExt};
234 | pub use saphir_macro::{controller, guard, middleware, openapi};
235 | 


--------------------------------------------------------------------------------
/deny.toml:
--------------------------------------------------------------------------------
  1 | # This template contains all of the possible sections and their default values
  2 | 
  3 | # Note that all fields that take a lint level have these possible values:
  4 | # * deny - An error will be produced and the check will fail
  5 | # * warn - A warning will be produced, but the check will not fail
  6 | # * allow - No warning or error will be produced, though in some cases a note
  7 | # will be
  8 | 
  9 | # The values provided in this template are the default values that will be used
 10 | # when any section or field is not specified in your own configuration
 11 | 
 12 | # If 1 or more target triples (and optionally, target_features) are specified,
 13 | # only the specified targets will be checked when running `cargo deny check`.
 14 | # This means, if a particular package is only ever used as a target specific
 15 | # dependency, such as, for example, the `nix` crate only being used via the
 16 | # `target_family = "unix"` configuration, that only having windows targets in
 17 | # this list would mean the nix crate, as well as any of its exclusive
 18 | # dependencies not shared by any other crates, would be ignored, as the target
 19 | # list here is effectively saying which targets you are building for.
 20 | targets = [
 21 |     # The triple can be any string, but only the target triples built in to
 22 |     # rustc (as of 1.40) can be checked against actual config expressions
 23 |     #{ triple = "x86_64-unknown-linux-musl" },
 24 |     # You can also specify which target_features you promise are enabled for a
 25 |     # particular target. target_features are currently not validated against
 26 |     # the actual valid features supported by the target architecture.
 27 |     #{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
 28 | ]
 29 | 
 30 | # This section is considered when running `cargo deny check advisories`
 31 | # More documentation for the advisories section can be found here:
 32 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
 33 | [advisories]
 34 | # The path where the advisory database is cloned/fetched into
 35 | db-path = "~/.cargo/advisory-db"
 36 | # The url(s) of the advisory databases to use
 37 | db-urls = ["https://github.com/rustsec/advisory-db"]
 38 | # The lint level for security vulnerabilities
 39 | vulnerability = "deny"
 40 | # The lint level for unmaintained crates
 41 | unmaintained = "warn"
 42 | # The lint level for crates that have been yanked from their source registry
 43 | yanked = "warn"
 44 | # The lint level for crates with security notices. Note that as of
 45 | # 2019-12-17 there are no security notice advisories in
 46 | # https://github.com/rustsec/advisory-db
 47 | notice = "warn"
 48 | # A list of advisory IDs to ignore. Note that ignored advisories will still
 49 | # output a note when they are encountered.
 50 | ignore = [
 51 |     "RUSTSEC-2020-0071" # Used by chrono; chrono have not released a patched version yet (0.4.22 is not patched)
 52 | ]
 53 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score
 54 | # lower than the range specified will be ignored. Note that ignored advisories
 55 | # will still output a note when they are encountered.
 56 | # * None - CVSS Score 0.0
 57 | # * Low - CVSS Score 0.1 - 3.9
 58 | # * Medium - CVSS Score 4.0 - 6.9
 59 | # * High - CVSS Score 7.0 - 8.9
 60 | # * Critical - CVSS Score 9.0 - 10.0
 61 | #severity-threshold =
 62 | 
 63 | # If this is true, then cargo deny will use the git executable to fetch advisory database.
 64 | # If this is false, then it uses a built-in git library.
 65 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
 66 | # See Git Authentication for more information about setting up git authentication.
 67 | #git-fetch-with-cli = true
 68 | 
 69 | # This section is considered when running `cargo deny check licenses`
 70 | # More documentation for the licenses section can be found here:
 71 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
 72 | [licenses]
 73 | # The lint level for crates which do not have a detectable license
 74 | unlicensed = "deny"
 75 | # List of explicitly allowed licenses
 76 | # See https://spdx.org/licenses/ for list of possible licenses
 77 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)].
 78 | allow = [
 79 |     "MIT",
 80 |     "Apache-2.0",
 81 |     "Unicode-DFS-2016",
 82 | ]
 83 | # List of explicitly disallowed licenses
 84 | # See https://spdx.org/licenses/ for list of possible licenses
 85 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)].
 86 | deny = [
 87 |     #"Nokia",
 88 | ]
 89 | # Lint level for licenses considered copyleft
 90 | copyleft = "warn"
 91 | # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
 92 | # * both - The license will be approved if it is both OSI-approved *AND* FSF
 93 | # * either - The license will be approved if it is either OSI-approved *OR* FSF
 94 | # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
 95 | # * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
 96 | # * neither - This predicate is ignored and the default lint level is used
 97 | allow-osi-fsf-free = "neither"
 98 | # Lint level used when no other predicates are matched
 99 | # 1. License isn't in the allow or deny lists
100 | # 2. License isn't copyleft
101 | # 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
102 | default = "deny"
103 | # The confidence threshold for detecting a license from license text.
104 | # The higher the value, the more closely the license text must be to the
105 | # canonical license text of a valid SPDX license file.
106 | # [possible values: any between 0.0 and 1.0].
107 | confidence-threshold = 0.8
108 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses
109 | # aren't accepted for every possible crate as with the normal allow list
110 | exceptions = [
111 |     # Each entry is the crate and version constraint, and its specific allow
112 |     # list
113 |     #{ allow = ["Zlib"], name = "adler32", version = "*" },
114 | ]
115 | 
116 | # Some crates don't have (easily) machine readable licensing information,
117 | # adding a clarification entry for it allows you to manually specify the
118 | # licensing information
119 | #[[licenses.clarify]]
120 | # The name of the crate the clarification applies to
121 | #name = "ring"
122 | # The optional version constraint for the crate
123 | #version = "*"
124 | # The SPDX expression for the license requirements of the crate
125 | #expression = "MIT AND ISC AND OpenSSL"
126 | # One or more files in the crate's source used as the "source of truth" for
127 | # the license expression. If the contents match, the clarification will be used
128 | # when running the license check, otherwise the clarification will be ignored
129 | # and the crate will be checked normally, which may produce warnings or errors
130 | # depending on the rest of your configuration
131 | #license-files = [
132 |     # Each entry is a crate relative path, and the (opaque) hash of its contents
133 |     #{ path = "LICENSE", hash = 0xbd0eed23 }
134 | #]
135 | 
136 | [licenses.private]
137 | # If true, ignores workspace crates that aren't published, or are only
138 | # published to private registries.
139 | # To see how to mark a crate as unpublished (to the official registry),
140 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
141 | ignore = false
142 | # One or more private registries that you might publish crates to, if a crate
143 | # is only published to private registries, and ignore is true, the crate will
144 | # not have its license(s) checked
145 | registries = [
146 |     #"https://sekretz.com/registry
147 | ]
148 | 
149 | # This section is considered when running `cargo deny check bans`.
150 | # More documentation about the 'bans' section can be found here:
151 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
152 | [bans]
153 | # Lint level for when multiple versions of the same crate are detected
154 | multiple-versions = "warn"
155 | # Lint level for when a crate version requirement is `*`
156 | wildcards = "allow"
157 | # The graph highlighting used when creating dotgraphs for crates
158 | # with multiple versions
159 | # * lowest-version - The path to the lowest versioned duplicate is highlighted
160 | # * simplest-path - The path to the version with the fewest edges is highlighted
161 | # * all - Both lowest-version and simplest-path are used
162 | highlight = "all"
163 | # List of crates that are allowed. Use with care!
164 | allow = [
165 |     #{ name = "ansi_term", version = "=0.11.0" },
166 | ]
167 | # List of crates to deny
168 | deny = [
169 |     # Each entry the name of a crate and a version range. If version is
170 |     # not specified, all versions will be matched.
171 |     #{ name = "ansi_term", version = "=0.11.0" },
172 |     #
173 |     # Wrapper crates can optionally be specified to allow the crate when it
174 |     # is a direct dependency of the otherwise banned crate
175 |     #{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
176 | ]
177 | # Certain crates/versions that will be skipped when doing duplicate detection.
178 | skip = [
179 |     #{ name = "ansi_term", version = "=0.11.0" },
180 | ]
181 | # Similarly to `skip` allows you to skip certain crates during duplicate
182 | # detection. Unlike skip, it also includes the entire tree of transitive
183 | # dependencies starting at the specified crate, up to a certain depth, which is
184 | # by default infinite
185 | skip-tree = [
186 |     #{ name = "ansi_term", version = "=0.11.0", depth = 20 },
187 | ]
188 | 
189 | # This section is considered when running `cargo deny check sources`.
190 | # More documentation about the 'sources' section can be found here:
191 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
192 | [sources]
193 | # Lint level for what to happen when a crate from a crate registry that is not
194 | # in the allow list is encountered
195 | unknown-registry = "warn"
196 | # Lint level for what to happen when a crate from a git repository that is not
197 | # in the allow list is encountered
198 | unknown-git = "warn"
199 | # List of URLs for allowed crate registries. Defaults to the crates.io index
200 | # if not specified. If it is specified but empty, no registries are allowed.
201 | allow-registry = ["https://github.com/rust-lang/crates.io-index"]
202 | # List of URLs for allowed Git repositories
203 | allow-git = []
204 | 
205 | [sources.allow-org]
206 | # 1 or more github.com organizations to allow git sources for
207 | #github = [""]
208 | # 1 or more gitlab.com organizations to allow git sources for
209 | #gitlab = [""]
210 | # 1 or more bitbucket.org organizations to allow git sources for
211 | #bitbucket = [""]
212 | 


--------------------------------------------------------------------------------
/saphir/src/error.rs:
--------------------------------------------------------------------------------
  1 | use crate::{
  2 |     http_context::HttpContext,
  3 |     responder::{DynResponder, Responder},
  4 |     response::Builder,
  5 | };
  6 | use http::{
  7 |     header::{InvalidHeaderValue, ToStrError},
  8 |     Error as HttpCrateError,
  9 | };
 10 | use hyper::Error as HyperError;
 11 | use std::{
 12 |     error::Error as StdError,
 13 |     fmt::{Debug, Formatter},
 14 |     io::Error as IoError,
 15 | };
 16 | use thiserror::Error;
 17 | 
 18 | /// Type representing an internal error inerrant to the underlining logic behind
 19 | /// saphir
 20 | #[derive(Error, Debug)]
 21 | pub enum InternalError {
 22 |     #[error("Http: {0}")]
 23 |     Http(HttpCrateError),
 24 |     #[error("Hyper: {0}")]
 25 |     Hyper(HyperError),
 26 |     #[error("ToStr: {0}")]
 27 |     ToStr(ToStrError),
 28 |     #[error("Stack")]
 29 |     Stack,
 30 | }
 31 | 
 32 | /// Error type throughout the saphir stack
 33 | #[derive(Error)]
 34 | pub enum SaphirError {
 35 |     ///
 36 |     #[error("Internal: {0}")]
 37 |     Internal(#[from] InternalError),
 38 |     ///
 39 |     #[error("Io: {0}")]
 40 |     Io(#[from] IoError),
 41 |     /// Body was taken and cannot be polled
 42 |     #[error("Body already taken")]
 43 |     BodyAlreadyTaken,
 44 |     /// The request was moved by a middleware without ending the request
 45 |     /// processing
 46 |     #[error("Request moved before handler")]
 47 |     RequestMovedBeforeHandler,
 48 |     /// The response was moved before being sent to the client
 49 |     #[error("Response moved")]
 50 |     ResponseMoved,
 51 |     /// Custom error type to map any other error
 52 |     #[error("Custom: {0}")]
 53 |     Custom(Box),
 54 |     /// Custom error type to map any other error
 55 |     #[error("Responder")]
 56 |     Responder(Box),
 57 |     ///
 58 |     #[error("Other: {0}")]
 59 |     Other(String),
 60 |     /// Error from (de)serializing json data
 61 |     #[cfg(feature = "json")]
 62 |     #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
 63 |     #[error("SerdeJson: {0}")]
 64 |     SerdeJson(#[from] serde_json::error::Error),
 65 |     /// Error from deserializing form data
 66 |     #[cfg(feature = "form")]
 67 |     #[cfg_attr(docsrs, doc(cfg(feature = "form")))]
 68 |     #[error("SerdeUrlDe: {0}")]
 69 |     SerdeUrlDe(#[from] serde_urlencoded::de::Error),
 70 |     /// Error from serializing form data
 71 |     #[cfg(feature = "form")]
 72 |     #[cfg_attr(docsrs, doc(cfg(feature = "form")))]
 73 |     #[error("SerdeUrlSer: {0}")]
 74 |     SerdeUrlSer(#[from] serde_urlencoded::ser::Error),
 75 |     ///
 76 |     #[error("Missing parameter `{0}` (is_query: {1})")]
 77 |     MissingParameter(String, bool),
 78 |     ///
 79 |     #[error("Invalid parameter `{0}` (is_query: {1})")]
 80 |     InvalidParameter(String, bool),
 81 |     ///
 82 |     #[error("Request timed out")]
 83 |     RequestTimeout,
 84 |     /// Attempted to build stack twice
 85 |     #[error("Stack alrealy initialized")]
 86 |     StackAlreadyInitialized,
 87 |     ///
 88 |     #[error("Too many requests")]
 89 |     TooManyRequests,
 90 |     /// Validator error
 91 |     #[cfg(feature = "validate-requests")]
 92 |     #[cfg_attr(docsrs, doc(cfg(feature = "validate-requests")))]
 93 |     #[error("ValidationErrors: {0}")]
 94 |     ValidationErrors(#[from] validator::ValidationErrors),
 95 | }
 96 | 
 97 | impl Debug for SaphirError {
 98 |     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 99 |         match self {
100 |             SaphirError::Internal(d) => std::fmt::Debug::fmt(d, f),
101 |             SaphirError::Io(d) => std::fmt::Debug::fmt(d, f),
102 |             SaphirError::BodyAlreadyTaken => f.write_str("BodyAlreadyTaken"),
103 |             SaphirError::RequestMovedBeforeHandler => f.write_str("RequestMovedBeforeHandler"),
104 |             SaphirError::ResponseMoved => f.write_str("ResponseMoved"),
105 |             SaphirError::Custom(d) => std::fmt::Debug::fmt(d, f),
106 |             SaphirError::Responder(_) => f.write_str("Responder"),
107 |             SaphirError::Other(d) => std::fmt::Debug::fmt(d, f),
108 |             #[cfg(feature = "json")]
109 |             SaphirError::SerdeJson(d) => std::fmt::Debug::fmt(d, f),
110 |             #[cfg(feature = "form")]
111 |             SaphirError::SerdeUrlDe(d) => std::fmt::Debug::fmt(d, f),
112 |             #[cfg(feature = "form")]
113 |             SaphirError::SerdeUrlSer(d) => std::fmt::Debug::fmt(d, f),
114 |             SaphirError::MissingParameter(d, _) => std::fmt::Debug::fmt(d, f),
115 |             SaphirError::InvalidParameter(d, _) => std::fmt::Debug::fmt(d, f),
116 |             SaphirError::RequestTimeout => f.write_str("RequestTimeout"),
117 |             SaphirError::StackAlreadyInitialized => f.write_str("StackAlreadyInitialized"),
118 |             SaphirError::TooManyRequests => f.write_str("TooManyRequests"),
119 |             #[cfg(feature = "validate-requests")]
120 |             SaphirError::ValidationErrors(d) => std::fmt::Debug::fmt(d, f),
121 |         }
122 |     }
123 | }
124 | 
125 | impl SaphirError {
126 |     pub fn responder(e: T) -> Self {
127 |         SaphirError::Responder(Box::new(Some(e)))
128 |     }
129 | 
130 |     pub(crate) fn response_builder(self, builder: Builder, ctx: &HttpContext) -> Builder {
131 |         match self {
132 |             SaphirError::Internal(_) => builder.status(500),
133 |             SaphirError::Io(_) => builder.status(500),
134 |             SaphirError::BodyAlreadyTaken => builder.status(500),
135 |             SaphirError::Custom(_) => builder.status(500),
136 |             SaphirError::Other(_) => builder.status(500),
137 |             #[cfg(feature = "json")]
138 |             SaphirError::SerdeJson(_) => builder.status(400),
139 |             #[cfg(feature = "form")]
140 |             SaphirError::SerdeUrlDe(_) => builder.status(400),
141 |             #[cfg(feature = "form")]
142 |             SaphirError::SerdeUrlSer(_) => builder.status(400),
143 |             SaphirError::MissingParameter(..) => builder.status(400),
144 |             SaphirError::InvalidParameter(..) => builder.status(400),
145 |             SaphirError::RequestMovedBeforeHandler => builder.status(500),
146 |             SaphirError::ResponseMoved => builder.status(500),
147 |             SaphirError::Responder(mut r) => r.dyn_respond(builder, ctx),
148 |             SaphirError::RequestTimeout => builder.status(408),
149 |             SaphirError::StackAlreadyInitialized => builder.status(500),
150 |             SaphirError::TooManyRequests => builder.status(429),
151 |             #[cfg(feature = "validate-requests")]
152 |             SaphirError::ValidationErrors(_) => builder.status(400),
153 |         }
154 |     }
155 | 
156 |     #[allow(unused_variables)]
157 |     pub(crate) fn log(&self, ctx: &HttpContext) {
158 |         let op_id = {
159 |             #[cfg(not(feature = "operation"))]
160 |             {
161 |                 String::new()
162 |             }
163 | 
164 |             #[cfg(feature = "operation")]
165 |             {
166 |                 format!("[Operation id: {}] ", ctx.operation_id)
167 |             }
168 |         };
169 | 
170 |         match self {
171 |             SaphirError::Internal(e) => {
172 |                 warn!("{}Saphir encountered an internal error that was returned as a responder: {:?}", op_id, e);
173 |             }
174 |             SaphirError::Io(e) => {
175 |                 warn!("{}Saphir encountered an Io error that was returned as a responder: {:?}", op_id, e);
176 |             }
177 |             SaphirError::BodyAlreadyTaken => {
178 |                 warn!("{}A controller handler attempted to take the request body more thant one time", op_id);
179 |             }
180 |             SaphirError::Custom(e) => {
181 |                 warn!("{}A custom error was returned as a responder: {:?}", op_id, e);
182 |             }
183 |             SaphirError::Other(e) => {
184 |                 warn!("{}Saphir encountered an Unknown error that was returned as a responder: {:?}", op_id, e);
185 |             }
186 |             #[cfg(feature = "json")]
187 |             SaphirError::SerdeJson(e) => {
188 |                 debug!("{}Unable to de/serialize json type: {:?}", op_id, e);
189 |             }
190 |             #[cfg(feature = "form")]
191 |             SaphirError::SerdeUrlDe(e) => {
192 |                 debug!("{}Unable to deserialize form type: {:?}", op_id, e);
193 |             }
194 |             #[cfg(feature = "form")]
195 |             SaphirError::SerdeUrlSer(e) => {
196 |                 debug!("{}Unable to serialize form type: {:?}", op_id, e);
197 |             }
198 |             SaphirError::MissingParameter(name, is_query) => {
199 |                 if *is_query {
200 |                     debug!("{}Missing query parameter {}", op_id, name);
201 |                 } else {
202 |                     debug!("{}Missing path parameter {}", op_id, name);
203 |                 }
204 |             }
205 |             SaphirError::InvalidParameter(name, is_query) => {
206 |                 if *is_query {
207 |                     debug!("{}Unable to parse query parameter {}", op_id, name);
208 |                 } else {
209 |                     debug!("{}Unable to parse path parameter {}", op_id, name);
210 |                 }
211 |             }
212 |             SaphirError::RequestMovedBeforeHandler => {
213 |                 warn!(
214 |                     "{}A request was moved out of its context by a middleware, but the middleware did not stop request processing",
215 |                     op_id
216 |                 );
217 |             }
218 |             SaphirError::ResponseMoved => {
219 |                 warn!("{}A response was moved before being sent to the client", op_id);
220 |             }
221 |             SaphirError::RequestTimeout => {
222 |                 warn!("{}Request timed out", op_id);
223 |             }
224 |             SaphirError::Responder(_) => {}
225 |             SaphirError::StackAlreadyInitialized => {
226 |                 warn!("{}Attempted to initialize stack twice", op_id);
227 |             }
228 |             SaphirError::TooManyRequests => {
229 |                 warn!("{}Made too many requests", op_id);
230 |             }
231 |             #[cfg(feature = "validate-requests")]
232 |             SaphirError::ValidationErrors(e) => {
233 |                 debug!("{}Validation error: {:?}", op_id, e);
234 |             }
235 |         }
236 |     }
237 | }
238 | 
239 | impl From for SaphirError {
240 |     fn from(e: HttpCrateError) -> Self {
241 |         SaphirError::Internal(InternalError::Http(e))
242 |     }
243 | }
244 | 
245 | impl From for SaphirError {
246 |     fn from(e: InvalidHeaderValue) -> Self {
247 |         SaphirError::Internal(InternalError::Http(HttpCrateError::from(e)))
248 |     }
249 | }
250 | 
251 | impl From for SaphirError {
252 |     fn from(e: HyperError) -> Self {
253 |         SaphirError::Internal(InternalError::Hyper(e))
254 |     }
255 | }
256 | 
257 | impl From for SaphirError {
258 |     fn from(e: ToStrError) -> Self {
259 |         SaphirError::Internal(InternalError::ToStr(e))
260 |     }
261 | }
262 | 
263 | impl Responder for SaphirError {
264 |     #[allow(unused_variables)]
265 |     fn respond_with_builder(self, builder: Builder, ctx: &HttpContext) -> Builder {
266 |         self.log(ctx);
267 |         self.response_builder(builder, ctx)
268 |     }
269 | }
270 | 


--------------------------------------------------------------------------------
/saphir/src/file/range.rs:
--------------------------------------------------------------------------------
  1 | // Copyright (c) 2014-2018 Sean McArthur
  2 | // Copyright (c) 2018 The hyperx Contributors
  3 | // license: https://github.com/dekellum/hyperx/blob/master/LICENSE
  4 | // source: https://github.com/dekellum/hyperx/blob/master/src/header/common/range.rs
  5 | 
  6 | use crate::error::SaphirError;
  7 | use std::{fmt::Display, str::FromStr};
  8 | 
  9 | /// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1)
 10 | ///
 11 | /// The "Range" header field on a GET request modifies the method
 12 | /// semantics to request transfer of only one or more subranges of the
 13 | /// selected representation data, rather than the entire selected
 14 | /// representation data.
 15 | #[derive(PartialEq, Eq, Clone, Debug)]
 16 | pub enum Range {
 17 |     /// Byte range
 18 |     Bytes(Vec),
 19 |     /// Custom range, with unit not registered at IANA
 20 |     /// (`other-range-unit`: String , `other-range-set`: String)
 21 |     Unregistered(String, String),
 22 | }
 23 | 
 24 | /// Each `Range::Bytes` header can contain one or more `ByteRangeSpecs`.
 25 | /// Each `ByteRangeSpec` defines a range of bytes to fetch
 26 | #[derive(PartialEq, Eq, Clone, Debug)]
 27 | pub enum ByteRangeSpec {
 28 |     /// Get all bytes between x and y ("x-y")
 29 |     FromTo(u64, u64),
 30 |     /// Get all bytes starting from x ("x-")
 31 |     AllFrom(u64),
 32 |     /// Get last x bytes ("-x")
 33 |     Last(u64),
 34 | }
 35 | 
 36 | impl ByteRangeSpec {
 37 |     /// Given the full length of the entity, attempt to normalize the byte range
 38 |     /// into an satisfiable end-inclusive (from, to) range.
 39 |     ///
 40 |     /// The resulting range is guaranteed to be a satisfiable range within the
 41 |     /// bounds of `0 <= from <= to < full_length`.
 42 |     ///
 43 |     /// If the byte range is deemed unsatisfiable, `None` is returned.
 44 |     /// An unsatisfiable range is generally cause for a server to either reject
 45 |     /// the client request with a `416 Range Not Satisfiable` status code, or to
 46 |     /// simply ignore the range header and serve the full entity using a `200
 47 |     /// OK` status code.
 48 |     ///
 49 |     /// This function closely follows [RFC 7233][1] section 2.1.
 50 |     /// As such, it considers ranges to be satisfiable if they meet the
 51 |     /// following conditions:
 52 |     ///
 53 |     /// > If a valid byte-range-set includes at least one byte-range-spec with
 54 |     /// > a first-byte-pos that is less than the current length of the
 55 |     /// > representation, or at least one suffix-byte-range-spec with a
 56 |     /// > non-zero suffix-length, then the byte-range-set is satisfiable.
 57 |     /// > Otherwise, the byte-range-set is unsatisfiable.
 58 |     ///
 59 |     /// The function also computes remainder ranges based on the RFC:
 60 |     ///
 61 |     /// > If the last-byte-pos value is
 62 |     /// > absent, or if the value is greater than or equal to the current
 63 |     /// > length of the representation data, the byte range is interpreted as
 64 |     /// > the remainder of the representation (i.e., the server replaces the
 65 |     /// > value of last-byte-pos with a value that is one less than the current
 66 |     /// > length of the selected representation).
 67 |     ///
 68 |     /// [1]: https://tools.ietf.org/html/rfc7233
 69 |     pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> {
 70 |         // If the full length is zero, there is no satisfiable end-inclusive range.
 71 |         if full_length == 0 {
 72 |             return None;
 73 |         }
 74 |         match self {
 75 |             ByteRangeSpec::FromTo(from, to) => {
 76 |                 if *from < full_length && *from <= *to {
 77 |                     Some((*from, ::std::cmp::min(*to, full_length - 1)))
 78 |                 } else {
 79 |                     None
 80 |                 }
 81 |             }
 82 |             ByteRangeSpec::AllFrom(from) => {
 83 |                 if *from < full_length {
 84 |                     Some((*from, full_length - 1))
 85 |                 } else {
 86 |                     None
 87 |                 }
 88 |             }
 89 |             ByteRangeSpec::Last(last) => {
 90 |                 if *last > 0 {
 91 |                     // From the RFC: If the selected representation is shorter
 92 |                     // than the specified suffix-length,
 93 |                     // the entire representation is used.
 94 |                     if *last > full_length {
 95 |                         Some((0, full_length - 1))
 96 |                     } else {
 97 |                         Some((full_length - *last, full_length - 1))
 98 |                     }
 99 |                 } else {
100 |                     None
101 |                 }
102 |             }
103 |         }
104 |     }
105 | }
106 | 
107 | impl Display for ByteRangeSpec {
108 |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 |         match *self {
110 |             ByteRangeSpec::FromTo(from, to) => write!(f, "{from}-{to}"),
111 |             ByteRangeSpec::Last(pos) => write!(f, "-{pos}"),
112 |             ByteRangeSpec::AllFrom(pos) => write!(f, "{pos}-"),
113 |         }
114 |     }
115 | }
116 | 
117 | impl Display for Range {
118 |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 |         match *self {
120 |             Range::Bytes(ref ranges) => {
121 |                 write!(f, "bytes=")?;
122 | 
123 |                 for (i, range) in ranges.iter().enumerate() {
124 |                     if i != 0 {
125 |                         write!(f, ",")?;
126 |                     }
127 |                     write!(f, "{range}")?;
128 |                 }
129 | 
130 |                 Ok(())
131 |             }
132 |             Range::Unregistered(ref unit, ref range_str) => write!(f, "{unit}={range_str}"),
133 |         }
134 |     }
135 | }
136 | 
137 | impl FromStr for Range {
138 |     type Err = SaphirError;
139 | 
140 |     fn from_str(s: &str) -> Result {
141 |         let mut iter = s.splitn(2, '=');
142 | 
143 |         match (iter.next(), iter.next()) {
144 |             (Some("bytes"), Some(ranges)) => {
145 |                 let ranges = from_comma_delimited(ranges);
146 |                 if ranges.is_empty() {
147 |                     return Err(SaphirError::Other("Range is empty".to_owned()));
148 |                 }
149 |                 Ok(Range::Bytes(ranges))
150 |             }
151 |             (Some(unit), Some(range_str)) if !unit.is_empty() && !range_str.is_empty() => Ok(Range::Unregistered(unit.to_owned(), range_str.to_owned())),
152 |             _ => Err(SaphirError::Other("Bad Format".to_owned())),
153 |         }
154 |     }
155 | }
156 | 
157 | impl FromStr for ByteRangeSpec {
158 |     type Err = SaphirError;
159 | 
160 |     fn from_str(s: &str) -> Result {
161 |         let mut parts = s.splitn(2, '-');
162 | 
163 |         match (parts.next(), parts.next()) {
164 |             (Some(""), Some(end)) => end
165 |                 .parse()
166 |                 .map_err(|_| SaphirError::Other("Could not parse bytes".to_owned()))
167 |                 .map(ByteRangeSpec::Last),
168 |             (Some(start), Some("")) => start
169 |                 .parse()
170 |                 .map_err(|_| SaphirError::Other("Could not parse bytes".to_owned()))
171 |                 .map(ByteRangeSpec::AllFrom),
172 |             (Some(start), Some(end)) => match (start.parse(), end.parse()) {
173 |                 (Ok(start), Ok(end)) if start <= end => Ok(ByteRangeSpec::FromTo(start, end)),
174 |                 _ => Err(SaphirError::Other("Could not parse bytes".to_owned())),
175 |             },
176 |             _ => Err(SaphirError::Other("ByteRange is missing or incomplete".to_owned())),
177 |         }
178 |     }
179 | }
180 | 
181 | fn from_comma_delimited(s: &str) -> Vec {
182 |     s.split(',')
183 |         .filter_map(|x| match x.trim() {
184 |             "" => None,
185 |             y => Some(y),
186 |         })
187 |         .filter_map(|x| x.parse().ok())
188 |         .collect()
189 | }
190 | 
191 | #[cfg(test)]
192 | mod tests {
193 |     use super::{ByteRangeSpec, Range};
194 |     use std::str::FromStr;
195 | 
196 |     pub fn bytes(from: u64, to: u64) -> Range {
197 |         Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)])
198 |     }
199 | 
200 |     #[test]
201 |     fn test_parse_bytes_range_valid() {
202 |         let r = Range::from_str("bytes=1-100").unwrap();
203 |         let r2 = Range::from_str("bytes=1-100,-").unwrap();
204 |         let r3 = bytes(1, 100);
205 |         assert_eq!(r, r2);
206 |         assert_eq!(r2, r3);
207 | 
208 |         let r = Range::from_str("bytes=1-100,200-").unwrap();
209 |         let r2 = Range::from_str("bytes= 1-100 , 101-xxx,  200- ").unwrap();
210 |         let r3 = Range::Bytes(vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200)]);
211 |         assert_eq!(r, r2);
212 |         assert_eq!(r2, r3);
213 | 
214 |         let r = Range::from_str("bytes=1-100,-100").unwrap();
215 |         let r2 = Range::from_str("bytes=1-100, ,,-100").unwrap();
216 |         let r3 = Range::Bytes(vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::Last(100)]);
217 |         assert_eq!(r, r2);
218 |         assert_eq!(r2, r3);
219 | 
220 |         let r = Range::from_str("custom=1-100,-100").unwrap();
221 |         let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned());
222 |         assert_eq!(r, r2);
223 |     }
224 | 
225 |     #[test]
226 |     fn test_parse_unregistered_range_valid() {
227 |         let r = Range::from_str("custom=1-100,-100").unwrap();
228 |         let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned());
229 |         assert_eq!(r, r2);
230 | 
231 |         let r = Range::from_str("custom=abcd").unwrap();
232 |         let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned());
233 |         assert_eq!(r, r2);
234 | 
235 |         let r = Range::from_str("custom=xxx-yyy").unwrap();
236 |         let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned());
237 |         assert_eq!(r, r2);
238 |     }
239 | 
240 |     #[test]
241 |     fn test_parse_invalid() {
242 |         let r = Range::from_str("bytes=1-a,-");
243 |         assert_eq!(r.ok(), None);
244 | 
245 |         let r = Range::from_str("bytes=1-2-3");
246 |         assert_eq!(r.ok(), None);
247 | 
248 |         let r = Range::from_str("abc");
249 |         assert_eq!(r.ok(), None);
250 | 
251 |         let r = Range::from_str("bytes=1-100=");
252 |         assert_eq!(r.ok(), None);
253 | 
254 |         let r = Range::from_str("bytes=");
255 |         assert_eq!(r.ok(), None);
256 | 
257 |         let r = Range::from_str("custom=");
258 |         assert_eq!(r.ok(), None);
259 | 
260 |         let r = Range::from_str("=1-100");
261 |         assert_eq!(r.ok(), None);
262 |     }
263 | 
264 |     #[test]
265 |     fn test_byte_range_spec_to_satisfiable_range() {
266 |         assert_eq!(Some((0, 0)), ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3));
267 |         assert_eq!(Some((1, 2)), ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3));
268 |         assert_eq!(Some((1, 2)), ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3));
269 |         assert_eq!(None, ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3));
270 |         assert_eq!(None, ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3));
271 |         assert_eq!(None, ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0));
272 | 
273 |         assert_eq!(Some((0, 2)), ByteRangeSpec::AllFrom(0).to_satisfiable_range(3));
274 |         assert_eq!(Some((2, 2)), ByteRangeSpec::AllFrom(2).to_satisfiable_range(3));
275 |         assert_eq!(None, ByteRangeSpec::AllFrom(3).to_satisfiable_range(3));
276 |         assert_eq!(None, ByteRangeSpec::AllFrom(5).to_satisfiable_range(3));
277 |         assert_eq!(None, ByteRangeSpec::AllFrom(0).to_satisfiable_range(0));
278 | 
279 |         assert_eq!(Some((1, 2)), ByteRangeSpec::Last(2).to_satisfiable_range(3));
280 |         assert_eq!(Some((2, 2)), ByteRangeSpec::Last(1).to_satisfiable_range(3));
281 |         assert_eq!(Some((0, 2)), ByteRangeSpec::Last(5).to_satisfiable_range(3));
282 |         assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3));
283 |         assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0));
284 |     }
285 | }
286 | 


--------------------------------------------------------------------------------