routerify
routerify copied to clipboard
Add some more utility methods to RequestExt
Reference methods: https://docs.rs/reqwest/0.10.4/reqwest/struct.Response.html#method.cookies
including .cookies().
Here are some utility methods for hyper::Body<hyper::Request> that I've ended up using in endpoint/handler functions before:
use std::fmt::Display;
use std::str::FromStr;
use hyper::{Body, Request, Response, StatusCode};
use routerify::prelude::RequestExt;
use routerify_query::RequestQueryExt;
use serde::de::DeserializeOwned;
use error::HandlerError;
#[derive(Clone, Debug, thiserror::Error)]
#[async_trait::async_trait]
trait RouterRequestExt {
/// Extract a query parameter from the request URI, and error if not provided.
fn required_query(&self, name: &'static str) -> Result<&str, HandlerError>;
/// Extract a required query parameter from the request URI, and fallibly parse
/// the value into a specified type.
fn required_query_as<T>(&self, name: &'static str) -> Result<T, HandlerError>
where
T: FromStr,
<T as FromStr>::Err: Display;
/// Extract an optional query parameter from the request URI.
fn optional_query(&self, name: &'static str) -> Option<&str>;
/// Extract a query parameter from the request URI, and fallibly parse the
/// value into a specified type.
fn optional_query_as<T>(&self, name: &'static str) -> Result<Option<T>, HandlerError>
where
T: FromStr,
<T as FromStr>::Err: Display;
/// Extract a URL parameter from the request URI, and error if not provided.
fn required_param(&self, name: &'static str) -> Result<&str, HandlerError>;
/// Extract a required URL parameter from the request URI, and attempt to
/// parse the value into a generic type.
fn required_param_as<T>(&self, name: &'static str) -> Result<T, HandlerError>
where
T: FromStr,
<T as FromStr>::Err: Display;
/// Extract an optional URL parameter from the request URI.
fn optional_param(&self, name: &'static str) -> Option<&str>;
/// Extract a URL parameter from the request URI, and fallibly parse the
/// value into a specified type.
fn optional_param_as<T>(&self, name: &'static str) -> Result<Option<T>, HandlerError>
where
T: FromStr,
<T as FromStr>::Err: Display;
/// Retrieve an already initialized singleton containing scope state, and
/// error if a singleton with the specified typeid does not exist.
fn state<S: Send + Sync + 'static>(&self) -> Result<&S, HandlerError>;
/// Attempt to deserialize the body of the request into a specific type.
async fn request_body<T: DeserializeOwned>(&mut self) -> Result<T, HandlerError>;
/// Retrieve a non-empty string containing the value of the bearer token.
///
/// This will error when:
/// 1. The "Authorization" header is not supplied.
/// 2. The authentication type is not "Bearer".
/// 2. The authentication credentials is empty.
async fn bearer_token(&self) -> Result<&str, HandlerError>;
}
They work to simplify common use-cases for parsing request input data and appropriate handling errors throughout, rather than .unwrap()'ing everywhere and potentially causing a panic. e.g.
/// An endpoint for authorized users to create a new product for a certain category.
async fn create_product(mut req: Request<Body>) -> Result<Response<Body>, HandlerError> {
#[derive(Deserialize, Debug)]
struct Request {
name: String,
image_url: String,
category_id: String,
}
#[derive(Serialize)]
struct Response {
product_id: String,
}
let access_token = req.bearer_token().await?;
let body = req.request_body::<Request>().await?;
let state = req.state::<State>()?;
let command = products::commands::CreateProduct {
name: body.name,
image_url: body.image_url,
category_id: body.category_id,
access_token,
};
match state.write_service.lock().await.create_product(command).await {
Ok(product_id) => {
let res = serde_json::to_string(&Response { product_id }).unwrap();
Ok(created(res)) // Helper function to create 201 CREATED
}
Err(e) => Err(e.into()), // ApplicationError into HandlerError
}
}
/// An endpoint for authorized users to search for products in a certain category.
///
/// The length of the results can be configured.
async fn find_products(req: Request<Body>) -> Result<Response<Body>, HandlerError> {
#[derive(Serialize)]
struct Response {
products: Vec<products::ProductReadModel>,
}
let access_token = req.bearer_token().await?;
let category_id = req.required_param("category_id")?;
let product_search_query = req.required_query("search_query")?;
let product_list_limit = req.optional_query_as::<i64>("limit")?;
let query = products::queries::FindProducts {
store_id,
product_search_query,
product_list_limit,
access_token,
};
let state = req.state::<State>()?;
match state.read_service.find_products(query).await {
Ok(products) => {
let resp = ok(serde_json::to_string(&Response { products }).unwrap()); // Helper function to create 201 CREATED
Ok(resp)
}
Err(e) => return Err(e.into()), // ApplicationError into HandlerError
}
}
I can provide the impl RouterRequestExt for hyper::Request<hyper::Body> if there's interest in adding these methods. The implementation requires the use of an error type for the Router that is similar to the following definition:
#[derive(Clone, Debug, Error)]
pub enum HandlerError {
// 400
#[error("A required URL argument was not specified for this request: {name}")]
MissingRequiredUrlArgument { name: &'static str },
#[error("A URL argument was not specified with the correct type: {name} failed with {cause}")]
InvalidUrlArgumentArgumentType { name: &'static str, cause: String },
#[error("A required query parameter was not specified for this request: {name}")]
MissingRequiredQueryParameter { name: &'static str },
#[error("A query parameter was not specified with the correct type: {name} failed with {cause}")]
InvalidQueryParameterType { name: &'static str, cause: String },
#[error("The body of the request is invalid: {cause}")]
InvalidBody { cause: String },
#[error("A required HTTP header was not specified: {name}")]
MissingRequiredHeader { name: &'static str },
#[error("The value provided for one of the HTTP headers was not in the correct format: {name}")]
InvalidHeaderValue { name: &'static str },
#[error("The request was rejected due to bad input from the client: {cause}")]
BadRequest { cause: String },
// 401
#[error("Server failed to authenticate the request.")]
Unauthorized { cause: String },
// 403
#[error("The agent does not have sufficient permissions to execute this operation.")]
Forbidden { cause: String },
// 500
#[error("The server encountered an internal error. Please retry the request.")]
InternalServerError { cause: String },
#[error("The server encountered an internal error. Please retry the request.")]
RouterMissingHandlerState,
}