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}