Skip to content

Commit 023cfc1

Browse files
Adding routing script
1 parent 0859db8 commit 023cfc1

File tree

2 files changed

+76
-1
lines changed

2 files changed

+76
-1
lines changed

aws_lambda_powertools/event_handler/events_appsync/_registry.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import logging
44
from typing import Any, Callable
55

6+
from aws_lambda_powertools.event_handler.events_appsync.functions import find_best_route
7+
68
logger = logging.getLogger(__name__)
79

810

@@ -55,7 +57,7 @@ def find_resolver(self, path: str) -> dict | None:
5557
A dictionary with the resolver and if raise exception on error
5658
"""
5759
logger.debug(f"Looking for resolver for path={path}")
58-
return self.resolvers.get(f"{path}")
60+
return self.resolvers.get(find_best_route(self.resolvers, path))
5961

6062
def merge(self, other_registry: ResolverEventsRegistry):
6163
"""Update current registry with incoming registry
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from functools import lru_cache
5+
from typing import Any
6+
7+
8+
def find_best_route(routes: dict[str, Any], path: str):
9+
"""
10+
Find the most specific matching route for a given path.
11+
12+
Examples of matches:
13+
Route: /default/v1/* Path: /default/v1/users -> MATCH
14+
Route: /default/v1/* Path: /default/v1/users/students -> MATCH
15+
Route: /default/v1/users/* Path: /default/v1/users/123 -> MATCH (this wins over /default/v1/*)
16+
Route: /* Path: /anything/here -> MATCH (lowest priority)
17+
18+
Parameters
19+
----------
20+
routes: dict[str, Any]
21+
Dictionary containing routes and their handlers
22+
Format: {
23+
'resolvers': {
24+
'/path/*': {'func': callable, 'aggregate': bool},
25+
'/path/specific/*': {'func': callable, 'aggregate': bool}
26+
}
27+
}
28+
path: str
29+
Actual path to match (e.g., '/default/v1/users')
30+
31+
Returns
32+
-------
33+
str: Most specific matching route or None if no match
34+
"""
35+
@lru_cache(maxsize=1024)
36+
def pattern_to_regex(route):
37+
"""
38+
Convert a route pattern to a regex pattern with caching.
39+
Examples:
40+
/default/v1/* -> ^/default/v1/[^/]+$
41+
/default/v1/users/* -> ^/default/v1/users/.*$
42+
43+
Parameters
44+
----------
45+
route (str): Route pattern with wildcards
46+
47+
Returns
48+
-------
49+
Pattern: Compiled regex pattern
50+
"""
51+
# Escape special regex chars but convert * to regex pattern
52+
pattern = re.escape(route).replace("\\*", "[^/]+")
53+
54+
# If pattern ends with [^/]+, replace with .* for multi-segment match
55+
if pattern.endswith("[^/]+"):
56+
pattern = pattern[:-6] + ".*"
57+
58+
# Compile and return the regex pattern
59+
return re.compile(f"^{pattern}$")
60+
61+
# Find all matching routes
62+
matches = [
63+
route for route in routes.keys()
64+
if pattern_to_regex(route).match(path)
65+
]
66+
67+
# Return the most specific route (longest length minus wildcards)
68+
# Examples of specificity:
69+
# - '/default/v1/users' -> score: 14 (len=14, wildcards=0)
70+
# - '/default/v1/users/*' -> score: 14 (len=15, wildcards=1)
71+
# - '/default/v1/*' -> score: 8 (len=9, wildcards=1)
72+
# - '/*' -> score: 0 (len=2, wildcards=1)
73+
return max(matches, key=lambda x: len(x) - x.count("*"), default=None)

0 commit comments

Comments
 (0)