Skip to content

Commit 77f7679

Browse files
committed
Add request parsing into api gateway
1 parent 1af7cf4 commit 77f7679

File tree

13 files changed

+676
-131
lines changed

13 files changed

+676
-131
lines changed

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Amazon.Lambda.TestTool.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
2020
<PackageReference Include="Blazored.Modal" Version="7.3.1" />
2121
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
22+
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" />
2223
</ItemGroup>
2324

2425
<ItemGroup>
Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
using Amazon.Lambda.TestTool.Models;
2-
3-
namespace Amazon.Lambda.TestTool.Extensions;
4-
1+
using Amazon.Lambda.TestTool.Models;
2+
3+
namespace Amazon.Lambda.TestTool.Extensions;
4+
55
/// <summary>
66
/// A class that contains extension methods for the <see cref="Exception"/> class.
7-
/// </summary>
8-
public static class ExceptionExtensions
9-
{
10-
/// <summary>
11-
/// True if the <paramref name="e"/> inherits from
12-
/// <see cref="TestToolException"/>.
13-
/// </summary>
14-
public static bool IsExpectedException(this Exception e) =>
15-
e is TestToolException;
16-
7+
/// </summary>
8+
public static class ExceptionExtensions
9+
{
10+
/// <summary>
11+
/// True if the <paramref name="e"/> inherits from
12+
/// <see cref="TestToolException"/>.
13+
/// </summary>
14+
public static bool IsExpectedException(this Exception e) =>
15+
e is TestToolException;
16+
1717
/// <summary>
1818
/// Prints an exception in a user-friendly way.
19-
/// </summary>
20-
public static string PrettyPrint(this Exception? e)
21-
{
22-
if (null == e)
23-
return string.Empty;
24-
25-
return $"{Environment.NewLine}{e.Message}" +
26-
$"{Environment.NewLine}{e.StackTrace}" +
27-
$"{PrettyPrint(e.InnerException)}";
28-
}
19+
/// </summary>
20+
public static string PrettyPrint(this Exception? e)
21+
{
22+
if (null == e)
23+
return string.Empty;
24+
25+
return $"{Environment.NewLine}{e.Message}" +
26+
$"{Environment.NewLine}{e.StackTrace}" +
27+
$"{PrettyPrint(e.InnerException)}";
28+
}
2929
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
namespace Amazon.Lambda.TestTool.Extensions;
2+
3+
using Amazon.Lambda.APIGatewayEvents;
4+
using Amazon.Lambda.TestTool.Models;
5+
using Amazon.Lambda.TestTool.Services;
6+
using Amazon.Lambda.TestTool.Utilities;
7+
using System.Text;
8+
using System.Web;
9+
using static Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest;
10+
11+
/// <summary>
12+
/// Provides extension methods to translate an <see cref="HttpContext"/> to different types of API Gateway requests.
13+
/// </summary>
14+
public static class HttpContextExtensions
15+
{
16+
17+
/// <summary>
18+
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayHttpApiV2ProxyRequest"/>.
19+
/// </summary>
20+
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
21+
/// <returns>An <see cref="APIGatewayHttpApiV2ProxyRequest"/> object representing the translated request.</returns>
22+
public static APIGatewayHttpApiV2ProxyRequest ToApiGatewayHttpV2Request(
23+
this HttpContext context,
24+
ApiGatewayRouteConfig apiGatewayRouteConfig)
25+
{
26+
var request = context.Request;
27+
28+
var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);
29+
30+
var (headers, _) = HttpRequestUtility.ExtractHeaders(request.Headers);
31+
var (queryStringParameters, _) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);
32+
33+
var httpApiV2ProxyRequest = new APIGatewayHttpApiV2ProxyRequest
34+
{
35+
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}",
36+
RawPath = request.Path,
37+
RawQueryString = request.QueryString.Value,
38+
Cookies = request.Cookies.Select(c => $"{c.Key}={c.Value}").ToArray(),
39+
Headers = headers,
40+
QueryStringParameters = queryStringParameters,
41+
PathParameters = pathParameters ?? new Dictionary<string, string>(),
42+
Body = HttpRequestUtility.ReadRequestBody(request),
43+
IsBase64Encoded = false,
44+
RequestContext = new ProxyRequestContext
45+
{
46+
Http = new HttpDescription
47+
{
48+
Method = request.Method,
49+
Path = request.Path,
50+
Protocol = request.Protocol
51+
},
52+
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}"
53+
},
54+
Version = "2.0"
55+
};
56+
57+
if (HttpRequestUtility.IsBinaryContent(request.ContentType))
58+
{
59+
httpApiV2ProxyRequest.Body = Convert.ToBase64String(Encoding.UTF8.GetBytes(httpApiV2ProxyRequest.Body));
60+
httpApiV2ProxyRequest.IsBase64Encoded = true;
61+
}
62+
63+
return httpApiV2ProxyRequest;
64+
}
65+
66+
/// <summary>
67+
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayProxyRequest"/>.
68+
/// </summary>
69+
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
70+
/// <returns>An <see cref="APIGatewayProxyRequest"/> object representing the translated request.</returns>
71+
public static APIGatewayProxyRequest ToApiGatewayRequest(
72+
this HttpContext context,
73+
ApiGatewayRouteConfig apiGatewayRouteConfig)
74+
{
75+
var request = context.Request;
76+
77+
var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);
78+
79+
var (headers, multiValueHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
80+
var (queryStringParameters, multiValueQueryStringParameters) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);
81+
82+
var proxyRequest = new APIGatewayProxyRequest
83+
{
84+
Resource = apiGatewayRouteConfig.Path,
85+
Path = HttpUtility.UrlEncode(request.Path),
86+
HttpMethod = request.Method,
87+
Headers = headers,
88+
MultiValueHeaders = multiValueHeaders,
89+
QueryStringParameters = queryStringParameters,
90+
MultiValueQueryStringParameters = multiValueQueryStringParameters,
91+
PathParameters = pathParameters ?? new Dictionary<string, string>(),
92+
Body = HttpRequestUtility.ReadRequestBody(request),
93+
IsBase64Encoded = false
94+
};
95+
96+
if (HttpRequestUtility.IsBinaryContent(request.ContentType))
97+
{
98+
proxyRequest.Body = Convert.ToBase64String(Encoding.UTF8.GetBytes(proxyRequest.Body));
99+
proxyRequest.IsBase64Encoded = true;
100+
}
101+
102+
return proxyRequest;
103+
}
104+
}
Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
1-
using Amazon.Lambda.TestTool.Services;
2-
using Amazon.Lambda.TestTool.Services.IO;
3-
using Microsoft.Extensions.DependencyInjection.Extensions;
4-
5-
namespace Amazon.Lambda.TestTool.Extensions;
6-
7-
/// <summary>
8-
/// A class that contains extension methods for the <see cref="IServiceCollection"/> interface.
9-
/// </summary>
10-
public static class ServiceCollectionExtensions
11-
{
1+
using Amazon.Lambda.TestTool.Services;
2+
using Amazon.Lambda.TestTool.Services.IO;
3+
using Microsoft.Extensions.DependencyInjection.Extensions;
4+
5+
namespace Amazon.Lambda.TestTool.Extensions;
6+
7+
/// <summary>
8+
/// A class that contains extension methods for the <see cref="IServiceCollection"/> interface.
9+
/// </summary>
10+
public static class ServiceCollectionExtensions
11+
{
1212
/// <summary>
1313
/// Adds a set of services for the .NET CLI portion of this application.
14-
/// </summary>
15-
public static void AddCustomServices(this IServiceCollection serviceCollection,
16-
ServiceLifetime lifetime = ServiceLifetime.Singleton)
17-
{
18-
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveService), lifetime));
19-
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDirectoryManager), typeof(DirectoryManager), lifetime));
14+
/// </summary>
15+
public static void AddCustomServices(this IServiceCollection serviceCollection,
16+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
17+
{
18+
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveService), lifetime));
19+
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDirectoryManager), typeof(DirectoryManager), lifetime));
2020
}
2121

2222
/// <summary>
2323
/// Adds a set of services for the API Gateway emulator portion of this application.
24-
/// </summary>
25-
public static void AddApiGatewayEmulatorServices(this IServiceCollection serviceCollection,
26-
ServiceLifetime lifetime = ServiceLifetime.Singleton)
27-
{
28-
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IApiGatewayRouteConfigService), typeof(ApiGatewayRouteConfigService), lifetime));
29-
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentManager), typeof(EnvironmentManager), lifetime));
30-
}
24+
/// </summary>
25+
public static void AddApiGatewayEmulatorServices(this IServiceCollection serviceCollection,
26+
ServiceLifetime lifetime = ServiceLifetime.Singleton)
27+
{
28+
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IApiGatewayRouteConfigService), typeof(ApiGatewayRouteConfigService), lifetime));
29+
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentManager), typeof(EnvironmentManager), lifetime));
30+
}
3131
}

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ public class ApiGatewayRouteConfig
99
/// The name of the Lambda function
1010
/// </summary>
1111
public required string LambdaResourceName { get; set; }
12-
12+
1313
/// <summary>
1414
/// The endpoint of the local Lambda Runtime API
1515
/// </summary>
1616
public string? Endpoint { get; set; }
17-
17+
1818
/// <summary>
1919
/// The HTTP Method for the API Gateway endpoint
2020
/// </summary>
2121
public required string HttpMethod { get; set; }
22-
22+
2323
/// <summary>
2424
/// The API Gateway HTTP Path of the Lambda function
2525
/// </summary>

Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Text.Json;
33
using Amazon.Lambda.TestTool.Models;
44
using Amazon.Lambda.TestTool.Services.IO;
5+
using Amazon.Lambda.TestTool.Utilities;
56
using Microsoft.AspNetCore.Routing.Template;
67

78
namespace Amazon.Lambda.TestTool.Services;
@@ -66,7 +67,7 @@ public ApiGatewayRouteConfigService(
6667
{
6768
var template = TemplateParser.Parse(routeConfig.Path);
6869

69-
var matcher = new TemplateMatcher(template, GetDefaults(template));
70+
var matcher = new TemplateMatcher(template, RouteTemplateUtility.GetDefaults(template));
7071

7172
var routeValueDictionary = new RouteValueDictionary();
7273
if (!matcher.TryMatch(path, routeValueDictionary))
@@ -80,19 +81,4 @@ public ApiGatewayRouteConfigService(
8081

8182
return null;
8283
}
83-
84-
private RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate)
85-
{
86-
var result = new RouteValueDictionary();
87-
88-
foreach (var parameter in parsedTemplate.Parameters)
89-
{
90-
if (parameter.DefaultValue != null)
91-
{
92-
if (parameter.Name != null) result.Add(parameter.Name, parameter.DefaultValue);
93-
}
94-
}
95-
96-
return result;
97-
}
9884
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
namespace Amazon.Lambda.TestTool
2+
{
3+
/// <summary>
4+
/// Utility class for handling HTTP requests in the context of API Gateway emulation.
5+
/// </summary>
6+
public static class HttpRequestUtility
7+
{
8+
/// <summary>
9+
/// Determines whether the specified content type represents binary content.
10+
/// </summary>
11+
/// <param name="contentType">The content type to check.</param>
12+
/// <returns>True if the content type represents binary content; otherwise, false.</returns>
13+
public static bool IsBinaryContent(string? contentType)
14+
{
15+
if (string.IsNullOrEmpty(contentType))
16+
return false;
17+
18+
return contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) ||
19+
contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) ||
20+
contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase) ||
21+
contentType.Equals("application/octet-stream", StringComparison.OrdinalIgnoreCase);
22+
}
23+
24+
/// <summary>
25+
/// Reads the body of the HTTP request as a string.
26+
/// </summary>
27+
/// <param name="request">The HTTP request.</param>
28+
/// <returns>The body of the request as a string.</returns>
29+
public static string ReadRequestBody(HttpRequest request)
30+
{
31+
using (var reader = new StreamReader(request.Body))
32+
{
33+
return reader.ReadToEnd();
34+
}
35+
}
36+
37+
/// <summary>
38+
/// Extracts headers from the request, separating them into single-value and multi-value dictionaries.
39+
/// </summary>
40+
/// <param name="headers">The request headers.</param>
41+
/// <returns>A tuple containing single-value and multi-value header dictionaries.</returns>
42+
/// <example>
43+
/// For headers:
44+
/// Accept: text/html
45+
/// Accept: application/xhtml+xml
46+
/// X-Custom-Header: value1
47+
///
48+
/// The method will return:
49+
/// singleValueHeaders: { "Accept": "application/xhtml+xml", "X-Custom-Header": "value1" }
50+
/// multiValueHeaders: { "Accept": ["text/html", "application/xhtml+xml"], "X-Custom-Header": ["value1"] }
51+
/// </example>
52+
public static (IDictionary<string, string>, IDictionary<string, IList<string>>) ExtractHeaders(IHeaderDictionary headers)
53+
{
54+
var singleValueHeaders = new Dictionary<string, string>();
55+
var multiValueHeaders = new Dictionary<string, IList<string>>();
56+
57+
foreach (var header in headers)
58+
{
59+
singleValueHeaders[header.Key] = header.Value.Last() ?? "";
60+
multiValueHeaders[header.Key] = [.. header.Value];
61+
}
62+
63+
return (singleValueHeaders, multiValueHeaders);
64+
}
65+
66+
/// <summary>
67+
/// Extracts query string parameters from the request, separating them into single-value and multi-value dictionaries.
68+
/// </summary>
69+
/// <param name="query">The query string collection.</param>
70+
/// <returns>A tuple containing single-value and multi-value query parameter dictionaries.</returns>
71+
/// <example>
72+
/// For query string: ?param1=value1&amp;param2=value2&amp;param2=value3
73+
///
74+
/// The method will return:
75+
/// singleValueParams: { "param1": "value1", "param2": "value3" }
76+
/// multiValueParams: { "param1": ["value1"], "param2": ["value2", "value3"] }
77+
/// </example>
78+
public static (IDictionary<string, string>, IDictionary<string, IList<string>>) ExtractQueryStringParameters(IQueryCollection query)
79+
{
80+
var singleValueParams = new Dictionary<string, string>();
81+
var multiValueParams = new Dictionary<string, IList<string>>();
82+
83+
foreach (var param in query)
84+
{
85+
singleValueParams[param.Key] = param.Value.Last() ?? "";
86+
multiValueParams[param.Key] = [.. param.Value];
87+
}
88+
89+
return (singleValueParams, multiValueParams);
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)