Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ApiGatewayHttpApiV2ProxyRequestTranslator and ApiGatewayProxyRequestTranslator #1901

Open
wants to merge 10 commits into
base: asmarp/api-gateway-emulator-skeleton
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
<PackageReference Include="Blazored.Modal" Version="7.3.1" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.11" />
<PackageReference Include="Amazon.Lambda.APIGatewayEvents" Version="2.7.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
using Amazon.Lambda.TestTool.Models;
gcbeattyAWS marked this conversation as resolved.
Show resolved Hide resolved

namespace Amazon.Lambda.TestTool.Extensions;

using Amazon.Lambda.TestTool.Models;
namespace Amazon.Lambda.TestTool.Extensions;
/// <summary>
/// A class that contains extension methods for the <see cref="Exception"/> class.
/// </summary>
public static class ExceptionExtensions
{
/// <summary>
/// True if the <paramref name="e"/> inherits from
/// <see cref="TestToolException"/>.
/// </summary>
public static bool IsExpectedException(this Exception e) =>
e is TestToolException;

/// </summary>
public static class ExceptionExtensions
{
/// <summary>
/// True if the <paramref name="e"/> inherits from
/// <see cref="TestToolException"/>.
/// </summary>
public static bool IsExpectedException(this Exception e) =>
e is TestToolException;
/// <summary>
/// Prints an exception in a user-friendly way.
/// </summary>
public static string PrettyPrint(this Exception? e)
{
if (null == e)
return string.Empty;

return $"{Environment.NewLine}{e.Message}" +
$"{Environment.NewLine}{e.StackTrace}" +
$"{PrettyPrint(e.InnerException)}";
}
/// </summary>
public static string PrettyPrint(this Exception? e)
{
if (null == e)
return string.Empty;
return $"{Environment.NewLine}{e.Message}" +
$"{Environment.NewLine}{e.StackTrace}" +
$"{PrettyPrint(e.InnerException)}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
namespace Amazon.Lambda.TestTool.Extensions;

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.TestTool.Models;
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Utilities;
using System.Text;
using System.Web;
using static Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest;

/// <summary>
/// Provides extension methods to translate an <see cref="HttpContext"/> to different types of API Gateway requests.
/// </summary>
public static class HttpContextExtensions
{
/// <summary>
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayHttpApiV2ProxyRequest"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param>
/// <returns>An <see cref="APIGatewayHttpApiV2ProxyRequest"/> object representing the translated request.</returns>
public static APIGatewayHttpApiV2ProxyRequest ToApiGatewayHttpV2Request(
this HttpContext context,
ApiGatewayRouteConfig apiGatewayRouteConfig)
{
var request = context.Request;

var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);
gcbeattyAWS marked this conversation as resolved.
Show resolved Hide resolved

// Format 2.0 doesn't have multiValueHeaders or multiValueQueryStringParameters fields. Duplicate headers are combined with commas and included in the headers field.
var (_, allHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
var headers = allHeaders.ToDictionary(
kvp => kvp.Key,
kvp => string.Join(",", kvp.Value)
);

// Duplicate query strings are combined with commas and included in the queryStringParameters field.
var (_, allQueryParams) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);
var queryStringParameters = allQueryParams.ToDictionary(
kvp => kvp.Key,
kvp => string.Join(",", kvp.Value)
);

var httpApiV2ProxyRequest = new APIGatewayHttpApiV2ProxyRequest
{
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}",
RawPath = request.Path.Value, // this should be decoded value
Body = HttpRequestUtility.ReadRequestBody(request),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you take note that when we get these ToAPIGatewayXXX() methods hooked up to the main code path that is asking for the APIGatewayHttpApiV2ProxyRequest. When we convert the APIGatewayHttpApiV2ProxyRequest to the MemoryStream to Lambda we should check the size of the stream is no more then 6MB. If so throw back the same error that users would get from API Gateway.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i will connect with phil on where he wants to do this logic at

IsBase64Encoded = false,
RequestContext = new ProxyRequestContext
{
Http = new HttpDescription
{
Method = request.Method,
Path = request.Path.Value, // this should be decoded value
Protocol = request.Protocol
},
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}"
},
Version = "2.0"
};

if (request.Cookies.Any())
{
httpApiV2ProxyRequest.Cookies = request.Cookies.Select(c => $"{c.Key}={c.Value}").ToArray();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

headers and cookies are just sent as-is (from my testing)

}

if (headers.Any())
{
httpApiV2ProxyRequest.Headers = headers;
}

if (queryStringParameters.Any())
{
// this should be decoded value
httpApiV2ProxyRequest.QueryStringParameters = queryStringParameters;

// this should be the url encoded value and not include the "?"
// e.g. key=%2b%2b%2b
httpApiV2ProxyRequest.RawQueryString = HttpUtility.UrlPathEncode(request.QueryString.Value?.Substring(1));

}

if (pathParameters.Any())
{
// this should be decoded value
httpApiV2ProxyRequest.PathParameters = pathParameters;
}

if (HttpRequestUtility.IsBinaryContent(request.ContentType))
{
httpApiV2ProxyRequest.Body = Convert.ToBase64String(Encoding.UTF8.GetBytes(httpApiV2ProxyRequest.Body));
httpApiV2ProxyRequest.IsBase64Encoded = true;
}

return httpApiV2ProxyRequest;
}

/// <summary>
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayProxyRequest"/>.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param>
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param>
/// <returns>An <see cref="APIGatewayProxyRequest"/> object representing the translated request.</returns>
public static APIGatewayProxyRequest ToApiGatewayRequest(
this HttpContext context,
ApiGatewayRouteConfig apiGatewayRouteConfig)
{
var request = context.Request;

var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path);

var (headers, multiValueHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
var (queryStringParameters, multiValueQueryStringParameters) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);

var proxyRequest = new APIGatewayProxyRequest
{
Resource = apiGatewayRouteConfig.Path,
Path = request.Path.Value,
HttpMethod = request.Method,
Body = HttpRequestUtility.ReadRequestBody(request),
IsBase64Encoded = false
};

if (headers.Any())
{
proxyRequest.Headers = headers;
}

if (multiValueHeaders.Any())
{
proxyRequest.MultiValueHeaders = multiValueHeaders;
}

if (queryStringParameters.Any())
{
// this should be decoded value
proxyRequest.QueryStringParameters = queryStringParameters;
}

if (multiValueQueryStringParameters.Any())
{
// this should be decoded value
proxyRequest.MultiValueQueryStringParameters = multiValueQueryStringParameters;
}

if (pathParameters.Any())
{
// this should be decoded value
proxyRequest.PathParameters = pathParameters;
}

if (HttpRequestUtility.IsBinaryContent(request.ContentType))
{
proxyRequest.Body = Convert.ToBase64String(Encoding.UTF8.GetBytes(proxyRequest.Body));
proxyRequest.IsBase64Encoded = true;
}

return proxyRequest;
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Services.IO;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Amazon.Lambda.TestTool.Extensions;

/// <summary>
/// A class that contains extension methods for the <see cref="IServiceCollection"/> interface.
/// </summary>
public static class ServiceCollectionExtensions
{
using Amazon.Lambda.TestTool.Services;
using Amazon.Lambda.TestTool.Services.IO;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Amazon.Lambda.TestTool.Extensions;
/// <summary>
/// A class that contains extension methods for the <see cref="IServiceCollection"/> interface.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds a set of services for the .NET CLI portion of this application.
/// </summary>
public static void AddCustomServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDirectoryManager), typeof(DirectoryManager), lifetime));
/// </summary>
public static void AddCustomServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IToolInteractiveService), typeof(ConsoleInteractiveService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IDirectoryManager), typeof(DirectoryManager), lifetime));
}

/// <summary>
/// Adds a set of services for the API Gateway emulator portion of this application.
/// </summary>
public static void AddApiGatewayEmulatorServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IApiGatewayRouteConfigService), typeof(ApiGatewayRouteConfigService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentManager), typeof(EnvironmentManager), lifetime));
}
/// </summary>
public static void AddApiGatewayEmulatorServices(this IServiceCollection serviceCollection,
ServiceLifetime lifetime = ServiceLifetime.Singleton)
{
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IApiGatewayRouteConfigService), typeof(ApiGatewayRouteConfigService), lifetime));
serviceCollection.TryAdd(new ServiceDescriptor(typeof(IEnvironmentManager), typeof(EnvironmentManager), lifetime));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ public class ApiGatewayRouteConfig
/// The name of the Lambda function
/// </summary>
public required string LambdaResourceName { get; set; }

/// <summary>
/// The endpoint of the local Lambda Runtime API
/// </summary>
public string? Endpoint { get; set; }

/// <summary>
/// The HTTP Method for the API Gateway endpoint
/// </summary>
public required string HttpMethod { get; set; }

/// <summary>
/// The API Gateway HTTP Path of the Lambda function
/// </summary>
Expand Down
Loading