pavex/router/
allowed_methods.rs

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