routerify icon indicating copy to clipboard operation
routerify copied to clipboard

Add some more utility methods to RequestExt

Open rousan opened this issue 6 years ago • 1 comments

Reference methods: https://docs.rs/reqwest/0.10.4/reqwest/struct.Response.html#method.cookies

including .cookies().

rousan avatar May 09 '20 20:05 rousan

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,
}

seanpianka avatar Jun 14 '21 00:06 seanpianka