Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add some more utility methods to RequestExt #13

Open
rousan opened this issue May 9, 2020 · 1 comment
Open

Add some more utility methods to RequestExt #13

rousan opened this issue May 9, 2020 · 1 comment
Labels
help wanted Extra attention is needed

Comments

@rousan
Copy link
Member

rousan commented May 9, 2020

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

including .cookies().

@seanpianka
Copy link
Member

seanpianka commented Jun 14, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

3 participants