@@ -5,6 +5,55 @@ use matchit::Router;
55use parking_lot:: Mutex ;
66use pyo3:: prelude:: * ;
77
8+ #[ derive( Clone , Copy , Debug , Default , PartialEq , Eq ) ]
9+ pub struct MethodMask ( pub u8 ) ;
10+
11+ impl MethodMask {
12+ pub const GET : u8 = 1 << 0 ;
13+ pub const HEAD : u8 = 1 << 1 ;
14+ pub const POST : u8 = 1 << 2 ;
15+ pub const PUT : u8 = 1 << 3 ;
16+ pub const PATCH : u8 = 1 << 4 ;
17+ pub const DELETE : u8 = 1 << 5 ;
18+ pub const OPTIONS : u8 = 1 << 6 ;
19+
20+ pub fn from_method ( method : & str ) -> Self {
21+ match method {
22+ "GET" => Self ( Self :: GET | Self :: HEAD ) ,
23+ "HEAD" => Self ( Self :: HEAD ) ,
24+ "POST" => Self ( Self :: POST ) ,
25+ "PUT" => Self ( Self :: PUT ) ,
26+ "PATCH" => Self ( Self :: PATCH ) ,
27+ "DELETE" => Self ( Self :: DELETE ) ,
28+ "OPTIONS" => Self ( Self :: OPTIONS ) ,
29+ _ => Self ( 0 ) ,
30+ }
31+ }
32+
33+ pub fn insert_method ( & mut self , method : & str ) {
34+ self . 0 |= Self :: from_method ( method) . 0 ;
35+ }
36+
37+ pub fn to_vec ( self ) -> Vec < String > {
38+ const ORDER : [ ( & str , u8 ) ; 7 ] = [
39+ ( "GET" , MethodMask :: GET ) ,
40+ ( "HEAD" , MethodMask :: HEAD ) ,
41+ ( "POST" , MethodMask :: POST ) ,
42+ ( "PUT" , MethodMask :: PUT ) ,
43+ ( "PATCH" , MethodMask :: PATCH ) ,
44+ ( "DELETE" , MethodMask :: DELETE ) ,
45+ ( "OPTIONS" , MethodMask :: OPTIONS ) ,
46+ ] ;
47+ let mut out = Vec :: with_capacity ( 7 ) ;
48+ for ( name, flag) in ORDER {
49+ if ( self . 0 & flag) != 0 {
50+ out. push ( name. to_string ( ) ) ;
51+ }
52+ }
53+ out
54+ }
55+ }
56+
857/// Immutable route tables built at [`AppState::freeze`](AppState) time so the request
958/// path can be matched without per-method `Mutex` locks (issue #4).
1059pub struct CompiledRouters {
@@ -15,6 +64,7 @@ pub struct CompiledRouters {
1564 pub delete : Router < usize > ,
1665 pub options : Router < usize > ,
1766 pub websocket : Router < usize > ,
67+ pub all_paths : Router < MethodMask > ,
1868}
1969
2070fn router_for_compiled < ' a > ( c : & ' a CompiledRouters , method : & str ) -> Option < & ' a Router < usize > > {
@@ -117,6 +167,8 @@ pub struct AppState {
117167 pub security_headers : Option < Py < PyAny > > ,
118168 /// Global connection pool for the Postgres database.
119169 pub db_pool : Option < sqlx:: PgPool > ,
170+ /// Bitmask of allowed HTTP methods per registered path template.
171+ pub path_method_masks : Mutex < std:: collections:: HashMap < String , MethodMask > > ,
120172}
121173
122174impl AppState {
@@ -146,6 +198,7 @@ impl AppState {
146198 cors : None ,
147199 security_headers : None ,
148200 db_pool : None ,
201+ path_method_masks : Mutex :: new ( std:: collections:: HashMap :: new ( ) ) ,
149202 }
150203 }
151204
@@ -172,6 +225,10 @@ impl AppState {
172225
173226 /// Clone current mutex-protected [`Router`]s into a snapshot (used at freeze / tests).
174227 pub fn snapshot_routers ( & self ) -> CompiledRouters {
228+ let mut all_paths = Router :: new ( ) ;
229+ for ( path, mask) in self . path_method_masks . lock ( ) . iter ( ) {
230+ let _ = all_paths. insert ( path, * mask) ;
231+ }
175232 CompiledRouters {
176233 get : self . get . lock ( ) . clone ( ) ,
177234 post : self . post . lock ( ) . clone ( ) ,
@@ -180,6 +237,7 @@ impl AppState {
180237 delete : self . delete . lock ( ) . clone ( ) ,
181238 options : self . options . lock ( ) . clone ( ) ,
182239 websocket : self . websocket . lock ( ) . clone ( ) ,
240+ all_paths,
183241 }
184242 }
185243}
@@ -235,6 +293,12 @@ pub fn match_route_compiled(
235293
236294/// All HTTP methods that match `path` in a precomputed [`CompiledRouters`] (lock-free 405 list).
237295pub fn methods_matching_path_compiled ( compiled : & CompiledRouters , path : & str ) -> Vec < String > {
296+ if let Ok ( m) = compiled. all_paths . at ( path) {
297+ let v = m. value . to_vec ( ) ;
298+ if !v. is_empty ( ) {
299+ return v;
300+ }
301+ }
238302 const ORDER : [ & str ; 7 ] = [ "GET" , "HEAD" , "POST" , "PUT" , "PATCH" , "DELETE" , "OPTIONS" ] ;
239303 let mut have = [ false ; 7 ] ;
240304 if compiled. get . at ( path) . is_ok ( ) {
@@ -286,77 +350,16 @@ pub fn map_method_router<'a>(
286350/// [1]: https://www.rfc-editor.org/rfc/rfc9110#name-405-method-not-allowed
287351#[ cfg( test) ]
288352fn methods_matching_path ( state : & AppState , path : & str ) -> Vec < String > {
289- const ORDER : [ & str ; 7 ] = [ "GET" , "HEAD" , "POST" , "PUT" , "PATCH" , "DELETE" , "OPTIONS" ] ;
290- let mut have = [ false ; 7 ] ;
291353 if let Some ( c) = & state. compiled {
292- if c. get . at ( path) . is_ok ( ) {
293- have[ 0 ] = true ;
294- have[ 1 ] = true ;
295- }
296- if c. post . at ( path) . is_ok ( ) {
297- have[ 2 ] = true ;
298- }
299- if c. put . at ( path) . is_ok ( ) {
300- have[ 3 ] = true ;
301- }
302- if c. patch . at ( path) . is_ok ( ) {
303- have[ 4 ] = true ;
304- }
305- if c. delete . at ( path) . is_ok ( ) {
306- have[ 5 ] = true ;
307- }
308- if c. options . at ( path) . is_ok ( ) {
309- have[ 6 ] = true ;
310- }
354+ methods_matching_path_compiled ( c, path)
311355 } else {
312- {
313- let g = state. get . lock ( ) ;
314- if g. at ( path) . is_ok ( ) {
315- have[ 0 ] = true ;
316- have[ 1 ] = true ;
317- }
318- }
319- {
320- let r = state. post . lock ( ) ;
321- if r. at ( path) . is_ok ( ) {
322- have[ 2 ] = true ;
323- }
324- }
325- {
326- let r = state. put . lock ( ) ;
327- if r. at ( path) . is_ok ( ) {
328- have[ 3 ] = true ;
329- }
330- }
331- {
332- let r = state. patch . lock ( ) ;
333- if r. at ( path) . is_ok ( ) {
334- have[ 4 ] = true ;
335- }
336- }
337- {
338- let r = state. delete . lock ( ) ;
339- if r. at ( path) . is_ok ( ) {
340- have[ 5 ] = true ;
341- }
342- }
343- {
344- let r = state. options . lock ( ) ;
345- if r. at ( path) . is_ok ( ) {
346- have[ 6 ] = true ;
347- }
348- }
356+ let compiled = state. snapshot_routers ( ) ;
357+ methods_matching_path_compiled ( & compiled, path)
349358 }
350- ORDER
351- . iter ( )
352- . zip ( have)
353- . filter ( |( _, ok) | * ok)
354- . map ( |( m, _) | ( * m) . to_string ( ) )
355- . collect ( )
356359}
357360
358361/// Returns route index and path params, or `None` if the method is unsupported; `Some(None)` if
359- /// no match; `Some(Some)` on success. Uses [`CompiledRouters`] when set (lock-free) .
362+ /// method is valid but path did not match .
360363#[ cfg( test) ]
361364#[ allow( clippy:: type_complexity) ]
362365fn match_route (
@@ -365,14 +368,7 @@ fn match_route(
365368 path : & str ,
366369) -> Option < Option < ( usize , Vec < ( String , String ) > ) > > {
367370 if let Some ( c) = & state. compiled {
368- let g = router_for_compiled ( c, method) ?;
369- return Some ( g. at ( path) . ok ( ) . map ( |m| {
370- let mut pmap = Vec :: new ( ) ;
371- for ( k, v) in m. params . iter ( ) {
372- pmap. push ( ( k. to_string ( ) , v. to_string ( ) ) ) ;
373- }
374- ( * m. value , pmap)
375- } ) ) ;
371+ return match_route_compiled ( c, method, path) ;
376372 }
377373 let g = map_method_router ( state, method) ?;
378374 Some ( g. at ( path) . ok ( ) . map ( |m| {
@@ -414,6 +410,11 @@ mod tests {
414410 fn methods_matching_path_uses_compiled ( ) {
415411 let mut s = AppState :: new ( ) ;
416412 s. post . lock ( ) . insert ( "/x" , 0usize ) . unwrap ( ) ;
413+ s. path_method_masks
414+ . lock ( )
415+ . entry ( "/x" . to_string ( ) )
416+ . or_default ( )
417+ . insert_method ( "POST" ) ;
417418 s. compiled = Some ( Arc :: new ( s. snapshot_routers ( ) ) ) ;
418419 let m = methods_matching_path ( & s, "/x" ) ;
419420 assert_eq ! ( m, vec![ "POST" . to_string( ) ] ) ;
0 commit comments