pavex/router/allowed_methods.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
use http::HeaderValue;
use smallvec::SmallVec;
use crate::http::Method;
/// The set of HTTP methods that are allowed for a given path.
///
/// # Example
///
/// ```rust
/// use pavex::router::AllowedMethods;
/// use pavex::response::Response;
/// use pavex::http::header::{ALLOW, HeaderValue};
/// use itertools::Itertools;
///
/// /// A fallback handler that returns a `404 Not Found` if the request path
/// /// doesn't match any of the registered routes, or a `405 Method Not Allowed`
/// /// if the request path matches a registered route but there is no handler
/// /// for its HTTP method.
/// pub async fn fallback(allowed_methods: AllowedMethods) -> Response {
/// if let Some(header_value) = allowed_methods.allow_header_value() {
/// Response::method_not_allowed()
/// .insert_header(ALLOW, header_value)
/// } else {
/// Response::not_found()
/// }
/// }
/// ```
///
/// # Framework primitive
///
/// `AllowedMethods` is a framework primitive—you don't need to register any constructor
/// with [`Blueprint`] to use it in your application.
///
/// # Use cases
///
/// [`AllowedMethods`] comes into the play when implementing [fallback handlers]: it is necessary
/// to set the `Allow` header to the correct value when returning a `405 Method Not Allowed`
/// response after a routing failure.
///
/// [`Blueprint`]: crate::blueprint::Blueprint
/// [fallback handlers]: crate::blueprint::Blueprint::fallback
#[derive(Debug, Clone)]
pub enum AllowedMethods {
/// Only a finite set of HTTP methods are allowed for a given path.
Some(MethodAllowList),
/// All HTTP methods are allowed for a given path, including custom ones.
All,
}
impl AllowedMethods {
/// The value that should be set for the `Allow` header
/// in a `405 Method Not Allowed` response for this route path.
///
/// It returns `None` if all methods are allowed.
/// It returns the comma-separated list of accepted HTTP methods otherwise.
pub fn allow_header_value(&self) -> Option<HeaderValue> {
match self {
AllowedMethods::Some(m) => m.allow_header_value(),
AllowedMethods::All => None,
}
}
}
#[derive(Debug, Clone)]
/// The variant of [`AllowedMethods`] that only allows a finite set of HTTP methods for a given path.
///
/// Check out [`AllowedMethods`] for more information.
pub struct MethodAllowList {
// We use 5 as our inlining limit because that's going to fit
// all methods in the most common case
// (i.e. `GET`/`POST`/`PUT`/`DELETE`/`PATCH` on a certain route path).
methods: SmallVec<[Method; 5]>,
}
impl MethodAllowList {
/// Create a new instance of [`MethodAllowList`] from an iterator
/// that yields [`Method`]s.
pub fn from_iter(iter: impl IntoIterator<Item = Method>) -> Self {
Self {
methods: SmallVec::from_iter(iter),
}
}
/// Iterate over the allowed methods, returned as a reference.
pub fn iter(&self) -> impl Iterator<Item = &Method> {
self.methods.iter()
}
/// Consume `self` and return an iterator over the allowed methods.
pub fn into_iter(self) -> impl Iterator<Item = Method> {
self.methods.into_iter()
}
/// Get the number of allowed methods.
pub fn len(&self) -> usize {
self.methods.len()
}
/// Check if there are no allowed methods.
pub fn is_empty(&self) -> bool {
self.methods.is_empty()
}
/// The value that should be set for the `Allow` header
/// in a `405 Method Not Allowed` response for this route path.
///
/// It returns `None` if there are no allowed methods.
/// It returns the comma-separated list of allowed methods otherwise.
pub fn allow_header_value(&self) -> Option<HeaderValue> {
if self.methods.is_empty() {
None
} else {
let allow_header = join(&mut self.methods.iter().map(|method| method.as_str()), ",");
Some(
HeaderValue::from_str(&allow_header)
.expect("Failed to assemble `Allow` header value"),
)
}
}
}
// Inlined from `itertools to avoid adding a dependency.
fn join<'a, I>(iter: &mut I, separator: &str) -> String
where
I: Iterator<Item = &'a str>,
{
use std::fmt::Write;
match iter.next() {
None => String::new(),
Some(first_elt) => {
let mut result = String::with_capacity(separator.len() * iter.size_hint().0);
write!(&mut result, "{}", first_elt).unwrap();
iter.for_each(|element| {
result.push_str(separator);
write!(&mut result, "{}", element).unwrap();
});
result
}
}
}
impl From<MethodAllowList> for AllowedMethods {
fn from(methods: MethodAllowList) -> Self {
Self::Some(methods)
}
}