using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
#if NET6_0_OR_GREATER
using System.Buffers;
using System.Text.Json;
using System.Text.Json.Serialization;
#endif
// This assembly is also used in the source generator which needs the .NET attributes and type infos defined in this project.
// The source generator requires all dependencies to target .NET Standard 2.0 to be compatible with .NET Framework used
// by Visual Studio. Several APIs are used in this file that are not available in .NET Standard 2.0 but the source
// generator doesn't use the methods that use those .NET APIs. Several areas in this file put stub implementations
// for .NET Standard 2.0 to allow the types to be available in the source generator but the implementations
// are not actually called in the source generator.
namespace Amazon.Lambda.Annotations.APIGateway
{
///
/// The options used by the IHttpResult to serialize into the required format for the event source of the Lambda funtion.
///
public class HttpResultSerializationOptions
{
///
/// The API Gateway protocol format used as the event source.
///
public enum ProtocolFormat {
///
/// Used when a function is defined with the RestApiAttribute.
///
RestApi,
///
/// Used when a function is defined with the HttpApiAttribute.
///
HttpApi
}
///
/// The API Gateway protocol version.
///
public enum ProtocolVersion {
///
/// V1 format for API Gateway Proxy responses. Used for functions defined with RestApiAttribute or HttpApiAttribute with explicit setting to V1.
///
V1,
///
/// V2 format for API Gateway Proxy responses. Used for functions defined with HttpApiAttribute with an implicit version of an explicit setting to V2.
///
V2
}
///
/// The API Gateway protocol used as the event source.
/// RestApi -> RestApiAttrbute
/// HttpApi -> HttpApiAttribute
///
public ProtocolFormat Format { get; set; }
///
/// The API Gateway protocol version used as the event source.
/// V1 -> RestApi or HttpApi specifically set as V1
/// V2 -> HttpApi either implicit or explicit set to V2
///
public ProtocolVersion Version { get; set; }
}
///
/// If this inteface is returned for an API Gateway Lambda function it will serialize itself to the correct JSON format for the
/// configured event source's protocol format and version.
///
/// Users should use the implementation class HttpResults to construct an instance of IHttpResult with the configured, status code, response body and headers.
///
///
/// return HttpResults.Ok("All Good")
/// .AddHeader("Custom-Header", "FooBar");
///
///
public interface IHttpResult
{
///
/// The Status code of the HttpResult
///
HttpStatusCode StatusCode { get; }
///
/// Used by the Lambda Annotations framework to serialize the IHttpResult to the correct JSON response.
///
///
///
Stream Serialize(HttpResultSerializationOptions options);
///
/// Add header to the IHttpResult. The AddHeader method can be called multiple times for the same header to add multi values for a header.
/// HTTP header names are case insensitive and the AddHeader method will normalize header name casing by calling ToLower on them.
///
/// HTTP header name
/// HTTP header value
/// The same instance to allow fluent call pattern.
IHttpResult AddHeader(string name, string value);
}
///
/// Implementation class for IHttpResult. Consumers should use one of the static methods to create the a result with the desired status code.
///
///
/// If a response body is provided it is format using the following rules:
///
/// -
/// For string then returned as is.
///
/// -
/// For Stream, byte[] or IList<byte> the data is consided binary and base 64 encoded.
///
/// -
/// Anything other type is serialized to JSON.
///
///
///
///
/// return HttpResults.Ok("All Good")
/// .AddHeader("Custom-Header", "FooBar");
///
///
public class HttpResults : IHttpResult
{
private const string HEADER_NAME_CONTENT_TYPE = "content-type";
private const string CONTENT_TYPE_APPLICATION_JSON = "application/json";
private const string CONTENT_TYPE_TEXT_PLAIN = "text/plain";
private const string CONTENT_TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream";
private string _body;
private IDictionary> _headers;
private bool _isBase64Encoded;
private string _defaultContentType;
private HttpResults(HttpStatusCode statusCode, object body = null)
{
StatusCode = statusCode;
FormatBody(body);
}
///
public IHttpResult AddHeader(string name, string value)
{
name = name.ToLower();
if (_headers == null)
{
_headers = new Dictionary>();
}
if (!_headers.TryGetValue(name, out var values))
{
values = new List();
_headers[name] = values;
}
values.Add(value);
return this;
}
///
/// Creates an IHttpResult for a Accepted (202) status code.
///
/// Optional response body
///
public static IHttpResult Accepted(object body = null)
{
return new HttpResults(HttpStatusCode.Accepted, body);
}
///
/// Creates an IHttpResult for a Bad Gateway (502) status code.
///
///
public static IHttpResult BadGateway()
{
return new HttpResults(HttpStatusCode.BadGateway);
}
///
/// Creates an IHttpResult for a BadRequest (400) status code.
///
/// Optional response body
///
public static IHttpResult BadRequest(object body = null)
{
return new HttpResults(HttpStatusCode.BadRequest, body);
}
///
/// Creates an IHttpResult for a Conflict (409) status code.
///
/// Optional response body
///
public static IHttpResult Conflict(object body = null)
{
return new HttpResults(HttpStatusCode.Conflict, body);
}
///
/// Creates an IHttpResult for a Created (201) status code.
///
/// Optional URI for the created resource. The value is set to the Location response header.
/// Optional response body
///
public static IHttpResult Created(string uri = null, object body = null)
{
var result = new HttpResults(HttpStatusCode.Created, body);
if (uri != null)
{
result.AddHeader("location", uri);
}
return result;
}
///
/// Creates an IHttpResult for a Forbidden (403) status code.
///
/// Optional response body
///
public static IHttpResult Forbid(object body = null)
{
return new HttpResults(HttpStatusCode.Forbidden, body);
}
///
/// Creates an IHttpResult for an Internal Server Error (500) status code.
///
/// Optional response body
///
public static IHttpResult InternalServerError(object body = null)
{
return new HttpResults(HttpStatusCode.InternalServerError, body);
}
///
/// Creates an IHttpResult for a NotFound (404) status code.
///
/// Optional response body
///
public static IHttpResult NotFound(object body = null)
{
return new HttpResults(HttpStatusCode.NotFound, body);
}
///
/// Creates an IHttpResult for a Ok (200) status code.
///
/// Optional response body
///
public static IHttpResult Ok(object body = null)
{
return new HttpResults(HttpStatusCode.OK, body);
}
///
/// Creates an IHttpResult for redirect responses.
///
///
/// This method uses the same logic for determing the the Http status code as the Microsoft.AspNetCore.Http.TypedResults.Redirect uses.
/// https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults.redirect
///
/// The URI to redirect to. The value will be set in the location header.
/// Whether the redirect should be a permanet (301) or temporary (302) redirect.
/// Whether the request method should be preserved. If set to true use 308 for permanent or 307 for temporary redirects.
///
public static IHttpResult Redirect(string uri, bool permanent = false, bool preserveMethod = false)
{
HttpStatusCode code;
if (permanent && preserveMethod)
{
code = (HttpStatusCode)308; // .NET Standard 2.0 does not have the enum value for PermanentRedirect so using direct number;
}
else if (!permanent && preserveMethod)
{
code = HttpStatusCode.TemporaryRedirect;
}
else if (permanent && !preserveMethod)
{
code = HttpStatusCode.MovedPermanently;
}
else
{
code = HttpStatusCode.Redirect;
}
var result = new HttpResults(code, null);
if (uri != null)
{
result.AddHeader("location", uri);
}
return result;
}
///
/// Creates an IHttpResult for a Service Unavailable (503) status code.
///
/// Optional number of seconds to return in a Retry-After header
///
public static IHttpResult ServiceUnavailable(int? delaySeconds = null)
{
var result = new HttpResults(HttpStatusCode.ServiceUnavailable);
if (delaySeconds != null && delaySeconds > 0)
{
result.AddHeader("Retry-After", delaySeconds.ToString());
}
return result;
}
///
/// Creates an IHttpResult for a Unauthorized (401) status code.
///
///
public static IHttpResult Unauthorized()
{
return new HttpResults(HttpStatusCode.Unauthorized);
}
///
/// Creates an IHttpResult for the specified status code.
///
/// Http status code used to create the IHttpResult instance.
/// Optional response body
///
public static IHttpResult NewResult(HttpStatusCode statusCode, object body = null)
{
return new HttpResults(statusCode, body);
}
#region Serialization
// See comment in class documentation on the rules for serializing. If any changes are made in this method be sure to update
// the comment above.
private void FormatBody(object body)
{
// See comment at the top about .NET Standard 2.0
#if NETSTANDARD2_0
throw new NotImplementedException();
#else
if (body == null)
return;
if (body is string str)
{
_defaultContentType = CONTENT_TYPE_TEXT_PLAIN;
_body = str;
}
else if (body is Stream stream)
{
_defaultContentType = CONTENT_TYPE_APPLICATION_OCTET_STREAM;
_isBase64Encoded = true;
var buffer = ArrayPool.Shared.Rent((int)stream.Length);
try
{
var readLength = stream.Read(buffer, 0, buffer.Length);
_body = Convert.ToBase64String(buffer, 0, readLength);
}
finally
{
ArrayPool.Shared.Return(buffer);
}
}
else if (body is byte[] binaryData)
{
_defaultContentType = CONTENT_TYPE_APPLICATION_OCTET_STREAM;
_isBase64Encoded = true;
_body = Convert.ToBase64String(binaryData, 0, binaryData.Length);
}
else if (body is IList listBinaryData)
{
_defaultContentType = CONTENT_TYPE_APPLICATION_OCTET_STREAM;
_isBase64Encoded = true;
_body = Convert.ToBase64String(listBinaryData.ToArray(), 0, listBinaryData.Count);
}
else
{
_defaultContentType = CONTENT_TYPE_APPLICATION_JSON;
_body = JsonSerializer.Serialize(body);
}
#endif
}
///
public HttpStatusCode StatusCode { get; }
///
/// Serialize the IHttpResult into the expect format for the event source.
///
///
///
public Stream Serialize(HttpResultSerializationOptions options)
{
// See comment at the top about .NET Standard 2.0
#if NETSTANDARD2_0
throw new NotImplementedException();
#else
// If the user didn't explicit set the content type then default to application/json
if(!string.IsNullOrEmpty(_body) && (_headers == null || !_headers.ContainsKey(HEADER_NAME_CONTENT_TYPE)))
{
AddHeader(HEADER_NAME_CONTENT_TYPE, _defaultContentType);
}
var stream = new MemoryStream();
if (options.Format == HttpResultSerializationOptions.ProtocolFormat.RestApi ||
(options.Format == HttpResultSerializationOptions.ProtocolFormat.HttpApi && options.Version == HttpResultSerializationOptions.ProtocolVersion.V1))
{
var response = new APIGatewayV1Response
{
StatusCode = (int)StatusCode,
Body = _body,
MultiValueHeaders = _headers,
IsBase64Encoded = _isBase64Encoded
};
JsonSerializer.Serialize(stream, response);
}
else
{
var response = new APIGatewayV2Response
{
StatusCode = (int)StatusCode,
Body = _body,
Headers = ConvertToV2MultiValueHeaders(_headers),
IsBase64Encoded = _isBase64Encoded
};
JsonSerializer.Serialize(stream, response);
}
stream.Position = 0;
return stream;
#endif
}
///
/// The V2 format used by HttpApi handles multi value headers by having the value be comma delimited. This
/// utility method handles converting the collection from the V1 format to the V2.
///
///
///
private static IDictionary ConvertToV2MultiValueHeaders(IDictionary> v1MultiHeaders)
{
if (v1MultiHeaders == null)
return null;
var v2MultiHeaders = new Dictionary();
foreach (var kvp in v1MultiHeaders)
{
var values = string.Join(",", kvp.Value);
v2MultiHeaders[kvp.Key] = values;
}
return v2MultiHeaders;
}
// See comment at the top about .NET Standard 2.0
#if !NETSTANDARD2_0
// Class representing the V1 API Gateway response. Very similiar to Amazon.Lambda.APIGatewayEvents.APIGatewayProxyResponse but this library can
// not take a dependency on Amazon.Lambda.APIGatewayEvents so it has to have its own version.
private class APIGatewayV1Response
{
[JsonPropertyName("statusCode")]
public int StatusCode { get; set; }
[JsonPropertyName("multiValueHeaders")]
public IDictionary> MultiValueHeaders { get; set; }
[JsonPropertyName("body")]
public string Body { get; set; }
[JsonPropertyName("isBase64Encoded")]
public bool IsBase64Encoded { get; set; }
}
// Class representing the V2 API Gateway response. Very similiar to Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse but this library can
// not take a dependency on Amazon.Lambda.APIGatewayEvents so it has to have its own version.
private class APIGatewayV2Response
{
[JsonPropertyName("statusCode")]
public int StatusCode { get; set; }
[JsonPropertyName("headers")]
public IDictionary Headers { get; set; }
public string[] Cookies { get; set; }
[JsonPropertyName("body")]
public string Body { get; set; }
[JsonPropertyName("isBase64Encoded")]
public bool IsBase64Encoded { get; set; }
}
#endif
#endregion
}
}