├── .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 | [](https://docs.rs/saphir/)
3 | [](https://crates.io/crates/saphir)
4 | [](https://github.com/richerarc/saphir/issues)
5 | 
6 | 
7 | [](https://github.com/richerarc/saphir/blob/master/LICENSE)
8 | [](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