diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs
index ab837a27b..243a1d147 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs
@@ -24,19 +24,4 @@ public class ApiGatewayRouteConfig
/// The API Gateway HTTP Path of the Lambda function
///
public required string Path { get; set; }
-
- ///
- /// The type of API Gateway Route. This is used to determine the priority of the route when there is route overlap.
- ///
- internal ApiGatewayRouteType ApiGatewayRouteType { get; set; }
-
- ///
- /// The number of characters preceding a greedy path variable {proxy+}. This is used to determine the priority of the route when there is route overlap.
- ///
- internal int LengthBeforeProxy { get; set; }
-
- ///
- /// The number of parameters in a path. This is used to determine the priority of the route when there is route overlap.
- ///
- internal int ParameterCount { get; set; }
}
\ No newline at end of file
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs
index 7b30ca538..bff1e9fe7 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs
@@ -2,7 +2,6 @@
using System.Text.Json;
using Amazon.Lambda.TestTool.Models;
using Amazon.Lambda.TestTool.Services.IO;
-using Microsoft.AspNetCore.Routing.Template;
namespace Amazon.Lambda.TestTool.Services;
@@ -26,7 +25,6 @@ public ApiGatewayRouteConfigService(
_environmentManager = environmentManager;
LoadLambdaConfigurationFromEnvironmentVariables();
- UpdateRouteConfigMetadataAndSorting();
}
///
@@ -61,10 +59,21 @@ private void LoadLambdaConfigurationFromEnvironmentVariables()
if (jsonValue.StartsWith('['))
{
_logger.LogDebug("Environment variable value starts with '['. Attempting to deserialize as a List.");
- var config = JsonSerializer.Deserialize>(jsonValue);
- if (config != null)
+ var configs = JsonSerializer.Deserialize>(jsonValue);
+ if (configs != null)
{
- _routeConfigs.AddRange(config);
+ foreach (var config in configs)
+ {
+ if (IsRouteConfigValid(config))
+ {
+ _routeConfigs.Add(config);
+ _logger.LogDebug("Environment variable deserialized and added to the existing configuration.");
+ }
+ else
+ {
+ _logger.LogDebug("The route config {Method} {Path} is not valid. It will be skipped.", config.HttpMethod, config.Path);
+ }
+ }
_logger.LogDebug("Environment variable deserialized and added to the existing configuration.");
}
else
@@ -78,8 +87,15 @@ private void LoadLambdaConfigurationFromEnvironmentVariables()
var config = JsonSerializer.Deserialize(jsonValue);
if (config != null)
{
- _routeConfigs.Add(config);
- _logger.LogDebug("Environment variable deserialized and added to the existing configuration.");
+ if (IsRouteConfigValid(config))
+ {
+ _routeConfigs.Add(config);
+ _logger.LogDebug("Environment variable deserialized and added to the existing configuration.");
+ }
+ else
+ {
+ _logger.LogDebug("The route config {Method} {Path} is not valid. It will be skipped.", config.HttpMethod, config.Path);
+ }
}
else
{
@@ -96,68 +112,28 @@ private void LoadLambdaConfigurationFromEnvironmentVariables()
}
///
- /// API Gateway selects the route with the most-specific match, using the following priorities:
- /// 1. Full match for a route and method.
- /// 2. Match for a route and method with path variable.
- /// 3. Match for a route and method with a greedy path variable ({proxy+}).
- ///
- /// For example, this is the order for the following example routes:
- /// 1. GET /pets/dog/1
- /// 2. GET /pets/dog/{id}
- /// 3. GET /pets/{proxy+}
- /// 4. ANY /{proxy+}
- ///
- /// For more info: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-routes.html
+ /// Applies some validity checks for Lambda route configuration.
///
- private void UpdateRouteConfigMetadataAndSorting()
+ /// Lambda route configuration
+ /// true if route is valid, false if not
+ private bool IsRouteConfigValid(ApiGatewayRouteConfig routeConfig)
{
- _logger.LogDebug("Updating the metadata needed to properly sort the Lambda config");
- foreach (var routeConfig in _routeConfigs)
+ var occurrences = routeConfig.Path.Split("{proxy+}").Length - 1;
+ if (occurrences > 1)
{
- if (routeConfig.Path.Contains("{proxy+}"))
- {
- routeConfig.ApiGatewayRouteType = ApiGatewayRouteType.Proxy;
- routeConfig.LengthBeforeProxy = routeConfig.Path.IndexOf("{proxy+}", StringComparison.InvariantCultureIgnoreCase);
- _logger.LogDebug("{Method} {Route} uses a proxy variable which starts at position {Position}.",
- routeConfig.HttpMethod,
- routeConfig.Path,
- routeConfig.LengthBeforeProxy);
- }
- else if (routeConfig.Path.Contains("{") && routeConfig.Path.Contains("}"))
- {
- routeConfig.ApiGatewayRouteType = ApiGatewayRouteType.Variable;
- routeConfig.LengthBeforeProxy = int.MaxValue;
-
- var template = TemplateParser.Parse(routeConfig.Path);
- routeConfig.ParameterCount = template.Parameters.Count;
-
- _logger.LogDebug("{Method} {Route} uses {ParameterCount} path variable(s).",
- routeConfig.HttpMethod,
- routeConfig.Path,
- routeConfig.ParameterCount);
- }
- else
- {
- routeConfig.ApiGatewayRouteType = ApiGatewayRouteType.Exact;
- routeConfig.LengthBeforeProxy = int.MaxValue;
-
- _logger.LogDebug("{Method} {Route} is an exact route with no variables.",
- routeConfig.HttpMethod,
- routeConfig.Path);
- }
+ _logger.LogDebug("The route config {Method} {Path} cannot have multiple greedy variables {{proxy+}}.",
+ routeConfig.HttpMethod, routeConfig.Path);
+ return false;
}
- _logger.LogDebug("Sorting the Lambda configs based on the updated metadata");
+ if (occurrences == 1 && !routeConfig.Path.EndsWith("/{proxy+}"))
+ {
+ _logger.LogDebug("The route config {Method} {Path} uses a greedy variable {{proxy+}} but does not end with it.",
+ routeConfig.HttpMethod, routeConfig.Path);
+ return false;
+ }
- // The sorting will be as follows:
- // 1. Exact paths first
- // 2. Paths with variables (the less the number of variables, the more exact the path is which means higher priority)
- // 3. Paths with greedy path variable {proxy+} (the more characters before {proxy+}, the more specific the path is, the higher the priority)
- _routeConfigs = _routeConfigs
- .OrderBy(x => x.ApiGatewayRouteType)
- .ThenBy(x => x.ParameterCount)
- .ThenByDescending(x => x.LengthBeforeProxy)
- .ToList();
+ return true;
}
///
@@ -183,39 +159,272 @@ private void UpdateRouteConfigMetadataAndSorting()
/// An corresponding to Lambda function with an API Gateway HTTP Method and Path.
public ApiGatewayRouteConfig? GetRouteConfig(string httpMethod, string path)
{
- foreach (var routeConfig in _routeConfigs)
+ var requestSegments = path.Trim('/').Split('/', StringSplitOptions.RemoveEmptyEntries);
+
+ var candidates = new List();
+
+ foreach (var route in _routeConfigs)
{
- _logger.LogDebug("Checking if '{Path}' matches '{Template}'.", path, routeConfig.Path);
+ _logger.LogDebug("{RequestMethod} {RequestPath}: Checking if matches with {TemplateMethod} {TemplatePath}.",
+ httpMethod, path, route.HttpMethod, route.Path);
- // ASP.NET has similar functionality as API Gateway which supports a greedy path variable.
- // Replace the API Gateway greedy parameter with ASP.NET catch-all parameter
- var transformedPath = routeConfig.Path.Replace("{proxy+}", "{*proxy}");
+ // Must match HTTP method or be ANY
+ if (!route.HttpMethod.Equals("ANY", StringComparison.InvariantCultureIgnoreCase) &&
+ !route.HttpMethod.Equals(httpMethod, StringComparison.InvariantCultureIgnoreCase))
+ {
+ _logger.LogDebug("{RequestMethod} {RequestPath}: The HTTP method does not match.",
+ httpMethod, path);
+ continue;
+ }
- var template = TemplateParser.Parse(transformedPath);
+ _logger.LogDebug("{RequestMethod} {RequestPath}: The HTTP method matches. Checking the route {TemplatePath}.",
+ httpMethod, path, route.Path);
- var matcher = new TemplateMatcher(template, new RouteValueDictionary());
+ var routeSegments = route.Path.Trim('/').Split('/', StringSplitOptions.RemoveEmptyEntries);
- var routeValueDictionary = new RouteValueDictionary();
- if (!matcher.TryMatch(path, routeValueDictionary))
+ var matchDetail = MatchRoute(routeSegments, requestSegments);
+ if (matchDetail.Matched)
{
- _logger.LogDebug("'{Path}' does not match '{Template}'.", path, routeConfig.Path);
- continue;
+ candidates.Add(new MatchResult
+ {
+ Route = route,
+ LiteralMatches = matchDetail.LiteralMatches,
+ GreedyVariables = matchDetail.GreedyCount,
+ NormalVariables = matchDetail.VariableCount,
+ TotalSegments = routeSegments.Length,
+ MatchedSegmentsBeforeGreedy = matchDetail.MatchedSegmentsBeforeGreedy
+ });
}
-
- _logger.LogDebug("'{Path}' matches '{Template}'. Now checking the HTTP Method.", path, routeConfig.Path);
-
- if (!routeConfig.HttpMethod.Equals("ANY") &&
- !routeConfig.HttpMethod.Equals(httpMethod, StringComparison.InvariantCultureIgnoreCase))
+ }
+
+ if (candidates.Count == 0)
+ {
+ _logger.LogDebug("{RequestMethod} {RequestPath}: The HTTP path does not match any configured route.",
+ httpMethod, path);
+ return null;
+ }
+
+ _logger.LogDebug("{RequestMethod} {RequestPath}: The following routes matched: {Routes}.",
+ httpMethod, path, string.Join(", ", candidates.Select(x => x.Route.Path)));
+
+ var best = candidates
+ .OrderByDescending(c => c.LiteralMatches)
+ .ThenByDescending(c => c.MatchedSegmentsBeforeGreedy)
+ .ThenBy(c => c.GreedyVariables)
+ .ThenBy(c => c.NormalVariables)
+ .ThenBy(c => c.TotalSegments)
+ .First();
+
+ _logger.LogDebug("{RequestMethod} {RequestPath}: Matched with the following route: {Routes}.",
+ httpMethod, path, best.Route.Path);
+
+ return best.Route;
+ }
+
+ ///
+ /// Attempts to match a given request path against a route template.
+ ///
+ /// The array of route template segments, which may include literal segments, normal variable segments, and greedy variable segments.
+ /// The array of request path segments to be matched against the route template.
+ ///
+ /// A tuple containing the following elements:
+ ///
+ /// -
+ /// Matched
+ /// true if the entire route template can be matched against the given request path segments; false otherwise.
+ ///
+ /// -
+ /// LiteralMatches
+ /// The number of literal segments in the route template that exactly matched the corresponding request path segments.
+ ///
+ /// -
+ /// VariableCount
+ /// The total number of normal variable segments matched during the process.
+ ///
+ /// -
+ /// GreedyCount
+ /// The total number of greedy variable segments matched during the process.
+ ///
+ /// -
+ /// MatchedSegmentsBeforeGreedy
+ /// The number of segments (literal or normal variable) that were matched before encountering any greedy variable segment. A higher number indicates a more specific match before resorting to greedily matching the remainder of the path.
+ ///
+ ///
+ ///
+ private (
+ bool Matched,
+ int LiteralMatches,
+ int VariableCount,
+ int GreedyCount,
+ int MatchedSegmentsBeforeGreedy)
+ MatchRoute(string[] routeSegments, string[] requestSegments)
+ {
+ var routeTemplateIndex = 0;
+ var requestPathIndex = 0;
+ var literalMatches = 0;
+ var variableCount = 0;
+ var greedyCount = 0;
+ var matched = true;
+
+ var matchedSegmentsBeforeGreedy = 0;
+ var encounteredGreedy = false;
+
+ while (
+ matched &&
+ routeTemplateIndex < routeSegments.Length &&
+ requestPathIndex < requestSegments.Length)
+ {
+ var routeTemplateSegment = routeSegments[routeTemplateIndex];
+ var requestPathSegment = requestSegments[requestPathIndex];
+
+ if (IsVariableSegment(routeTemplateSegment))
{
- _logger.LogDebug("HTTP Method of '{Path}' is {HttpMethod} and does not match the method of '{Template}' which is {TemplateMethod}.", path, httpMethod, routeConfig.Path, routeConfig.HttpMethod);
- continue;
+ if (IsGreedyVariable(routeTemplateSegment))
+ {
+ // Greedy variable must match at least one segment
+ // Check if we have at least one segment remaining
+ if (requestPathIndex >= requestSegments.Length)
+ {
+ // No segments left to match the greedy variable
+ matched = false;
+ }
+ else
+ {
+ greedyCount++;
+ encounteredGreedy = true;
+ routeTemplateIndex++;
+ // Greedy matches all remaining segments
+ requestPathIndex = requestSegments.Length;
+ }
+ }
+ else
+ {
+ variableCount++;
+ if (!encounteredGreedy) matchedSegmentsBeforeGreedy++;
+ routeTemplateIndex++;
+ requestPathIndex++;
+ }
}
-
- _logger.LogDebug("{HttpMethod} {Path} matches the existing configuration {TemplateMethod} {Template}.", httpMethod, path, routeConfig.HttpMethod, routeConfig.Path);
+ else
+ {
+ if (!routeTemplateSegment.Equals(requestPathSegment, StringComparison.OrdinalIgnoreCase))
+ {
+ matched = false;
+ }
+ else
+ {
+ literalMatches++;
+ if (!encounteredGreedy) matchedSegmentsBeforeGreedy++;
+ routeTemplateIndex++;
+ requestPathIndex++;
+ }
+ }
+ }
+
+ // If there are leftover route segments
+ while (matched && routeTemplateIndex < routeSegments.Length)
+ {
+ var rs = routeSegments[routeTemplateIndex];
+ if (IsVariableSegment(rs))
+ {
+ if (IsGreedyVariable(rs))
+ {
+ // Greedy variable must match at least one segment
+ // At this point, j points to the next request segment to match.
+ if (requestPathIndex >= requestSegments.Length)
+ {
+ // No segments left for greedy variable
+ matched = false;
+ }
+ else
+ {
+ greedyCount++;
+ encounteredGreedy = true;
+ // Greedy consumes all remaining segments
+ routeTemplateIndex++;
+ requestPathIndex = requestSegments.Length;
+ }
+ }
+ else
+ {
+ // Normal variable with no corresponding request segment is not allowed
+ matched = false;
+ routeTemplateIndex++;
+ }
+ }
+ else
+ {
+ // Literal not matched
+ matched = false;
+ routeTemplateIndex++;
+ }
+ }
- return routeConfig;
+ // If request has leftover segments that aren't matched
+ if (matched && requestPathIndex < requestSegments.Length)
+ {
+ matched = false;
}
- return null;
+ return (matched, literalMatches, variableCount, greedyCount, matchedSegmentsBeforeGreedy);
+ }
+
+ ///
+ /// Determines if a given segment represents a variable segment.
+ ///
+ /// The route template segment to check.
+ /// true if the segment is a variable segment; false otherwise.
+ private bool IsVariableSegment(string segment)
+ {
+ return segment.StartsWith("{") && segment.EndsWith("}");
+ }
+
+ ///
+ /// Determines if a given segment represents a greedy variable segment.
+ /// Greedy variables match one or more segments at the end of the route.
+ ///
+ /// The route template segment to check.
+ /// true if the segment is a greedy variable segment; false otherwise.
+ private bool IsGreedyVariable(string segment)
+ {
+ return segment.Equals("{proxy+}", StringComparison.InvariantCultureIgnoreCase);
+ }
+
+ ///
+ /// Represents a match result for a particular route configuration.
+ /// Contains information about how closely it matched, such as how many literal segments were matched,
+ /// how many greedy and normal variables were used, and how many segments were matched before any greedy variable.
+ ///
+ private class MatchResult
+ {
+ ///
+ /// The route configuration that this match result corresponds to.
+ ///
+ public required ApiGatewayRouteConfig Route { get; set; }
+
+ ///
+ /// The number of literal segments matched.
+ ///
+ public int LiteralMatches { get; set; }
+
+ ///
+ /// The number of greedy variables matched.
+ ///
+ public int GreedyVariables { get; set; }
+
+ ///
+ /// The number of normal variables matched.
+ ///
+ public int NormalVariables { get; set; }
+
+ ///
+ /// The total number of segments in the route template.
+ ///
+ public int TotalSegments { get; set; }
+
+ ///
+ /// The number of segments (literal or normal variable) matched before encountering any greedy variable.
+ ///
+ public int MatchedSegmentsBeforeGreedy { get; set; }
}
}
\ No newline at end of file
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Services/ApiGatewayRouteConfigServiceTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Services/ApiGatewayRouteConfigServiceTests.cs
index 3880fdb5c..be1954532 100644
--- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Services/ApiGatewayRouteConfigServiceTests.cs
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Services/ApiGatewayRouteConfigServiceTests.cs
@@ -157,7 +157,7 @@ public void Constructor_LoadsAndParsesListOfConfigs()
}
[Fact]
- public void ProperlySortRouteConfigs()
+ public void ProperlyMatchRouteConfigs()
{
// Arrange
var routeConfigs = new List
@@ -209,6 +209,24 @@ public void ProperlySortRouteConfigs()
LambdaResourceName = "F8",
HttpMethod = "GET",
Path = "/pets/dog/cat/1"
+ },
+ new ApiGatewayRouteConfig
+ {
+ LambdaResourceName = "F9",
+ HttpMethod = "GET",
+ Path = "/resource/{id}/subsegment/{proxy+}"
+ },
+ new ApiGatewayRouteConfig
+ {
+ LambdaResourceName = "F10",
+ HttpMethod = "GET",
+ Path = "/resource/{id}/subsegment/{id2}/{proxy+}"
+ },
+ new ApiGatewayRouteConfig
+ {
+ LambdaResourceName = "F11",
+ HttpMethod = "GET",
+ Path = "/resource/1/subsegment/3/{proxy+}"
}
};
@@ -236,6 +254,9 @@ public void ProperlySortRouteConfigs()
var result12 = service.GetRouteConfig("GET", "/pet/dog/cat/2");
var result13 = service.GetRouteConfig("GET", "/pet/cat/dog/1");
var result14 = service.GetRouteConfig("GET", "/pet/dog/1/2/3/4");
+ var result15 = service.GetRouteConfig("GET", "/resource/1/subsegment/more");
+ var result16 = service.GetRouteConfig("GET", "/resource/1/subsegment/2/more");
+ var result17 = service.GetRouteConfig("GET", "/resource/1/subsegment/3/more");
// Assert
Assert.Equal("F8", result1?.LambdaResourceName);
@@ -252,5 +273,8 @@ public void ProperlySortRouteConfigs()
Assert.Equal("F1", result12?.LambdaResourceName);
Assert.Equal("F1", result13?.LambdaResourceName);
Assert.Equal("F1", result14?.LambdaResourceName);
+ Assert.Equal("F9", result15?.LambdaResourceName);
+ Assert.Equal("F10", result16?.LambdaResourceName);
+ Assert.Equal("F11", result17?.LambdaResourceName);
}
}
\ No newline at end of file