darpi 0.1.0

A web framework with type safety and speed in mind
Documentation

darpi

A web api framework with speed and safety in mind. One of the big goals is to catch all errors at compile time, if possible. The framework uses hyper and ideally, the performance should be as if you were using hyper yourself. The framework also uses shaku for compile time verifiable dependency injection.

The framework is in early development. All feedback is appreciated.

An example of a simple app

Cargo.toml

[dependencies]
darpi = {git = "https://github.com/petar-dambovaliev/darpi.git", branch = "master"}
serde = { version = "1.0", features = ["derive"] }
tokio = {version = "0.2.11", features = ["full"]}
shaku = {version = "0.5.0", features = ["thread_safe"]}

main.rs

use async_trait::async_trait;
use darpi::{
    app, handler, middleware, middleware::Expect, path_type, query_type, request::ExtractBody,
    response::ResponderError, Body, Json, Method, Path, Query, RequestParts,
};
use derive_more::{Display, From};
use serde::{Deserialize, Serialize};
use shaku::{module, Component, Interface};
use std::sync::Arc;
use UserRole::Admin;
 
///////////// setup dependencies with shaku ///////////
#[derive(Component)]
#[shaku(interface = UserExtractor)]
struct UserExtractorImpl;

#[async_trait]
impl UserExtractor for UserExtractorImpl {
    async fn extract(&self, p: &RequestParts) -> Result<UserRole, Error> {
        Ok(UserRole::Admin)
    }
}

#[async_trait]
trait UserExtractor: Interface {
    async fn extract(&self, p: &RequestParts) -> Result<UserRole, Error>;
}

module! {
    Container {
        components = [UserExtractorImpl, MyLogger],
        providers = [],
    }
}

fn make_container() -> Container {
    Container::builder().build()
}

////////////////////////

#[derive(Debug, Display, From)]
enum Error {
    #[display(fmt = "no auth header")]
    NoAuthHeaderError,
    #[display(fmt = "Access denied")]
    AccessDenied,
}

impl ResponderError for Error {}

#[derive(Eq, PartialEq, Ord, PartialOrd)]
enum UserRole {
    Regular,
    Admin,
}

// there are 2 types of middleware `Request` and `Response`
// the constant argument that needs to be present is &RequestParts
// everything else is up to the user
// Arc<dyn UserExtractor> types are injected from the shaku container
// Expect<UserRole> is a special type that is provided by the user when
// the middleware is linked to a handler. This allows the expected value
// to be different per handler + middleware
// middlewares are obgligated to return Result<(), impl ResponderErr>
// if a middleware returns an Err(e) all work is aborted and the coresponding
// response is sent to the user
#[middleware(Request)]
async fn access_control(
    user_role_extractor: Arc<dyn UserExtractor>,
    p: &RequestParts,
    expected_role: Expect<UserRole>,
) -> Result<(), Error> {
    let actual_role = user_role_extractor.extract(p).await?;

    if expected_role > actual_role {
        return Err(Error::AccessDenied);
    }
    Ok(())
}

#[path_type]
#[query_type]
#[derive(Deserialize, Serialize, Debug)]
pub struct Name {
    name: String,
}

// Path<Name> is extracted from the registered path "/hello_world/{name}"
// and it is always mandatory. A request without "{name}" will result
// in the request path not matching the handler. It will either match another
// handler or result in an 404
// Option<Query<Name>> is extracted from the url query "?name=jason"
// it is optional, as the type suggests. To make it mandatory, simply
// remove the Option type. If there is a Query<T> in the handler and
// an incoming request url does not contain the query parameters, it will
// result in an error response
#[handler]
async fn hello_world(p: Path<Name>, q: Option<Query<Name>>) -> String {
    let other = q.map_or("nobody".to_owned(), |n| n.0.name);
    format!("{} sends hello to {}", p.name, other)
}

// the handler macro has 2 optional arguments
// the shaku container type and a collection of middlewares
// the enum variant `Admin` is corresponding to the middlewre `access_control`'s Expect<UserRole>
// ExtractBody<T<Name>> is extracted from the request body
// where T is the format and it has to implement the trait FromRequestBody
// Json, Yaml and Xml are supported out of the box
// failure to extract will result in an error response
#[handler(Container, [access_control(Admin)])]
async fn do_something(p: Path<Name>, payload: ExtractBody<Json<Name>>) -> String {
    format!("{} sends hello to {}", p.name, payload.name)
}
 
 #[tokio::main]
 async fn main() -> Result<(), darpi::Error> {
    // the `app` macro creates a server and allows the user to call
    // the method `run` and await on that future
     app!({
        // if the address is a string literal, it is verified at compile time
        // if it is a string variable, obviously, it won't
         address: "127.0.0.1:3000",
         // via the container we inject our dependencies
         // in this case, MyLogger type
         // any handler that has the trait Logger as an argument
         // will be given MyLogger
         module: make_container => Container,
         bind: [
             {
                 // When a path argument is defined in the route,
                 // the handler is required to have Path<T> as an argument
                 // if not present, it will result in a compilation error
                 route: "/hello_world/{name}",
                 method: Method::GET,
                 // handlers bound with a GET method are not allowed
                 // to request a body(payload) from the request.
                 // Json<T> argument would result in a compilation error
                 handler: hello_world
             },
             {
                 route: "/hello_world/{name}",
                 method: Method::POST,
                 // the POST method allows this handler to have
                 // Json<Name> as an argument
                 handler: do_something
             },
         ],
     })
    .run()
    .await
 }