Skip to content

Commit dbc942a

Browse files
support wildcard matching
1 parent 90077ba commit dbc942a

File tree

1 file changed

+217
-8
lines changed

1 file changed

+217
-8
lines changed

crates/common/src/integrations/registry.rs

Lines changed: 217 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,36 @@ impl IntegrationRegistry {
292292
}
293293
}
294294

295+
fn find_route(&self, method: &Method, path: &str) -> Option<&RouteValue> {
296+
// First try exact match
297+
let key = (method.clone(), path.to_string());
298+
if let Some(route_value) = self.inner.route_map.get(&key) {
299+
return Some(route_value);
300+
}
301+
302+
// If no exact match, try wildcard matching
303+
// Routes ending with /* should match any path with that prefix + additional segments
304+
for ((route_method, route_path), route_value) in &self.inner.route_map {
305+
if route_method != method {
306+
continue;
307+
}
308+
309+
if let Some(prefix) = route_path.strip_suffix("/*") {
310+
if path.starts_with(prefix)
311+
&& path.len() > prefix.len()
312+
&& path[prefix.len()..].starts_with('/')
313+
{
314+
return Some(route_value);
315+
}
316+
}
317+
}
318+
319+
None
320+
}
321+
295322
/// Return true when any proxy is registered for the provided route.
296323
pub fn has_route(&self, method: &Method, path: &str) -> bool {
297-
self.inner
298-
.route_map
299-
.contains_key(&(method.clone(), path.to_string()))
324+
self.find_route(method, path).is_some()
300325
}
301326

302327
/// Dispatch a proxy request when an integration handles the path.
@@ -307,11 +332,7 @@ impl IntegrationRegistry {
307332
settings: &Settings,
308333
req: Request,
309334
) -> Option<Result<Response, Report<TrustedServerError>>> {
310-
if let Some((proxy, _)) = self
311-
.inner
312-
.route_map
313-
.get(&(method.clone(), path.to_string()))
314-
{
335+
if let Some((proxy, _)) = self.find_route(method, path) {
315336
Some(proxy.handle(settings, req).await)
316337
} else {
317338
None
@@ -399,4 +420,192 @@ impl IntegrationRegistry {
399420
}),
400421
}
401422
}
423+
424+
#[cfg(test)]
425+
pub fn from_routes(routes: HashMap<RouteKey, RouteValue>) -> Self {
426+
Self {
427+
inner: Arc::new(IntegrationRegistryInner {
428+
route_map: routes,
429+
routes: Vec::new(),
430+
html_rewriters: Vec::new(),
431+
script_rewriters: Vec::new(),
432+
}),
433+
}
434+
}
435+
}
436+
437+
#[cfg(test)]
438+
mod tests {
439+
use super::*;
440+
441+
// Mock integration proxy for testing
442+
struct MockProxy;
443+
444+
#[async_trait(?Send)]
445+
impl IntegrationProxy for MockProxy {
446+
fn routes(&self) -> Vec<IntegrationEndpoint> {
447+
vec![]
448+
}
449+
450+
async fn handle(
451+
&self,
452+
_settings: &Settings,
453+
_req: Request,
454+
) -> Result<Response, Report<TrustedServerError>> {
455+
Ok(Response::new())
456+
}
457+
}
458+
459+
#[test]
460+
fn test_exact_route_matching() {
461+
let mut routes = HashMap::new();
462+
routes.insert(
463+
(Method::GET, "/integrations/test/exact".to_string()),
464+
(Arc::new(MockProxy) as Arc<dyn IntegrationProxy>, "test"),
465+
);
466+
467+
let registry = IntegrationRegistry::from_routes(routes);
468+
469+
// Should match exact route
470+
assert!(registry.has_route(&Method::GET, "/integrations/test/exact"));
471+
472+
// Should not match different paths
473+
assert!(!registry.has_route(&Method::GET, "/integrations/test/other"));
474+
assert!(!registry.has_route(&Method::GET, "/integrations/test/exact/nested"));
475+
476+
// Should not match different methods
477+
assert!(!registry.has_route(&Method::POST, "/integrations/test/exact"));
478+
}
479+
480+
#[test]
481+
fn test_wildcard_route_matching() {
482+
let mut routes = HashMap::new();
483+
routes.insert(
484+
(Method::GET, "/integrations/lockr/api/*".to_string()),
485+
(Arc::new(MockProxy) as Arc<dyn IntegrationProxy>, "lockr"),
486+
);
487+
488+
let registry = IntegrationRegistry::from_routes(routes);
489+
490+
// Should match paths under the wildcard prefix
491+
assert!(registry.has_route(&Method::GET, "/integrations/lockr/api/settings"));
492+
assert!(registry.has_route(
493+
&Method::GET,
494+
"/integrations/lockr/api/publisher/app/v1/identityLockr/settings"
495+
));
496+
assert!(registry.has_route(&Method::GET, "/integrations/lockr/api/page-view"));
497+
assert!(registry.has_route(&Method::GET, "/integrations/lockr/api/a/b/c/d/e"));
498+
499+
// Should not match paths that don't start with the prefix
500+
assert!(!registry.has_route(&Method::GET, "/integrations/lockr/sdk"));
501+
assert!(!registry.has_route(&Method::GET, "/integrations/lockr/other"));
502+
assert!(!registry.has_route(&Method::GET, "/integrations/other/api/settings"));
503+
504+
// Should not match different methods
505+
assert!(!registry.has_route(&Method::POST, "/integrations/lockr/api/settings"));
506+
}
507+
508+
#[test]
509+
fn test_wildcard_and_exact_routes_coexist() {
510+
let mut routes = HashMap::new();
511+
routes.insert(
512+
(Method::GET, "/integrations/test/api/*".to_string()),
513+
(Arc::new(MockProxy) as Arc<dyn IntegrationProxy>, "test"),
514+
);
515+
routes.insert(
516+
(Method::GET, "/integrations/test/exact".to_string()),
517+
(Arc::new(MockProxy) as Arc<dyn IntegrationProxy>, "test"),
518+
);
519+
520+
let registry = IntegrationRegistry::from_routes(routes);
521+
522+
// Exact route should match
523+
assert!(registry.has_route(&Method::GET, "/integrations/test/exact"));
524+
525+
// Wildcard routes should match
526+
assert!(registry.has_route(&Method::GET, "/integrations/test/api/anything"));
527+
assert!(registry.has_route(&Method::GET, "/integrations/test/api/nested/path"));
528+
529+
// Non-matching should fail
530+
assert!(!registry.has_route(&Method::GET, "/integrations/test/other"));
531+
}
532+
533+
#[test]
534+
fn test_multiple_wildcard_routes() {
535+
let mut routes = HashMap::new();
536+
routes.insert(
537+
(Method::GET, "/integrations/lockr/api/*".to_string()),
538+
(Arc::new(MockProxy) as Arc<dyn IntegrationProxy>, "lockr"),
539+
);
540+
routes.insert(
541+
(Method::POST, "/integrations/lockr/api/*".to_string()),
542+
(Arc::new(MockProxy) as Arc<dyn IntegrationProxy>, "lockr"),
543+
);
544+
routes.insert(
545+
(Method::GET, "/integrations/testlight/api/*".to_string()),
546+
(
547+
Arc::new(MockProxy) as Arc<dyn IntegrationProxy>,
548+
"testlight",
549+
),
550+
);
551+
552+
let registry = IntegrationRegistry::from_routes(routes);
553+
554+
// Lockr GET routes should match
555+
assert!(registry.has_route(&Method::GET, "/integrations/lockr/api/settings"));
556+
557+
// Lockr POST routes should match
558+
assert!(registry.has_route(&Method::POST, "/integrations/lockr/api/settings"));
559+
560+
// Testlight routes should match
561+
assert!(registry.has_route(&Method::GET, "/integrations/testlight/api/auction"));
562+
assert!(registry.has_route(&Method::GET, "/integrations/testlight/api/any-path"));
563+
564+
// Cross-integration paths should not match
565+
assert!(!registry.has_route(&Method::GET, "/integrations/lockr/other-endpoint"));
566+
assert!(!registry.has_route(&Method::GET, "/integrations/other/api/test"));
567+
}
568+
569+
#[test]
570+
fn test_wildcard_preserves_casing() {
571+
let mut routes = HashMap::new();
572+
routes.insert(
573+
(Method::GET, "/integrations/lockr/api/*".to_string()),
574+
(Arc::new(MockProxy) as Arc<dyn IntegrationProxy>, "lockr"),
575+
);
576+
577+
let registry = IntegrationRegistry::from_routes(routes);
578+
579+
// Should match with camelCase preserved
580+
assert!(registry.has_route(
581+
&Method::GET,
582+
"/integrations/lockr/api/publisher/app/v1/identityLockr/settings"
583+
));
584+
assert!(registry.has_route(
585+
&Method::GET,
586+
"/integrations/lockr/api/publisher/app/v1/identitylockr/settings"
587+
));
588+
}
589+
590+
#[test]
591+
fn test_wildcard_edge_cases() {
592+
let mut routes = HashMap::new();
593+
routes.insert(
594+
(Method::GET, "/api/*".to_string()),
595+
(Arc::new(MockProxy) as Arc<dyn IntegrationProxy>, "test"),
596+
);
597+
598+
let registry = IntegrationRegistry::from_routes(routes);
599+
600+
// Should match paths under /api/
601+
assert!(registry.has_route(&Method::GET, "/api/v1"));
602+
assert!(registry.has_route(&Method::GET, "/api/v1/users"));
603+
604+
// Should not match /api without trailing content
605+
// The current implementation requires a / after the prefix
606+
assert!(!registry.has_route(&Method::GET, "/api"));
607+
608+
// Should not match partial prefix matches
609+
assert!(!registry.has_route(&Method::GET, "/apiv1"));
610+
}
402611
}

0 commit comments

Comments
 (0)