Getting Started
If you're looking to start writing simple, productive, safe and fast web backends , you've come to the right place.
What This Book Covers
This book aims to be a comprehensive, up-to-date guide to using the web
framework darpi
and the crates associated with it, appropriate for beginners and old hands alike.
A demo app to check out
Let's go ahead!
Why darpi?
We all love how Rust allows us to write fast, safe software.
darpi
attempts at extending that and having stronger invariants proven that are specific to the web.
Performance, safety and simplicity are the main goals the framework is trying to achieve.
Applications built with darpi
have the potential to be much faster and
use fewer resources than other frameworks. To achieve its goals, darpi
utilizes macros, heavily.
The State of Rust web frameworks
While some great web frameworks are out there, there is room for another point of view.
Some of them are either not very flexible, too complicated, pushed compile time errors to runtime by using
the type std::any::Any
or all of the above.
With continuation of the Rust way, we would have to extend the Rust guarantees:
- Outstanding runtime performance for concurrent workloads.
- Flexible, testable and composable software.
- Minimizing runtime errors, as much as possible.
- Simplicity.
Basics
darpi
provides various primitives to build web servers and applications with Rust. It provides routing, middleware, pre-processing of requests, post-processing of responses, etc.
darpi
uses shaku
for statically verifiable dependency injection, since it is alligned with the goals darpi
has.
It is important to note that darpi
does not store dynamic information about the application.
Everything is achieved by code generation. For example, the provided routes are represented by an enum variants used in a match statement.
darpi
solves conflicting paths by sorting, based on different of factors such as number of arguments in
a path and their position within the string.
/user/{name}
and /user/article
are conflicting, if a user's name happens to be article
. Therefore, the more generic path should be matched later.
App
The app
macro generates code that link all the other components together.
#[tokio::main] async fn main() -> Result<(), darpi::Error> { let address = format!("127.0.0.1:{}", 3000); app!({ address: address, container: { factory: make_container(), type: Container }, middleware: { request: [body_size_limit(128), decompress()], response: [] }, jobs: { request: [], response: [first_sync_job, first_sync_job1, first_sync_io_job] }, handlers: [ { route: "/", method: Method::GET, handler: home }, ] }) .run() .await }
Lets break it down.
address
can be either a String
or a &'static str
container
has a factory
function, which is used to create a shaku
container
and a type
(supports arguments too), which is the return type of the factory
.
middleware
, jobs
and handlers
we will tackle in the next chapters.
Handlers
The handler
macro has 3 optional arguments in a json like format.
Trailing commas are not supported as of now.
#![allow(unused)] fn main() { #[handler({ container: Container, middleware: { request: [], response: [] }, jobs: { request: [], response: [] } })] }
A request handler is an async function that accepts zero or more arguments.
The arguments can be provided in several ways, by macro attributes.
Handlers bound by a GET
method do not have access to a request body.
The return type must impl darpi::response::Responder
. For convenience, it is implemented for all common types.
possible arguments
- dependency injection container
#[inject] my_arg: Arc<dyn SomeTrait>
- request
#[request] r: darpi::Request<darpi::Body>
consumes the entire request therefore prevents other attributes from being used except#[path]
#[request_parts] rp: &darpi::RequestParts
#[query] q: MyStruct
whereMyStruct
has to implementserde::Deserialize
. Furthermore, if wrapped in anOption
it is not mandatory.#[path] path: MyStruct
whereMyStruct
has to implementserde::Deserialize
./user/{user_id}/article/{article_id}
will deserialize both ids intoMyStruct
.#[body] data: impl FromRequestBody<T: serde:Deserialize>
if handler is not linked to aGET
request
- middleware
#[middleware::request(i) my_arg: T]
wherei
is a literal index of the middleware linked to the handler#[middleware::response(i) my_arg: T]
does not exist (obviously), because response middleware runs after the handler returns
While i recommend using the macros. If you really want to, you could implement the handler trait.
#![allow(unused)] fn main() { use darpi_middleware::body_size_limit; use darpi::{app, handler, response::Responder, Method, Path, Json, Query}; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, Debug, Path, Query)] pub struct Name { name: String, } #[handler({ middleware: { // roundtrip returns Result<String, Error> // later we can access it via #[middleware::request(0)] request: [roundtrip("blah")] } })] async fn do_something( #[request_parts] _rp: &RequestParts, // the request query is deserialized into Name // if deseriliazation fails, it will result in an error response // to make it optional wrap it in an Option<Name> #[query] query: Name, // the request path is deserialized into Name #[path] path: Name, // the request body is deserialized into the struct Name // it is important to mention that the wrapper around Name // should implement darpi::request::FromRequestBody // Common formats like Json, Xml and Yaml are supported out // of the box but users can implement their own #[body] payload: Json<Name>, // we can access the T from Ok(T) in the middleware result #[middleware::request(0)] m_str: String, // returning a String works because darpi has implemented // the Responder trait for common types ) -> String { format!( "query: {:#?} path: {} body: {} middleware: {}", query, path.name, payload.name, m_str ) } }
An example returning a Response object. For more information visit
#![allow(unused)] fn main() { use darpi::{handler, Body, Response, StatusCode}; #[handler] async fn my_handler() -> Response<Body> { Response::builder() .status(StatusCode::OK) .body(Default::default()) .unwrap() } }
errors
Any error implementing the ResponderError
trait can be used in a handler return type.
In this case, we are using the default implementation of the trait.
use darpi::response::ResponderError;
#[derive(Display, Debug)]
pub enum Error {
#[display(fmt = "wrong credentials")]
WrongCredentialsError,
#[display(fmt = "jwt token not valid")]
JWTTokenError,
#[display(fmt = "jwt token creation error")]
JWTTokenCreationError,
#[display(fmt = "no auth header")]
NoAuthHeaderError,
#[display(fmt = "invalid auth header")]
InvalidAuthHeaderError,
#[display(fmt = "no permission")]
NoPermissionError,
}
impl ResponderError for Error {}
#[derive(Deserialize, Serialize, Debug)]
pub struct Login {
email: String,
password: String,
}
#[handler({
container: Container
})]
async fn login(
#[body] data: Json<Login>,
#[inject] jwt_tok_creator: Arc<dyn JwtTokenCreator>,
) -> Result<Token, Error> {
//verify user data
let admin = Role::Admin; // hardcoded just for the example
let uid = "uid"; // hardcoded just for the example
let tok = jwt_tok_creator.create(uid, &admin).await?;
Ok(tok)
}
middleware
darpiās middleware system allows us to add additional behavior to request/response processing. Middleware can hook into an incoming request process, enabling us to halt request processing to return a response early and modify the request body. Middleware can also hook into response processing.
middleware types
- global (app)
- Request
- Response
- local (handler)
- Request
- Response
possible arguments
-
Request middleware
- dependency injection container
#[inject] my_arg: Arc<dyn SomeTrait>
- request
#[request] rp: &mut darpi::Request<darpi::Body>
supports shared and mutable reference
- handler
#[handler] arg: T
where the value must be provided when the middleware is invoked
- dependency injection container
-
Response middleware
- dependency injection container
#[inject] my_arg: Arc<dyn SomeTrait>
- response
#[response] r: &mut darpi::Response<Body>
supports shared and mutable reference
- handler
#[handler] arg: T
where the value must be provided when the middleware is invoked
- dependency injection container
While i recommend using the macros. If you really want to, you could implement the middleware request and response traits.
#![allow(unused)] fn main() { use darpi::{middleware, request::PayloadError, Body, HttpBody}; #[middleware(Request)] pub async fn body_size_limit( #[request] r: &Request<Body>, #[handler] size: u64, ) -> Result<(), PayloadError> { if let Some(limit) = r.size_hint().upper() { if size < limit { return Err(PayloadError::Size(size, limit)); } } Ok(()) } }
a more complicated example
#![allow(unused)] fn main() { #[middleware(Request)] pub async fn authorize( #[handler] role: impl UserRole, #[request] rp: &Request<Body>, #[inject] algo_provider: Arc<dyn JwtAlgorithmProvider>, #[inject] token_ext: Arc<dyn TokenExtractor>, #[inject] secret_provider: Arc<dyn JwtSecretProvider>, ) -> Result<Claims, Error> { let token_res = token_ext.extract(&rp).await; match token_res { Ok(jwt) => { let decoded = decode::<Claims>( &jwt, secret_provider.decoding_key().await, &Validation::new(algo_provider.algorithm().await), ).map_err(|_| Error::JWTTokenError)?; if !role.is_authorized(&decoded.claims) { return Err(Error::NoPermissionError); } Ok(decoded.claims) } Err(e) => return Err(e), } } }
Jobs
Jobs
represent some work packaged up in a function that the application
has to execute but it's not necessary to hold up the response to the user.
Just like middleware
, jobs
are bound to a Request
or a Response
and
utilize the same argument system as middleware
.
We have 3 types of jobs
based on their behaviour.
This is necessary to have an optimal way of using our resources.
- Job
Future
that does not block and does not perform any significant intensive work . It is queued on the regular tokio runtime.CpuBound
is a type of function that would do heavy computation and does not benefit from the tokio runtime. We would execute those on theRayon
runtime that is optimized for that.IOBlocking
is suitable for various kinds of IO operations that cannot be performed asynchronously. It is executed on a separate tokio blocking threads.
While i recommend using the macros. If you really want to, you could implement the Job factory request and response traits.
#![allow(unused)] fn main() { pub enum Job<T = ()> { Future(FutureJob<T>), CpuBound(CpuJob<T>), IOBlocking(IOBlockingJob<T>), } pub struct FutureJob<T = ()>(Pin<Box<dyn Future<Output = T> + Send>>); pub struct CpuJob<T = ()>(Box<dyn FnOnce() -> T + Send>); pub struct IOBlockingJob<T = ()>(Box<dyn FnOnce() -> T + Send>); }
Few short examples.
#![allow(unused)] fn main() { #[job_factory(Request)] async fn first_async_job() -> FutureJob { async { println!("first job in the background.") }.into() } }
// blocking here is ok!
#![allow(unused)] fn main() { #[job_factory(Response)] async fn first_sync_job(#[response] r: &Response<Body>) -> IOBlockingJob { let status_code = r.status(); let job = move || { std::thread::sleep(std::time::Duration::from_secs(2)); println!( "first_sync_job in the background for a request with status {}", status_code ); }; job.into() } }
#![allow(unused)] fn main() { #[job_factory(Response)] async fn my_heavy_computation() -> CpuJob { let job = || { for _ in 0..100 { let mut r = 0; for _ in 0..10000000 { r += 1; } println!("my_heavy_computation runs in the background. {}", r); } }; job.into() } }
Web sockets
#![allow(unused)] fn main() { use darpi::futures::{SinkExt, StreamExt}; use darpi::{app, handler, job::FutureJob, response::UpgradeWS, Body, Method, Request}; use tokio_tungstenite::{tungstenite::protocol::Role, WebSocketStream}; #[handler] async fn hello_world(#[request] r: Request<Body>) -> Result<UpgradeWS, String> { let resp = UpgradeWS::from_header(r.headers()) .ok_or("missing SEC_WEBSOCKET_KEY header".to_string())?; FutureJob::from(async move { let upgraded = darpi::upgrade::on(r).await.unwrap(); let mut ws_stream = WebSocketStream::from_raw_socket(upgraded, Role::Server, None).await; while let Some(msg) = ws_stream.next().await { let msg = match msg { Ok(m) => m, Err(e) => { println!("error trying to receive: `{:#?}`", e); return; } }; if msg.is_text() || msg.is_binary() { println!("received a message `{}`", msg); if let Err(e) = ws_stream.send(msg).await { println!("error trying to send: `{:#?}`", e); return; } } else if msg.is_close() { println!("closing websocket"); return; } } }) .spawn() .map_err(|e| format!("{}", e))?; Ok(resp) } }
Advanced
In this chapter, we will learn how we can extend the framework with our own custom implementations.
Extractors
As we learned from the Handlers chapter, we can get a request body by declaring #[body] payload: Json<Name>
in the handler arguments.
Extractors are a way to get a request body in a certain format.
While the framework provides Json
, Xml
and Yaml
extractor implementations, you might need to implement your own.
To do that, we need to implement the FromRequestBody
or FromRequestBodyWithContainer
trait, depending on whether we need to use the dependency injection container.
Lets start with FromRequestBody
. The trait definition is as follows.
As you can see, we have 2 generic types.
If in any case, an error is returned from the trait methods, the request processing will immediately stop and an error response will be returned to the client.
This is the reason the E
type has ResponderError
bounds.
#![allow(unused)] fn main() { #[async_trait] pub trait FromRequestBody<T, E> where T: de::DeserializeOwned + 'static, E: ResponderError + 'static, { async fn assert_content_type(_content_type: Option<&HeaderValue>) -> Result<(), E> { Ok(()) } async fn extract(headers: &HeaderMap, b: Body) -> Result<T, E>; } }
As the implementor, we need to decide whether we want to require the content type header or not.
You can choose to not have it as a requirement from the client, then you can simply not implement it and use the default implementation.
And finally, the extract
method is the thing that does most of the work.
You can very well imagine that a basic implementation of the would be as follows.
#![allow(unused)] fn main() { use darpi::hyper::body; pub struct MyFormat<T>(pub T); #[async_trait] impl<T> FromRequestBody<MyFormat<T>, MyErr> for MyFormat<T> where T: DeserializeOwned + 'static, { async fn assert_content_type(content_type: Option<&HeaderValue>) -> Result<(), MyErr> { if let Some(hv) = content_type { if hv != "application/my_format" { return Err(MyErr::InvalidContentType); } return Ok(()); } Err(MyErr::MissingContentType) } async fn extract(_: &HeaderMap, b: Body) -> Result<MyFormat<T>, MyErr> { let full_body = body::to_bytes(b).await?; // here is where we need to deserialize the full_body bytes // for the example, serde_json is used. let ser: T = serde_json::from_slice(&full_body)?; Ok(MyFormat(ser)) } } #[derive(Display)] pub enum MyErr { ReadBody(hyper::Error), Serde(Error), InvalidContentType, MissingContentType, } impl From<Error> for MyErr { fn from(e: Error) -> Self { Self::Serde(e) } } impl From<hyper::Error> for MyErr { fn from(e: hyper::Error) -> Self { Self::ReadBody(e) } } impl ResponderError for MyErr {} }
If you have dependencies that you need to access, you can instead implement FromRequestBodyWithContainer
.
The implementation of the methods is exactly the same as above. The only difference is the C
generic type that you need to specify the relevant bounds.
#![allow(unused)] fn main() { #[async_trait] impl<F, T, E, C> FromRequestBodyWithContainer<T, E, C> for F where F: FromRequestBody<T, E> + 'static, T: de::DeserializeOwned + 'static, E: ResponderError + 'static, C: std::any::Any + Sync + Send, { async fn assert_content_type(content_type: Option<&HeaderValue>, _: Arc<C>) -> Result<(), E> { F::assert_content_type(content_type).await } async fn extract(headers: &HeaderMap<HeaderValue>, b: Body, _: Arc<C>) -> Result<T, E> { F::extract(headers, b).await } } }
As an example, we can see the graphql
integration implements this trait.
As you can see the line let opts = container.resolve().get();
is how you can fetch the specified dependency.
#![allow(unused)] fn main() { pub trait MultipartOptionsProvider: Interface { fn get(&self) -> MultipartOptions; } #[derive(Component)] #[shaku(interface = MultipartOptionsProvider)] pub struct MultipartOptionsProviderImpl { opts: MultipartOptions, } impl MultipartOptionsProvider for MultipartOptionsProviderImpl { fn get(&self) -> MultipartOptions { self.opts.clone() } } #[async_trait] impl<C: 'static> FromRequestBodyWithContainer<GraphQLBody<BatchRequest>, GraphQLError, C> for GraphQLBody<BatchRequest> where C: HasComponent<dyn MultipartOptionsProvider>, { async fn extract( headers: &HeaderMap, mut body: darpi::Body, container: Arc<C>, ) -> Result<GraphQLBody<BatchRequest>, GraphQLError> { let content_type = headers .get(http::header::CONTENT_TYPE) .and_then(|value| value.to_str().ok()) .map(|value| value.to_string()); let (tx, rx): ( Sender<std::result::Result<Bytes, _>>, Receiver<std::result::Result<Bytes, _>>, ) = bounded(16); darpi::job::FutureJob::from(async move { while let Some(item) = body.next().await { if tx.send(item).await.is_err() { return; } } }) .spawn() .map_err(|e| GraphQLError::Send(e.to_string()))?; let opts = container.resolve().get(); Ok(GraphQLBody(BatchRequest( async_graphql::http::receive_batch_body( content_type, rx.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e)) .into_async_read(), opts, ) .await .map_err(|e| GraphQLError::ParseRequest(e))?, ))) } } }