Skip to content

Commit ad7ccc1

Browse files
support wildcard matching
1 parent 85c4bde commit ad7ccc1

File tree

1 file changed

+215
-8
lines changed

1 file changed

+215
-8
lines changed

crates/common/src/integrations/registry.rs

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

295+
/// Find a matching route, supporting wildcard patterns (routes ending with /*).
296+
fn find_route(&self, method: &Method, path: &str) -> Option<&RouteValue> {
297+
// First try exact match
298+
let key = (method.clone(), path.to_string());
299+
if let Some(route_value) = self.inner.route_map.get(&key) {
300+
return Some(route_value);
301+
}
302+
303+
// If no exact match, try wildcard matching
304+
// Routes ending with /* should match any path with that prefix + additional segments
305+
for ((route_method, route_path), route_value) in &self.inner.route_map {
306+
if route_method != method {
307+
continue;
308+
}
309+
310+
// Check if this is a wildcard route (ends with /*)
311+
if let Some(prefix) = route_path.strip_suffix("/*") {
312+
// Match if the incoming path starts with the prefix followed by /
313+
// This ensures /api/* matches /api/v1 but not /api or /apiv1
314+
if path.starts_with(prefix) && path.len() > prefix.len() && path[prefix.len()..].starts_with('/') {
315+
return Some(route_value);
316+
}
317+
}
318+
}
319+
320+
None
321+
}
322+
295323
/// Return true when any proxy is registered for the provided route.
296324
pub fn has_route(&self, method: &Method, path: &str) -> bool {
297-
self.inner
298-
.route_map
299-
.contains_key(&(method.clone(), path.to_string()))
325+
self.find_route(method, path).is_some()
300326
}
301327

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

0 commit comments

Comments
 (0)