blob: 429d7699a54ffce3251852e98478a493d03a0335 [file] [log] [blame]
# Copyright (c) 2025 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any, Dict, List, Optional
from urllib.parse import parse_qs
from route_configuration import Route
def match_route(routing: List[Route], method: str, path: str, query: Optional[Dict[str, Any] | str] = None) -> Optional[Route]:
"""
Finds the best matching route configuration for an incoming HTTP request.
Evaluates incoming requests against a list of routes based on three criteria
in order of priority:
1. HTTP method (e.g., GET, POST, PUT, DELETE)
2. URL path pattern (including wildcard support)
3. Query parameters (if specified in route configuration)
Args:
routing (List[Route]): List of available route configurations
method (str): HTTP method from the incoming request
path (str): URL path from the incoming request
query (Optional[Dict[str, Any] | str]): Query parameters from the request,
either as a dictionary or URL-encoded string. Defaults to None.
Returns:
Optional[Route]: The first matching Route object that satisfies all criteria,
or None if no match is found. When multiple routes match, returns the
first matching route in the original routing list order.
Examples:
# Available routes
routes = [
Route(method="GET", path="/api/device/*", query=None),
Route(method="GET", path="/api/device/status", query={"type": "sensor"})
]
# These would match:
match_route(routes, "GET", "/api/device/123") # Returns first route
match_route(routes, "GET", "/api/device/status", {"type": "sensor"}) # Returns second route
# These would return None:
match_route(routes, "POST", "/api/device/123")
match_route(routes, "GET", "/api/other")
Note:
- Routes are evaluated in order, returning the first match
- Wildcard paths (ending in *) match any path with the specified prefix
- Query parameters must match exactly if specified in the route
- Method matching is case-sensitive
- Path matching is case-sensitive
- Query strings are automatically parsed into dictionaries
"""
# Filter routes to only those matching the HTTP method
method_routes: List[Route] = []
for route in routing:
if route.method == method:
method_routes.append(route)
if not method_routes:
return None
# Filter routes to only those matching the path
path_routes: List[Route] = []
for route in method_routes:
if route.path == path:
path_routes.append(route)
elif route.path.endswith("*"):
# Handle wildcard paths
if path.startswith(route.path[:-1]):
path_routes.append(route)
if not path_routes:
return None
# Parse query parameters if present
query_params = {}
if query:
if isinstance(query, str):
# parse_qs returns values as lists, we'll take the first value for each parameter
parsed = parse_qs(query)
query_params = {k: v[0] if v else "" for k, v in parsed.items()}
else:
query_params = query
# Find the first route that matches the path and query parameters
for route in path_routes:
if not route.query:
return route
# Check if all required query parameters are present
if all(param in query_params for param in route.query.params):
return route
return None