From f2c3c07bb4cfa9e90934ce03a99dc214279d79d2 Mon Sep 17 00:00:00 2001 From: Malhar Khimsaria <96malhar@gmail.com> Date: Mon, 17 Jun 2024 09:20:36 -0700 Subject: [PATCH] feat: Add support for SQS events in Amazon.Lambda.Annotations (#1758) --- Libraries/Amazon.Lambda.Annotations.slnf | 1 + ....Lambda.Annotations.SourceGenerator.csproj | 5 + .../Diagnostics/AnalyzerReleases.Shipped.md | 9 + .../Diagnostics/DiagnosticDescriptors.cs | 23 +- .../Generator.cs | 196 +-------- .../Attributes/AttributeModelBuilder.cs | 10 + .../Attributes/SQSEventAttributeBuilder.cs | 52 +++ .../Models/EventTypeBuilder.cs | 8 +- .../Models/ILambdaFunctionSerializable.cs | 8 +- .../Models/LambdaFunctionModel.cs | 9 +- .../Models/LambdaFunctionModelBuilder.cs | 62 ++- .../Models/LambdaMethodModel.cs | 17 +- .../Templates/LambdaFunctionTemplate.cs | 6 +- .../Templates/LambdaFunctionTemplate.tt | 4 +- .../TypeFullNames.cs | 10 +- .../Validation/LambdaFunctionValidator.cs | 208 +++++++++ .../Writers/CloudFormationWriter.cs | 222 ++++++++-- .../Writers/ITemplateWriter.cs | 7 + .../Writers/JsonWriter.cs | 30 +- .../Writers/YamlWriter.cs | 26 +- .../Amazon.Lambda.Annotations.csproj | 21 +- .../src/Amazon.Lambda.Annotations/README.md | 123 +++++- .../SQS/SQSEventAttribute.cs | 175 ++++++++ ....Annotations.SourceGenerators.Tests.csproj | 9 +- .../CSharpSourceGeneratorVerifier.cs | 2 + ...esWithBatchFailureReporting_Generated.g.cs | 57 +++ ...idSQSEvents_ProcessMessages_Generated.g.cs | 57 +++ .../complexCalculator.template | 16 +- .../customizeResponse.template | 59 ++- .../ServerlessTemplates/greeter.template | 18 +- .../greeter_executable.template | 18 +- .../nullreferenceexample.template | 8 +- .../simpleCalculator.template | 32 +- ...urcegeneratorserializationexample.template | 8 +- .../ServerlessTemplates/sqsEvents.template | 135 ++++++ .../SourceGeneratorTests.cs | 131 +++++- .../WriterTests/CloudFormationWriterTests.cs | 16 +- .../WriterTests/JsonWriterTests.cs | 39 ++ .../WriterTests/SQSEventsTests.cs | 399 ++++++++++++++++++ .../WriterTests/YamlWriterTests.cs | 40 ++ .../WriterTests/snapshot.json | 10 +- .../WriterTests/snapshot.yaml | 5 +- .../serverless.template | 84 +++- .../Helpers/LambdaHelper.cs | 9 + .../IntegrationTestContextFixture.cs | 4 +- .../SQSEventSourceMapping.cs | 43 ++ .../TestServerlessApp.IntegrationTests.csproj | 2 +- .../InvalidSQSEvents.cs.error | 87 ++++ .../SQSEventExamples/ValidSQSEvents.cs.txt | 33 ++ .../TestServerlessApp/SqsMessageProcessing.cs | 18 + .../TestServerlessApp.csproj | 1 + .../TestServerlessApp/serverless.template | 214 +++++++++- 52 files changed, 2480 insertions(+), 306 deletions(-) create mode 100644 Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs create mode 100644 Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs create mode 100644 Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SQS/ValidSQSEvents_ProcessMessagesWithBatchFailureReporting_Generated.g.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SQS/ValidSQSEvents_ProcessMessages_Generated.g.cs create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/sqsEvents.template create mode 100644 Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/SQSEventsTests.cs create mode 100644 Libraries/test/TestServerlessApp.IntegrationTests/SQSEventSourceMapping.cs create mode 100644 Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error create mode 100644 Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt create mode 100644 Libraries/test/TestServerlessApp/SqsMessageProcessing.cs diff --git a/Libraries/Amazon.Lambda.Annotations.slnf b/Libraries/Amazon.Lambda.Annotations.slnf index 1b89a7014..377a01b7e 100644 --- a/Libraries/Amazon.Lambda.Annotations.slnf +++ b/Libraries/Amazon.Lambda.Annotations.slnf @@ -7,6 +7,7 @@ "src\\Amazon.Lambda.Annotations\\Amazon.Lambda.Annotations.csproj", "src\\Amazon.Lambda.Core\\Amazon.Lambda.Core.csproj", "src\\Amazon.Lambda.RuntimeSupport\\Amazon.Lambda.RuntimeSupport.csproj", + "src\\Amazon.Lambda.SQSEvents\\Amazon.Lambda.SQSEvents.csproj", "src\\Amazon.Lambda.Serialization.SystemTextJson\\Amazon.Lambda.Serialization.SystemTextJson.csproj", "test\\Amazon.Lambda.Annotations.SourceGenerators.Tests\\Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj", "test\\TestExecutableServerlessApp\\TestExecutableServerlessApp.csproj", diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj index 622e6a0df..114feb426 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj @@ -4,6 +4,11 @@ 1.4.0 netstandard2.0 + + ..\..\..\buildtools\public.snk + true + true diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Shipped.md b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Shipped.md index e59823e20..f664c3c35 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Shipped.md +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Shipped.md @@ -1,6 +1,15 @@ ; Shipped analyzer releases ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +## Release 1.5.0 +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +AWSLambda0115 | AWSLambdaCSharpGenerator | Error | Invalid Usage of API Parameters +AWSLambda0116 | AWSLambdaCSharpGenerator | Error | Invalid SQSEventAttribute encountered +AWSLambda0117 | AWSLambdaCSharpGenerator | Error | Invalid Lambda Method Signature + ## Release 1.1.0 ### New Rules diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs index a511b4b60..fdf0813ec 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs @@ -103,7 +103,7 @@ public static class DiagnosticDescriptors category: "AWSLambdaCSharpGenerator", DiagnosticSeverity.Error, isEnabledByDefault: true); - + public static readonly DiagnosticDescriptor ExecutableWithNoFunctions = new DiagnosticDescriptor(id: "AWSLambda0113", title: "Executable output with no LambdaFunction annotations", messageFormat: "Your project is configured to output an executable and generate a static Main method, but you have not configured any methods with the 'LambdaFunction' attribute", @@ -117,5 +117,26 @@ public static class DiagnosticDescriptors category: "AWSLambdaCSharpGenerator", DiagnosticSeverity.Error, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor ApiParametersOnNonApiFunction = new DiagnosticDescriptor(id: "AWSLambda0115", + title: "Invalid Usage of API Parameters", + messageFormat: "The Lambda function parameters are annotated with HTTP API attributes but the Lambda function itself is not annotated with an HTTP API attribute", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidSqsEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0116", + title: "Invalid SQSEventAttribute", + messageFormat: "Invalid SQSEventAttribute encountered: {0}", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidLambdaMethodSignature = new DiagnosticDescriptor(id: "AWSLambda0117", + title: "Invalid Lambda Method Signature", + messageFormat: "Invalid Lambda method signature encountered: {0}", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); } } \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs index 01413fb16..ee14afd19 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Generator.cs @@ -2,18 +2,14 @@ using Amazon.Lambda.Annotations.SourceGenerator.Extensions; using Amazon.Lambda.Annotations.SourceGenerator.FileIO; using Amazon.Lambda.Annotations.SourceGenerator.Models; -using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; using Amazon.Lambda.Annotations.SourceGenerator.Templates; using Amazon.Lambda.Annotations.SourceGenerator.Writers; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Text; -using System.Text.RegularExpressions; namespace Amazon.Lambda.Annotations.SourceGenerator { @@ -41,12 +37,6 @@ public class Generator : ISourceGenerator "dotnet8" }; - // Only allow alphanumeric characters - private readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$"); - - // Regex for the 'Name' property for API Gateway attributes - https://docs.aws.amazon.com/apigateway/latest/developerguide/request-response-data-mappings.html - private readonly Regex _parameterAttributeNameRegex = new Regex("^[a-zA-Z0-9._$-]+$"); - public Generator() { #if DEBUG @@ -144,7 +134,7 @@ public void Execute(GeneratorExecutionContext context) } } - var configureMethodModel = semanticModelProvider.GetConfigureMethodModel(receiver.StartupClasses.FirstOrDefault()); + var configureMethodSymbol = semanticModelProvider.GetConfigureMethodModel(receiver.StartupClasses.FirstOrDefault()); var annotationReport = new AnnotationReport(); @@ -152,98 +142,38 @@ public void Execute(GeneratorExecutionContext context) var lambdaModels = new List(); - foreach (var lambdaMethod in receiver.LambdaMethods) + foreach (var lambdaMethodDeclarationSyntax in receiver.LambdaMethods) { - var lambdaMethodModel = semanticModelProvider.GetMethodSemanticModel(lambdaMethod); + var lambdaMethodSymbol = semanticModelProvider.GetMethodSemanticModel(lambdaMethodDeclarationSyntax); + var lambdaMethodLocation = lambdaMethodDeclarationSyntax.GetLocation(); - if (!HasSerializerAttribute(context, lambdaMethodModel)) + var lambdaFunctionModel = LambdaFunctionModelBuilder.BuildAndValidate(lambdaMethodSymbol, lambdaMethodLocation, configureMethodSymbol, context, isExecutable, defaultRuntime, diagnosticReporter); + if (!lambdaFunctionModel.IsValid) { - diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingLambdaSerializer, - lambdaMethod.GetLocation())); - + // If the model is not valid then skip it from further processing foundFatalError = true; continue; } - // Check for necessary references - if (lambdaMethodModel.HasAttribute(context, TypeFullNames.RestApiAttribute) - || lambdaMethodModel.HasAttribute(context, TypeFullNames.HttpApiAttribute)) - { - // Check for arbitrary type from "Amazon.Lambda.APIGatewayEvents" - if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.APIGatewayEvents") == null) - { - diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, - lambdaMethod.GetLocation(), - "Amazon.Lambda.APIGatewayEvents")); - - foundFatalError = true; - continue; - } - } - - var serializerInfo = GetSerializerInfoAttribute(context, lambdaMethodModel); - - var model = LambdaFunctionModelBuilder.Build(lambdaMethodModel, configureMethodModel, context, isExecutable, serializerInfo, defaultRuntime); - - // If there are more than one event, report them as errors - if (model.LambdaMethod.Events.Count > 1) - { - foreach (var attribute in lambdaMethodModel.GetAttributes().Where(attribute => TypeFullNames.Events.Contains(attribute.AttributeClass.ToDisplayString()))) - { - diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MultipleEventsNotSupported, - Location.Create(attribute.ApplicationSyntaxReference.SyntaxTree, attribute.ApplicationSyntaxReference.Span), - DiagnosticSeverity.Error)); - } - - foundFatalError = true; - // Skip multi-event lambda method from processing and check remaining lambda methods for diagnostics - continue; - } - if(model.LambdaMethod.ReturnsIHttpResults && !model.LambdaMethod.Events.Contains(EventType.API)) - { - diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.HttpResultsOnNonApiFunction, - Location.Create(lambdaMethod.SyntaxTree, lambdaMethod.Span), - DiagnosticSeverity.Error)); - - foundFatalError = true; - continue; - } - - if (!_resourceNameRegex.IsMatch(model.ResourceName)) - { - diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.InvalidResourceName, - Location.Create(lambdaMethod.SyntaxTree, lambdaMethod.Span), - DiagnosticSeverity.Error)); - - foundFatalError = true; - continue; - } - - if (!AreLambdaMethodParametersValid(lambdaMethod, model, diagnosticReporter)) - { - foundFatalError = true; - continue; - } - - var template = new LambdaFunctionTemplate(model); + var template = new LambdaFunctionTemplate(lambdaFunctionModel); string sourceText; try { sourceText = template.TransformText().ToEnvironmentLineEndings(); - context.AddSource($"{model.GeneratedMethod.ContainingType.Name}.g.cs", SourceText.From(sourceText, Encoding.UTF8, SourceHashAlgorithm.Sha256)); + context.AddSource($"{lambdaFunctionModel.GeneratedMethod.ContainingType.Name}.g.cs", SourceText.From(sourceText, Encoding.UTF8, SourceHashAlgorithm.Sha256)); } catch (Exception e) when (e is NotSupportedException || e is InvalidOperationException) { - diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.CodeGenerationFailed, Location.Create(lambdaMethod.SyntaxTree, lambdaMethod.Span), e.Message)); + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.CodeGenerationFailed, Location.Create(lambdaMethodDeclarationSyntax.SyntaxTree, lambdaMethodDeclarationSyntax.Span), e.Message)); return; } // report every generated file to build output - diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.CodeGeneration, Location.None, $"{model.GeneratedMethod.ContainingType.Name}.g.cs", sourceText)); + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.CodeGeneration, Location.None, $"{lambdaFunctionModel.GeneratedMethod.ContainingType.Name}.g.cs", sourceText)); - lambdaModels.Add(model); - annotationReport.LambdaFunctions.Add(model); + lambdaModels.Add(lambdaFunctionModel); + annotationReport.LambdaFunctions.Add(lambdaFunctionModel); } if (isExecutable) @@ -348,110 +278,10 @@ private static ExecutableAssembly GenerateExecutableAssemblySource( lambdaModels[0].LambdaMethod.ContainingNamespace); } - private bool HasSerializerAttribute(GeneratorExecutionContext context, IMethodSymbol methodModel) - { - return methodModel.ContainingAssembly.HasAttribute(context, TypeFullNames.LambdaSerializerAttribute); - } - - private LambdaSerializerInfo GetSerializerInfoAttribute(GeneratorExecutionContext context, IMethodSymbol methodModel) - { - var serializerString = DEFAULT_LAMBDA_SERIALIZER; - - ISymbol symbol = null; - - // First check if method has the Lambda Serializer. - if (methodModel.HasAttribute( - context, - TypeFullNames.LambdaSerializerAttribute)) - { - symbol = methodModel; - } - // Then check assembly - else if (methodModel.ContainingAssembly.HasAttribute( - context, - TypeFullNames.LambdaSerializerAttribute)) - { - symbol = methodModel.ContainingAssembly; - } - // Else return the default serializer. - else - { - return new LambdaSerializerInfo(serializerString); - } - - var attribute = symbol.GetAttributes().FirstOrDefault(attr => attr.AttributeClass.Name == TypeFullNames.LambdaSerializerAttributeWithoutNamespace); - - var serializerValue = attribute.ConstructorArguments.FirstOrDefault(kvp => kvp.Type.Name == nameof(Type)).Value; - - if (serializerValue != null) - { - serializerString = serializerValue.ToString(); - } - - return new LambdaSerializerInfo(serializerString); - } - public void Initialize(GeneratorInitializationContext context) { // Register a syntax receiver that will be created for each generation pass context.RegisterForSyntaxNotifications(() => new SyntaxReceiver(_fileManager, _directoryManager)); } - - private bool AreLambdaMethodParametersValid(MethodDeclarationSyntax declarationSyntax, LambdaFunctionModel model, DiagnosticReporter diagnosticReporter) - { - var isValid = true; - foreach (var parameter in model.LambdaMethod.Parameters) - { - if (parameter.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromQueryAttribute)) - { - var fromQueryAttribute = parameter.Attributes.First(att => att.Type.FullName == TypeFullNames.FromQueryAttribute) as AttributeModel; - // Use parameter name as key, if Name has not specified explicitly in the attribute definition. - var parameterKey = fromQueryAttribute?.Data?.Name ?? parameter.Name; - - if (!parameter.Type.IsPrimitiveType() && !parameter.Type.IsPrimitiveEnumerableType()) - { - isValid = false; - diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.UnsupportedMethodParameterType, - Location.Create(declarationSyntax.SyntaxTree, declarationSyntax.Span), - parameterKey, parameter.Type.FullName)); - } - } - - foreach (var att in parameter.Attributes) - { - var parameterAttributeName = string.Empty; - switch (att.Type.FullName) - { - case TypeFullNames.FromQueryAttribute: - var fromQueryAttribute = (AttributeModel)att; - parameterAttributeName = fromQueryAttribute.Data.Name; - break; - - case TypeFullNames.FromRouteAttribute: - var fromRouteAttribute = (AttributeModel)att; - parameterAttributeName = fromRouteAttribute.Data.Name; - break; - - case TypeFullNames.FromHeaderAttribute: - var fromHeaderAttribute = (AttributeModel)att; - parameterAttributeName = fromHeaderAttribute.Data.Name; - break; - - default: - break; - } - - if (!string.IsNullOrEmpty(parameterAttributeName) && !_parameterAttributeNameRegex.IsMatch(parameterAttributeName)) - { - isValid = false; - diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.InvalidParameterAttributeName, - Location.Create(declarationSyntax.SyntaxTree, declarationSyntax.Span), - parameterAttributeName, parameter.Name)); - } - } - } - - return isValid; - } } } \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs index 21419dcb8..fe18ca208 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs @@ -1,5 +1,6 @@ using System; using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.Annotations.SQS; using Microsoft.CodeAnalysis; namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes @@ -71,6 +72,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext Type = TypeModelBuilder.Build(att.AttributeClass, context) }; } + else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.SQSEventAttribute), SymbolEqualityComparer.Default)) + { + var data = SQSEventAttributeBuilder.Build(att); + model = new AttributeModel + { + Data = data, + Type = TypeModelBuilder.Build(att.AttributeClass, context) + }; + } else { model = new AttributeModel diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs new file mode 100644 index 000000000..c58063c48 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SQSEventAttributeBuilder.cs @@ -0,0 +1,52 @@ +using Amazon.Lambda.Annotations.SQS; +using Microsoft.CodeAnalysis; +using System; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes +{ + /// + /// Builder for . + /// + public class SQSEventAttributeBuilder + { + public static SQSEventAttribute Build(AttributeData att) + { + if (att.ConstructorArguments.Length != 1) + { + throw new NotSupportedException($"{TypeFullNames.SQSEventAttribute} must have constructor with 1 argument."); + } + var queue = att.ConstructorArguments[0].Value as string; + var data = new SQSEventAttribute(queue); + + foreach (var pair in att.NamedArguments) + { + if (pair.Key == nameof(data.ResourceName) && pair.Value.Value is string resourceName) + { + data.ResourceName = resourceName; + } + if (pair.Key == nameof(data.BatchSize) && pair.Value.Value is uint batchSize) + { + data.BatchSize = batchSize; + } + else if (pair.Key == nameof(data.Enabled) && pair.Value.Value is bool enabled) + { + data.Enabled = enabled; + } + else if (pair.Key == nameof(data.MaximumBatchingWindowInSeconds) && pair.Value.Value is uint maximumBatchingWindowInSeconds) + { + data.MaximumBatchingWindowInSeconds = maximumBatchingWindowInSeconds; + } + else if (pair.Key == nameof(data.Filters) && pair.Value.Value is string filters) + { + data.Filters = filters; + } + else if (pair.Key == nameof(data.MaximumConcurrency) && pair.Value.Value is uint maximumConcurrency) + { + data.MaximumConcurrency = maximumConcurrency; + } + } + + return data; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs index 246504491..f70260803 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs @@ -11,10 +11,10 @@ namespace Amazon.Lambda.Annotations.SourceGenerator.Models /// public class EventTypeBuilder { - public static List Build(IMethodSymbol lambdaMethodSymbol, + public static HashSet Build(IMethodSymbol lambdaMethodSymbol, GeneratorExecutionContext context) { - var events = new List(); + var events = new HashSet(); foreach (var attribute in lambdaMethodSymbol.GetAttributes()) { if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAttribute @@ -22,6 +22,10 @@ public static List Build(IMethodSymbol lambdaMethodSymbol, { events.Add(EventType.API); } + else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.SQSEventAttribute) + { + events.Add(EventType.SQS); + } } return events; diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs index 9ed23c88f..6cb6161fa 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ILambdaFunctionSerializable.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; namespace Amazon.Lambda.Annotations.SourceGenerator.Models @@ -69,6 +68,11 @@ public interface ILambdaFunctionSerializable /// IList Attributes { get; } + /// + /// The fully qualified name of the return type of the Lambda method written by the user. + /// + string ReturnTypeFullName { get; } + /// /// The assembly version of the Amazon.Lambda.Annotations.SourceGenerator package. /// diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs index 8ef99542c..fad1f5f53 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModel.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; @@ -73,7 +72,15 @@ public class LambdaFunctionModel : ILambdaFunctionSerializable /// public IList Attributes => LambdaMethod.Attributes ?? new List(); + /// + public string ReturnTypeFullName => LambdaMethod.ReturnType.FullName; + /// public string SourceGeneratorVersion { get; set; } + + /// + /// Indicates if the model is valid. + /// + public bool IsValid { get; set; } } } \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs index 545a5e8fa..f347789a7 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaFunctionModelBuilder.cs @@ -1,5 +1,8 @@ +using System; using System.Linq; -using System.Reflection; +using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics; +using Amazon.Lambda.Annotations.SourceGenerator.Extensions; +using Amazon.Lambda.Annotations.SourceGenerator.Validation; using Microsoft.CodeAnalysis; namespace Amazon.Lambda.Annotations.SourceGenerator.Models @@ -9,7 +12,20 @@ namespace Amazon.Lambda.Annotations.SourceGenerator.Models /// public static class LambdaFunctionModelBuilder { - public static LambdaFunctionModel Build(IMethodSymbol lambdaMethodSymbol, IMethodSymbol configureMethodSymbol, GeneratorExecutionContext context, bool isExecutable, LambdaSerializerInfo serializerInfo, string runtime) + public static LambdaFunctionModel BuildAndValidate(IMethodSymbol lambdaMethodSymbol, Location LambdamethodLocation, IMethodSymbol configureMethodSymbol, GeneratorExecutionContext context, bool isExecutable, string runtime, DiagnosticReporter diagnosticReporter) + { + // We need to check for the necessary dependencies before attempting to build the LambdaFunctionModel otherwise the generator blows up with unknown exceptions. + if (!LambdaFunctionValidator.ValidateDependencies(context, lambdaMethodSymbol, LambdamethodLocation, diagnosticReporter)) + { + return new LambdaFunctionModel() { IsValid = false }; + } + + var lambdaFunctionModel = Build(lambdaMethodSymbol, configureMethodSymbol, context, isExecutable, runtime); + lambdaFunctionModel.IsValid = LambdaFunctionValidator.ValidateFunction(context, lambdaMethodSymbol, LambdamethodLocation, lambdaFunctionModel, diagnosticReporter); + return lambdaFunctionModel; + } + + private static LambdaFunctionModel Build(IMethodSymbol lambdaMethodSymbol, IMethodSymbol configureMethodSymbol, GeneratorExecutionContext context, bool isExecutable, string runtime) { var lambdaMethod = LambdaMethodModelBuilder.Build(lambdaMethodSymbol, configureMethodSymbol, context); var generatedMethod = GeneratedMethodModelBuilder.Build(lambdaMethodSymbol, configureMethodSymbol, lambdaMethod, context); @@ -17,16 +33,54 @@ public static LambdaFunctionModel Build(IMethodSymbol lambdaMethodSymbol, IMetho { GeneratedMethod = generatedMethod, LambdaMethod = lambdaMethod, - SerializerInfo = serializerInfo, + SerializerInfo = GetSerializerInfoAttribute(context, lambdaMethodSymbol), StartupType = configureMethodSymbol != null ? TypeModelBuilder.Build(configureMethodSymbol.ContainingType, context) : null, SourceGeneratorVersion = context.Compilation .ReferencedAssemblyNames.FirstOrDefault(x => string.Equals(x.Name, "Amazon.Lambda.Annotations")) ?.Version.ToString(), IsExecutable = isExecutable, - Runtime = runtime + Runtime = runtime, }; return model; } + + private static LambdaSerializerInfo GetSerializerInfoAttribute(GeneratorExecutionContext context, IMethodSymbol methodModel) + { + var serializerString = TypeFullNames.DefaultLambdaSerializer; + + ISymbol symbol = null; + + // First check if method has the Lambda Serializer. + if (methodModel.HasAttribute( + context, + TypeFullNames.LambdaSerializerAttribute)) + { + symbol = methodModel; + } + // Then check assembly + else if (methodModel.ContainingAssembly.HasAttribute( + context, + TypeFullNames.LambdaSerializerAttribute)) + { + symbol = methodModel.ContainingAssembly; + } + // Else return the default serializer. + else + { + return new LambdaSerializerInfo(serializerString); + } + + var attribute = symbol.GetAttributes().FirstOrDefault(attr => attr.AttributeClass.Name == TypeFullNames.LambdaSerializerAttributeWithoutNamespace); + + var serializerValue = attribute.ConstructorArguments.FirstOrDefault(kvp => kvp.Type.Name == nameof(Type)).Value; + + if (serializerValue != null) + { + serializerString = serializerValue.ToString(); + } + + return new LambdaSerializerInfo(serializerString); + } } } \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaMethodModel.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaMethodModel.cs index 9a81ee95c..24caa34c1 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaMethodModel.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/LambdaMethodModel.cs @@ -64,6 +64,21 @@ public bool ReturnsIHttpResults } } + /// + /// Returns true if the Lambda function returns either void, Task, SQSBatchResponse or Task + /// + public bool ReturnsVoidTaskOrSqsBatchResponse + { + get + { + if (ReturnsVoid || ReturnsVoidTask || ReturnType.FullName == TypeFullNames.SQSBatchResponse) + { + return true; + } + return ReturnsGenericTask && ReturnType.TypeArguments.Count == 1 && ReturnType.TypeArguments[0].FullName == TypeFullNames.SQSBatchResponse; + } + } + /// /// Gets or sets the parameters of original method. If this method has no parameters, returns /// an empty list. @@ -99,7 +114,7 @@ public bool ReturnsIHttpResults /// /// Gets or sets type of Lambda event /// - public List Events { get; set; } + public HashSet Events { get; set; } /// /// Gets or sets the for the containing type. Returns null if the diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/LambdaFunctionTemplate.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/LambdaFunctionTemplate.cs index 0dfa338e2..ced24e1d1 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/LambdaFunctionTemplate.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/LambdaFunctionTemplate.cs @@ -182,7 +182,9 @@ public virtual string TransformText() this.Write(apiParameters.TransformText()); this.Write(new APIGatewayInvoke(_model, apiParameters.ParameterSignature).TransformText()); } - else if (_model.LambdaMethod.Events.Count == 0) + // Currently we only support 2 event types - APIGatewayEvents and SQSEvents. + // Since SQSEvents does not require any special code generation, the generated method body will be same as when the Lambda method contains no events. + else { this.Write(new NoEventMethodBody(_model).TransformText()); } @@ -206,7 +208,7 @@ private static void SetExecutionEnvironment() envValue.Append(""lib/amazon-lambda-annotations#"); - #line 82 "C:\codebase\V3\HLL\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\LambdaFunctionTemplate.tt" + #line 84 "C:\codebase\V3\HLL\aws-lambda-dotnet\Libraries\src\Amazon.Lambda.Annotations.SourceGenerator\Templates\LambdaFunctionTemplate.tt" this.Write(this.ToStringHelper.ToStringWithCulture(_model.SourceGeneratorVersion)); #line default diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/LambdaFunctionTemplate.tt b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/LambdaFunctionTemplate.tt index f1aa76cb0..e32430f70 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/LambdaFunctionTemplate.tt +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Templates/LambdaFunctionTemplate.tt @@ -60,7 +60,9 @@ this.Write(new FieldsAndConstructor(_model).TransformText()); this.Write(apiParameters.TransformText()); this.Write(new APIGatewayInvoke(_model, apiParameters.ParameterSignature).TransformText()); } - else if (_model.LambdaMethod.Events.Count == 0) + // Currently we only support 2 event types - APIGatewayEvents and SQSEvents. + // Since SQSEvents does not require any special code generation, the generated method body will be same as when the Lambda method contains no events. + else { this.Write(new NoEventMethodBody(_model).TransformText()); } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs index 6450a31b9..53a0cc610 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs @@ -32,8 +32,13 @@ public static class TypeFullNames public const string FromBodyAttribute = "Amazon.Lambda.Annotations.APIGateway.FromBodyAttribute"; public const string FromRouteAttribute = "Amazon.Lambda.Annotations.APIGateway.FromRouteAttribute"; + public const string SQSEvent = "Amazon.Lambda.SQSEvents.SQSEvent"; + public const string SQSBatchResponse = "Amazon.Lambda.SQSEvents.SQSBatchResponse"; + public const string SQSEventAttribute = "Amazon.Lambda.Annotations.SQS.SQSEventAttribute"; + public const string LambdaSerializerAttribute = "Amazon.Lambda.Core.LambdaSerializerAttribute"; - + public const string DefaultLambdaSerializer = "Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer"; + public const string LambdaSerializerAttributeWithoutNamespace = "LambdaSerializerAttribute"; public static HashSet Requests = new HashSet @@ -45,7 +50,8 @@ public static class TypeFullNames public static HashSet Events = new HashSet { RestApiAttribute, - HttpApiAttribute + HttpApiAttribute, + SQSEventAttribute }; } } \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs new file mode 100644 index 000000000..f8e31c0b0 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs @@ -0,0 +1,208 @@ +using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics; +using Amazon.Lambda.Annotations.SourceGenerator.Extensions; +using Amazon.Lambda.Annotations.SourceGenerator.Models; +using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; +using Amazon.Lambda.Annotations.SQS; +using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Validation +{ + internal static class LambdaFunctionValidator + { + // Only allow alphanumeric characters + private static readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$"); + + // Regex for the 'Name' property for API Gateway attributes - https://docs.aws.amazon.com/apigateway/latest/developerguide/request-response-data-mappings.html + private static readonly Regex _parameterAttributeNameRegex = new Regex("^[a-zA-Z0-9._$-]+$"); + + internal static bool ValidateFunction(GeneratorExecutionContext context, IMethodSymbol lambdaMethodSymbol, Location methodLocation, LambdaFunctionModel lambdaFunctionModel, DiagnosticReporter diagnosticReporter) + { + var diagnostics = new List(); + + // Validate the resource name + if (!_resourceNameRegex.IsMatch(lambdaFunctionModel.ResourceName)) + { + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidResourceName, methodLocation)); + } + + // Check for Serializer attribute + if (!lambdaMethodSymbol.ContainingAssembly.HasAttribute(context, TypeFullNames.LambdaSerializerAttribute)) + { + if (!lambdaMethodSymbol.HasAttribute(context, TypeFullNames.LambdaSerializerAttribute)) + { + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.MissingLambdaSerializer, methodLocation)); + } + } + + // Check for multiple event types + if (lambdaFunctionModel.LambdaMethod.Events.Count > 1) + { + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.MultipleEventsNotSupported, methodLocation)); + + // If multiple event types are encountered then return early without validating each individual event + // since at this point we do not know which event type does the user want to preserve + return ReportDiagnostics(diagnosticReporter, diagnostics); + } + + // Validate Events + ValidateApiGatewayEvents(lambdaFunctionModel, methodLocation, diagnostics); + ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics); + + return ReportDiagnostics(diagnosticReporter, diagnostics); + } + + internal static bool ValidateDependencies(GeneratorExecutionContext context, IMethodSymbol lambdaMethodSymbol, Location methodLocation, DiagnosticReporter diagnosticReporter) + { + // Check for references to "Amazon.Lambda.APIGatewayEvents" if the Lambda method is annotated with RestApi or HttpApi attributes. + if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.RestApiAttribute) || lambdaMethodSymbol.HasAttribute(context, TypeFullNames.HttpApiAttribute)) + { + if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.APIGatewayEvents") == null) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.APIGatewayEvents")); + return false; + } + } + + // Check for references to "Amazon.Lambda.SQSEvents" if the Lambda method is annotated with SQSEvent attribute. + if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.SQSEventAttribute)) + { + if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.SQSEvents") == null) + { + diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.SQSEvents")); + return false; + } + } + + return true; + } + + private static void ValidateApiGatewayEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List diagnostics) + { + // If the method does not contain any APIGatewayEvents, then it cannot return IHttpResults and can also not have parameters that are annotated with HTTP API attributes + if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.API)) + { + if (lambdaFunctionModel.LambdaMethod.ReturnsIHttpResults) + { + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.HttpResultsOnNonApiFunction, methodLocation)); + } + + foreach (var parameter in lambdaFunctionModel.LambdaMethod.Parameters) + { + if (parameter.Attributes.Any(att => + att.Type.FullName == TypeFullNames.FromBodyAttribute || + att.Type.FullName == TypeFullNames.FromHeaderAttribute || + att.Type.FullName == TypeFullNames.FromRouteAttribute || + att.Type.FullName == TypeFullNames.FromQueryAttribute)) + { + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.ApiParametersOnNonApiFunction, methodLocation)); + } + } + + return; + } + + // Validate FromRoute, FromQuery and FromHeader parameters + foreach (var parameter in lambdaFunctionModel.LambdaMethod.Parameters) + { + if (parameter.Attributes.Any(att => att.Type.FullName == TypeFullNames.FromQueryAttribute)) + { + var fromQueryAttribute = parameter.Attributes.First(att => att.Type.FullName == TypeFullNames.FromQueryAttribute) as AttributeModel; + // Use parameter name as key, if Name has not specified explicitly in the attribute definition. + var parameterKey = fromQueryAttribute?.Data?.Name ?? parameter.Name; + + if (!parameter.Type.IsPrimitiveType() && !parameter.Type.IsPrimitiveEnumerableType()) + { + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.UnsupportedMethodParameterType, methodLocation, parameter.Name, parameter.Type.FullName)); + } + } + + foreach (var att in parameter.Attributes) + { + var parameterAttributeName = string.Empty; + switch (att.Type.FullName) + { + case TypeFullNames.FromQueryAttribute: + var fromQueryAttribute = (AttributeModel)att; + parameterAttributeName = fromQueryAttribute.Data.Name; + break; + + case TypeFullNames.FromRouteAttribute: + var fromRouteAttribute = (AttributeModel)att; + parameterAttributeName = fromRouteAttribute.Data.Name; + break; + + case TypeFullNames.FromHeaderAttribute: + var fromHeaderAttribute = (AttributeModel)att; + parameterAttributeName = fromHeaderAttribute.Data.Name; + break; + + default: + break; + } + + if (!string.IsNullOrEmpty(parameterAttributeName) && !_parameterAttributeNameRegex.IsMatch(parameterAttributeName)) + { + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidParameterAttributeName, methodLocation, parameterAttributeName, parameter.Name)); + } + } + } + } + + private static void ValidateSqsEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List diagnostics) + { + // If the method does not contain any SQS events, then simply return early + if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.SQS)) + { + return; + } + + // Validate SQSEventAttributes + foreach (var att in lambdaFunctionModel.Attributes) + { + if (att.Type.FullName != TypeFullNames.SQSEventAttribute) + continue; + + var sqsEventAttribute = ((AttributeModel)att).Data; + var validationErrors = sqsEventAttribute.Validate(); + validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidSqsEventAttribute, methodLocation, errorMessage))); + } + + // Validate method parameters - When using SQSEventAttribute, the method signature must be (SQSEvent sqsEvent) or (SQSEvent sqsEvent, ILambdaContext context) + var parameters = lambdaFunctionModel.LambdaMethod.Parameters; + if (parameters.Count == 0 || + parameters.Count > 2 || + (parameters.Count == 1 && parameters[0].Type.FullName != TypeFullNames.SQSEvent) || + (parameters.Count == 2 && (parameters[0].Type.FullName != TypeFullNames.SQSEvent || parameters[1].Type.FullName != TypeFullNames.ILambdaContext))) + { + var errorMessage = $"When using the {nameof(SQSEventAttribute)}, the Lambda method can accept at most 2 parameters. " + + $"The first parameter is required and must be of type {TypeFullNames.SQSEvent}. " + + $"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}."; + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage)); + } + + // Validate method return type - When using SQSEventAttribute, the return type must be either void, Task, SQSBatchResponse or Task + if (!lambdaFunctionModel.LambdaMethod.ReturnsVoidTaskOrSqsBatchResponse) + { + var errorMessage = $"When using the {nameof(SQSEventAttribute)}, the Lambda method can return either void, {TypeFullNames.Task}, {TypeFullNames.SQSBatchResponse} or Task<{TypeFullNames.SQSBatchResponse}>"; + diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage)); + } + } + + private static bool ReportDiagnostics(DiagnosticReporter diagnosticReporter, List diagnostics) + { + var isValid = true; + foreach (var diagnostic in diagnostics) + { + diagnosticReporter.Report(diagnostic); + if (diagnostic.Severity == DiagnosticSeverity.Error) + { + isValid = false; + } + } + return isValid; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs index 3f0cd08c7..993a312ed 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs @@ -3,11 +3,11 @@ using Amazon.Lambda.Annotations.SourceGenerator.FileIO; using Amazon.Lambda.Annotations.SourceGenerator.Models; using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; +using Amazon.Lambda.Annotations.SQS; using Microsoft.CodeAnalysis; using System; using System.Collections.Generic; using System.ComponentModel; -using System.IO; using System.Linq; using System.Reflection; @@ -183,6 +183,7 @@ private void ProcessPackageTypeProperty(ILambdaFunctionSerializable lambdaFuncti private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable lambdaFunction) { var currentSyncedEvents = new List(); + var currentSyncedEventProperties = new Dictionary>(); foreach (var attributeModel in lambdaFunction.Attributes) { @@ -190,70 +191,137 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la switch (attributeModel) { case AttributeModel httpApiAttributeModel: - eventName = ProcessHttpApiAttribute(lambdaFunction, httpApiAttributeModel.Data); + eventName = ProcessHttpApiAttribute(lambdaFunction, httpApiAttributeModel.Data, currentSyncedEventProperties); currentSyncedEvents.Add(eventName); break; case AttributeModel restApiAttributeModel: - eventName = ProcessRestApiAttribute(lambdaFunction, restApiAttributeModel.Data); + eventName = ProcessRestApiAttribute(lambdaFunction, restApiAttributeModel.Data, currentSyncedEventProperties); + currentSyncedEvents.Add(eventName); + break; + case AttributeModel sqsAttributeModel: + eventName = ProcessSqsAttribute(lambdaFunction, sqsAttributeModel.Data, currentSyncedEventProperties); currentSyncedEvents.Add(eventName); break; } } - var eventsPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events"; - var syncedEventsMetadataPath = $"Resources.{lambdaFunction.ResourceName}.Metadata.SyncedEvents"; - var previousSyncedEvents = _templateWriter.GetToken>(syncedEventsMetadataPath, new List()); - - // Remove all events that exist in the serverless template but were not encountered during the current source generation pass. - foreach (var previousEventName in previousSyncedEvents) - { - if (!currentSyncedEvents.Contains(previousEventName)) - _templateWriter.RemoveToken($"{eventsPath}.{previousEventName}"); - } - - if (currentSyncedEvents.Any()) - _templateWriter.SetToken(syncedEventsMetadataPath, currentSyncedEvents, TokenType.List); - else - _templateWriter.RemoveToken(syncedEventsMetadataPath); + SynchronizeEventsAndProperties(currentSyncedEvents, currentSyncedEventProperties, lambdaFunction); } /// /// Writes all properties associated with to the serverless template. /// - private string ProcessRestApiAttribute(ILambdaFunctionSerializable lambdaFunction, RestApiAttribute restApiAttribute) + private string ProcessRestApiAttribute(ILambdaFunctionSerializable lambdaFunction, RestApiAttribute restApiAttribute, Dictionary> syncedEventProperties) { - var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events"; - var methodName = restApiAttribute.Method.ToString(); - var methodPath = $"{eventPath}.Root{methodName}"; + var eventName = $"Root{restApiAttribute.Method}"; + var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}"; - _templateWriter.SetToken($"{methodPath}.Type", "Api"); - _templateWriter.SetToken($"{methodPath}.Properties.Path", restApiAttribute.Template); - _templateWriter.SetToken($"{methodPath}.Properties.Method", methodName.ToUpper()); + _templateWriter.SetToken($"{eventPath}.Type", "Api"); + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Path", restApiAttribute.Template); + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Method", restApiAttribute.Method.ToString().ToUpper()); - return $"Root{methodName}"; + return eventName; } /// /// Writes all properties associated with to the serverless template. /// - private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunction, HttpApiAttribute httpApiAttribute) + private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunction, HttpApiAttribute httpApiAttribute, Dictionary> syncedEventProperties) { - var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events"; - var methodName = httpApiAttribute.Method.ToString(); - var methodPath = $"{eventPath}.Root{methodName}"; + var eventName = $"Root{httpApiAttribute.Method}"; + var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}"; - _templateWriter.SetToken($"{methodPath}.Type", "HttpApi"); - _templateWriter.SetToken($"{methodPath}.Properties.Path", httpApiAttribute.Template); - _templateWriter.SetToken($"{methodPath}.Properties.Method", methodName.ToUpper()); + _templateWriter.SetToken($"{eventPath}.Type", "HttpApi"); + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Path", httpApiAttribute.Template); + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Method", httpApiAttribute.Method.ToString().ToUpper()); // Only set the PayloadFormatVersion for 1.0. // If no PayloadFormatVersion is specified then by default 2.0 is used. if (httpApiAttribute.Version == HttpApiVersion.V1) - _templateWriter.SetToken($"{methodPath}.Properties.PayloadFormatVersion", "1.0"); + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "PayloadFormatVersion", "1.0"); - return $"Root{methodName}"; + return eventName; } + /// + /// Writes all properties associated with to the serverless template. + /// + private string ProcessSqsAttribute(ILambdaFunctionSerializable lambdaFunction, SQSEventAttribute att, Dictionary> syncedEventProperties) + { + var eventName = att.ResourceName; + var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}"; + + // Set event type - https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-eventsource.html#sam-function-eventsource-type + _templateWriter.SetToken($"{eventPath}.Type", "SQS"); + + // Set SQS properties - https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-sqs.html + + // Queue + // Remove Queue if set previously + _templateWriter.RemoveToken($"{eventPath}.Properties.Queue"); + if (!att.Queue.StartsWith("@")) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Queue", att.Queue); + } + else + { + var queue = att.Queue.Substring(1); + if (_templateWriter.Exists($"{PARAMETERS}.{queue}")) + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Queue.{REF}", queue); + else + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Queue.{GET_ATTRIBUTE}", new List { queue, "Arn" }, TokenType.List); + } + + // BatchSize + if (att.IsBatchSizeSet) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "BatchSize", att.BatchSize); + } + + // Enabled + if (att.IsEnabledSet) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Enabled", att.Enabled); + } + + // FilterCriteria + if (att.IsFiltersSet) + { + const char SEPERATOR = ';'; + var filters = att.Filters.Split(SEPERATOR).Select(x => x.Trim()).ToList(); + var filterList = new List>(); + foreach (var filter in filters) + { + filterList.Add(new Dictionary { { "Pattern", filter } }); + } + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "FilterCriteria.Filters", filterList, TokenType.List); + } + + // FunctionResponseTypes + if (lambdaFunction.ReturnTypeFullName.Contains(TypeFullNames.SQSBatchResponse)) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "FunctionResponseTypes", new List { "ReportBatchItemFailures" }, TokenType.List); + } + + // MaximumBatchingWindowInSeconds + if (att.IsMaximumBatchingWindowInSecondsSet) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "MaximumBatchingWindowInSeconds", att.MaximumBatchingWindowInSeconds); + } + + // ScalingConfig + if (att.IsMaximumConcurrencySet) + { + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "ScalingConfig.MaximumConcurrency", att.MaximumConcurrency); + } + + return att.ResourceName; + } + + /// + /// Writes all properties associated with to the serverless template. + /// + /// /// Writes the default values for the Lambda function's metadata and properties. /// @@ -462,5 +530,89 @@ private string ExtractCurrentDescriptionSuffix(string templateDescription) return string.Empty; } + + /// + /// This sets the event property for a given event and property path. + /// It also keeps track of which properties have been set for each event so that we can remove any orphaned properties later. + /// + private void SetEventProperty(Dictionary> syncedEventProperties, string lambdaResourceName, string eventResourceName, string propertyPath, object value, TokenType tokenType = TokenType.Other) + { + _templateWriter.SetToken($"Resources.{lambdaResourceName}.Properties.Events.{eventResourceName}.Properties.{propertyPath}", value, tokenType); + if (!syncedEventProperties.ContainsKey(eventResourceName)) + { + syncedEventProperties[eventResourceName] = new List(); + } + syncedEventProperties[eventResourceName].Add(propertyPath); + } + + /// + /// Synchronizes events and their properties for a given lambda function in its CloudFormation metadata. + /// + /// List of events to synchronize. + /// Dictionary containing event properties to synchronize. + /// The lambda function for which to synchronize events and properties. + private void SynchronizeEventsAndProperties(List syncedEvents, Dictionary> syncedEventProperties, ILambdaFunctionSerializable lambdaFunction) + { + // Construct paths for synced events in the resource template. + var eventsPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events"; + var syncedEventsPath = $"Resources.{lambdaFunction.ResourceName}.Metadata.SyncedEvents"; + + // Get previously synced events. + var previousSyncedEvents = _templateWriter.GetToken>(syncedEventsPath, new List()); + + // Remove orphaned events. + var orphanedEvents = previousSyncedEvents.Except(syncedEvents).ToList(); + orphanedEvents.ForEach(eventName => _templateWriter.RemoveToken($"{eventsPath}.{eventName}")); + + // Update synced events in the template. + _templateWriter.RemoveToken(syncedEventsPath); + if (syncedEvents.Any()) + _templateWriter.SetToken(syncedEventsPath, syncedEvents, TokenType.List); + + // Construct path for synced event properties in the resource template. + var syncedEventPropertiesPath = $"Resources.{lambdaFunction.ResourceName}.Metadata.SyncedEventProperties"; + + // Get previously synced event properties. + var previousSyncedEventProperties = _templateWriter.GetToken>>(syncedEventPropertiesPath, new Dictionary>()); + + // Remove orphaned event properties. + foreach (var eventName in previousSyncedEventProperties.Keys.Intersect(syncedEventProperties.Keys)) + { + var orphanedEventProperties = previousSyncedEventProperties[eventName].Except(syncedEventProperties[eventName]).ToList(); + orphanedEventProperties.ForEach(propertyPath => + { + // If previously a property existed as a terminal property but now exists as complex property then do not delete it. + // This can happen when a property was previously added as an ARN by is now being added as a Ref. + if (syncedEventProperties[eventName].Any(p => p.StartsWith(propertyPath))) + { + return; + } + + _templateWriter.RemoveToken($"{eventsPath}.{eventName}.Properties.{propertyPath}"); + + // Remove the terminal property and parent properties if they're now empty. + // Consider the following example: + // { + // "A": { + // "B": { + // "C": "D" + // } + // } + // } + // If A.B.C is removed, then A.B and A must also be removed since they're now empty because of the cascading effects. + var propertyPathList = propertyPath.Split('.').ToList(); + while (propertyPathList.Any()) + { + _templateWriter.RemoveTokenIfNullOrEmpty($"{eventsPath}.{eventName}.Properties.{string.Join(".", propertyPathList)}"); + propertyPathList.RemoveAt(propertyPathList.Count - 1); + } + }); + } + + // Update synced event properties in the template. + _templateWriter.RemoveToken(syncedEventPropertiesPath); + if (syncedEventProperties.Any()) + _templateWriter.SetToken(syncedEventPropertiesPath, syncedEventProperties, TokenType.KeyVal); + } } } \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/ITemplateWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/ITemplateWriter.cs index 3712bccbd..209d6c315 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/ITemplateWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/ITemplateWriter.cs @@ -43,6 +43,13 @@ public interface ITemplateWriter /// dot(.) seperated path. Example "Person.Name.FirstName" void RemoveToken(string path); + /// + /// Deletes the token found at the dot(.) separated path if it points to a null value or empty object. + /// It does not do anything if the jsonPath does not exist. + /// + /// dot(.) seperated path. Example "Person.Name.FirstName" + void RemoveTokenIfNullOrEmpty(string path); + /// /// Returns the template as a string /// diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/JsonWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/JsonWriter.cs index 1290b91eb..9bacea74b 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/JsonWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/JsonWriter.cs @@ -39,7 +39,15 @@ public bool Exists(string jsonPath) { return false; } - currentNode = currentNode[property]; + try + { + currentNode = currentNode[property]; + } + // If the currentNode is already a leaf value then we will encounter an InvalidOperationException + catch (InvalidOperationException) + { + return false; + } } return currentNode != null; @@ -174,6 +182,26 @@ public void RemoveToken(string jsonPath) currentNode.Remove(lastProperty); } + /// + public void RemoveTokenIfNullOrEmpty(string jsonPath) + { + if (!Exists(jsonPath)) + { + return; + } + + JToken currentNode = _rootNode; + foreach (var property in jsonPath.Split('.')) + { + currentNode = currentNode[property]; + } + + if (currentNode.Type == JTokenType.Null || (currentNode.Type == JTokenType.Object && !currentNode.HasValues)) + { + RemoveToken(jsonPath); + } + } + /// /// Returns the template as a string /// diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/YamlWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/YamlWriter.cs index 9b89e717d..78db53886 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/YamlWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/YamlWriter.cs @@ -38,7 +38,7 @@ public bool Exists(string yamlPath) YamlNode currentNode = _rootNode; foreach (var property in yamlPath.Split('.')) { - if (currentNode == null) + if (currentNode == null || currentNode.NodeType != YamlNodeType.Mapping) { return false; } @@ -213,6 +213,30 @@ public void RemoveToken(string yamlPath) terminalNode.Children.Remove(lastProperty); } + /// + public void RemoveTokenIfNullOrEmpty(string yamlPath) + { + if (!Exists(yamlPath)) + { + return; + } + + YamlNode currentNode = _rootNode; + foreach (var property in yamlPath.Split('.')) + { + currentNode = currentNode[property]; + } + + if (currentNode is YamlScalarNode scalarNode && (scalarNode.Value == "null" || scalarNode.Value == string.Empty)) + { + RemoveToken(yamlPath); + } + else if (currentNode is YamlMappingNode mappingNode && mappingNode.Children.Count == 0) + { + RemoveToken(yamlPath); + } + } + /// /// Parses the YAML string as a /// diff --git a/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj b/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj index dfa34f2c0..c90d519af 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj +++ b/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj @@ -4,8 +4,13 @@ 1.4.0 netstandard2.0;net6.0;net8.0 true - - + + + ..\..\..\buildtools\public.snk + true + + IL2026,IL2067,IL2075,IL3050 true @@ -14,5 +19,15 @@ - + + + + + <_Parameter1>Amazon.Lambda.Annotations.SourceGenerator, PublicKey="0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4" + + + <_Parameter1>Amazon.Lambda.Annotations.SourceGenerators.Tests, PublicKey="0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4" + + + \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations/README.md b/Libraries/src/Amazon.Lambda.Annotations/README.md index 31dc65772..3de36aa1b 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/README.md +++ b/Libraries/src/Amazon.Lambda.Annotations/README.md @@ -5,14 +5,27 @@ idiomatic .NET coding patterns. [C# Source Generators](https://docs.microsoft.co gap between the Lambda programming model to the Lambda Annotations programming model without adding any performance penalty. Topics: -* [Getting Started](#getting-started) -* [How does Lambda Annotations work?](#how-does-lambda-annotations-work) -* [Dependency Injection integration](#dependency-injection-integration) -* [Synchronizing CloudFormation template](#synchronizing-cloudFormation-template) -* [Getting build information](#getting-build-information) -* [Amazon API Gateway example](#amazon-api-gateway-example) -* [Amazon S3 example](#amazon-s3-example) -* [Lambda .NET Attributes Reference](#lambda-net-attributes-reference) +- [Amazon.Lambda.Annotations](#amazonlambdaannotations) + - [How does Lambda Annotations work?](#how-does-lambda-annotations-work) + - [Getting started](#getting-started) + - [Visual Studio 2022](#visual-studio-2022) + - [.NET CLI](#net-cli) + - [The sample project](#the-sample-project) + - [Deployment](#deployment) + - [Adding Lambda Annotations to an existing project](#adding-lambda-annotations-to-an-existing-project) + - [Dependency Injection integration](#dependency-injection-integration) + - [Synchronizing CloudFormation template](#synchronizing-cloudformation-template) + - [Lambda Global Properties](#lambda-global-properties) + - [Amazon API Gateway example](#amazon-api-gateway-example) + - [Amazon S3 example](#amazon-s3-example) + - [SQS Event Example](#sqs-event-example) + - [Getting build information](#getting-build-information) + - [Lambda .NET Attributes Reference](#lambda-net-attributes-reference) + - [Event Attributes](#event-attributes) + - [Parameter Attributes](#parameter-attributes) + - [Customizing responses for API Gateway Lambda functions](#customizing-responses-for-api-gateway-lambda-functions) + - [Content-Type](#content-type) + - [Project References](#project-references) ## How does Lambda Annotations work? @@ -709,6 +722,98 @@ public class Functions_Resize_Generated } ``` +## SQS Event Example +This example shows how users can use the `SQSEvent` attribute to set up event source mapping between their SQS queues and Lambda function. + +The `SQSEvent` attribute contains the following properties: +* **Queue** (Required) - The SQS queue that will act as the event trigger for the Lambda function. This can either be the queue ARN or reference to the SQS queue resource that is already defined in the serverless template. To reference a SQS queue resource in the serverless template, prefix the resource name with "@" symbol. +* **ResourceName** (Optional) - The CloudFormation resource name for the SQS event source mapping. By default this is set to the SQS queue name if `Queue` is set to an SQS queue ARN. If `Queue` is set to an existing CloudFormation resource, than that is used as the default value without the "@" prefix. +* **Enabled** (Optional) - If set to false, the event source mapping will be disabled and message polling will be paused. Default value is true. +* **BatchSize** (Optional) - The maximum number of messages that will be sent for processing in a single batch. This value must be between 1 to 10000. For FIFO queues the maximum allowed value is 10. Default value is 10. +* **MaximumBatchingWindowInSeconds** (Optional) - The maximum amount of time, in seconds, to gather records before invoking the function. This value must be between 0 to 300. Default value is 0. When BatchSize is set to a value greater than 10 MaximumBatchingWindowInSeconds must be set to at least 1. This property must not be set if the event source mapping is being created for a FIFO queue. +* **Filters** (Optional) - A collection of semicolon (;) separated strings where each string denotes a pattern. Only those SQS messages that conform to at least 1 pattern will be forwarded to the Lambda function for processing. +* **MaximumConcurrency** (Optional) - The maximum number of concurrent Lambda invocations that the SQS queue can trigger. This value must be between 2 to 1000. The default value is 1000. + +The `SQSEvent` attribute must be applied to Lambda method along with the `LambdaFunction` attribute. + +The Lambda method must conform to the following rules when it is tagged with the `SQSEvent` attribute: + + 1. It must have at least 1 argument and can have at most 2 arguments. + - The first argument is required and must be of type `SQSEvent` defined in the [Amazon.Lambda.SQSEvents](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.SQSEvents/SQSEvent.cs) package. + - The second argument is optional and must be of type `ILambdaContext` defined in the [Amazon.Lambda.Core](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs) package. + 2. The method return type must be one of `void`, `Task`, `SQSBatchResponse` or `Task`. The `SQSBatchResponse` type is defined in the [Amazon.Lambda.SQSEvents](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.SQSEvents/SQSBatchResponse.cs) package. If the return type is `SQSBatchResponse` or `Task`, then the [FunctionResponseTypes](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-sqs.html#sam-function-sqs-functionresponsetypes) in the event source mapping is set to report `ReportBatchItemFailures` + +```csharp + +[LambdaFunction(ResourceName = "SQSMessageHandler", Policies = "AWSLambdaSQSQueueExecutionRole", PackageType = LambdaPackageType.Image)] +[SQSEvent("@TestQueue", ResourceName = "TestQueueEvent", BatchSize = 50, MaximumConcurrency = 5, MaximumBatchingWindowInSeconds = 5, Filters = "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }")] +public SQSBatchResponse HandleMessage(SQSEvent evnt, ILambdaContext lambdaContext) +{ + lambdaContext.Logger.Log($"Received {evnt.Records.Count} messages"); + return new SQSBatchResponse(); +} +``` +In the above example `TestQueue` refers to an existing SQS queue resource in the CloudFormation template. + +The following SQS event source mapping will be generated for the `SQSMessageHandler` Lambda function + +```json + "SQSMessageHandler": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "TestQueueEvent" + ] + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaSQSQueueExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" + ] + }, + "Events": { + "TestQueueEvent": { + "Type": "SQS", + "Properties": { + "Queue": { + "Fn::GetAtt": [ + "TestQueue", + "Arn" + ] + }, + "BatchSize": 50, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" + } + ] + }, + "FunctionResponseTypes": [ + "ReportBatchItemFailures" + ], + "MaximumBatchingWindowInSeconds": 5, + "ScalingConfig": { + "MaximumConcurrency": 5 + } + } + } + } + } + }, + "TestQueue": { + "Type": "AWS::SQS::Queue" + } +``` + ## Getting build information The source generator integrates with MSBuild's compiler error and warning reporting when there are problems generating the boiler plate code. @@ -741,6 +846,8 @@ parameter to the `LambdaFunction` must be the event object and the event source * Configures the Lambda function to be called from an API Gateway REST API. The HTTP method and resource path are required to be set on the attribute. * HttpApi * Configures the Lambda function to be called from an API Gateway HTTP API. The HTTP method, HTTP API payload version and resource path are required to be set on the attribute. +* SQSEvent + * Sets up event source mapping between the Lambda function and SQS queues. The SQS queue ARN is required to be set on the attribute. If users want to pass a reference to an existing SQS queue resource defined in their CloudFormation template, they can pass the SQS queue resource name prefixed with the '@' symbol. ### Parameter Attributes diff --git a/Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs new file mode 100644 index 000000000..3358eee36 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Amazon.Lambda.Annotations.SQS +{ + /// + /// This attribute defines the SQS event source configuration for a Lambda function. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class SQSEventAttribute : Attribute + { + // Except for Queue all other properties are optional. + // .NET attributes cannot be nullable. To work around this, we have added nullable backing fields to all optional properties and added an internal IsSet method to identify which properties were explicitly set the customer. + // These internal methods are used by the CloudFormationWriter while deciding which properties to write in the CF template. + + // Only allow alphanumeric characters + private static readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$"); + + /// + /// The SQS queue that will act as the event trigger for the Lambda function. + /// This can either be the queue ARN or reference to the SQS queue resource that is already defined in the serverless template. + /// To reference a SQS queue resource in the serverless template, prefix the resource name with "@" symbol. + /// + public string Queue { get; set; } + + /// + /// The CloudFormation resource name for the SQS event source mapping. By default this is set to the SQS queue name if the is set to an SQS queue ARN. + /// If is set to an existing CloudFormation resource, than that is used as the default value without the "@" prefix. + /// + public string ResourceName + { + get + { + if (IsResourceNameSet) + { + return resourceName; + } + if (Queue.StartsWith("@")) + { + return Queue.Substring(1); + } + + var arnTokens = Queue.Split(new char[] { ':' }, 6); + var queueName = arnTokens[5]; + var sanitizedQueueName = string.Join(string.Empty, queueName.Where(char.IsLetterOrDigit)); + return sanitizedQueueName; + } + set => resourceName = value; + } + + private string resourceName { get; set; } = null; + internal bool IsResourceNameSet => resourceName != null; + + + /// + /// If set to false, the event source mapping will be disabled and message polling will be paused. + /// Default value is true. + /// + public bool Enabled + { + get => enabled.GetValueOrDefault(); + set => enabled = value; + } + private bool? enabled { get; set; } + internal bool IsEnabledSet => enabled.HasValue; + + /// + /// The maximum number of messages that will be sent for processing in a single batch. + /// This value must be between 1 to 10000. For FIFO queues the maximum allowed value is 10. Default value is 10. + /// + public uint BatchSize + { + get => batchSize.GetValueOrDefault(); + set => batchSize = value; + } + private uint? batchSize { get; set; } + internal bool IsBatchSizeSet => batchSize.HasValue; + + /// + /// The maximum amount of time, in seconds, to gather records before invoking the function. + /// This value must be between 0 to 300. Default value is 0. + /// When is set to a value greater than 10 must be set to at least 1. + /// This property must not be set if the event source mapping is being created for a FIFO queue. + /// + public uint MaximumBatchingWindowInSeconds + { + get => maximumBatchingWindowInSeconds.GetValueOrDefault(); + set => maximumBatchingWindowInSeconds = value; + } + private uint? maximumBatchingWindowInSeconds { get; set; } + internal bool IsMaximumBatchingWindowInSecondsSet => maximumBatchingWindowInSeconds.HasValue; + + /// + /// A collection of semicolon (;) separated strings where each string denotes a pattern. + /// Only those SQS messages that conform to at least 1 pattern will be forwarded to the Lambda function for processing. + /// + public string Filters { get; set; } = null; + internal bool IsFiltersSet => Filters != null; + + /// + /// The maximum number of concurrent Lambda invocations that the SQS queue can trigger. + /// This value must be between 2 to 1000. The default value is 1000. + /// + public uint MaximumConcurrency + { + get => maximumConcurrency.GetValueOrDefault(); + set => maximumConcurrency = value; + } + private uint? maximumConcurrency { get; set; } + internal bool IsMaximumConcurrencySet => maximumConcurrency.HasValue; + + /// + /// Creates an instance of the class. + /// + /// property"/> + public SQSEventAttribute(string queue) + { + Queue = queue; + } + + internal List Validate() + { + var validationErrors = new List(); + + if (IsBatchSizeSet && (BatchSize < 1 || BatchSize > 10000)) + { + validationErrors.Add($"{nameof(SQSEventAttribute.BatchSize)} = {BatchSize}. It must be between 1 and 10000"); + } + if (IsMaximumConcurrencySet && (MaximumConcurrency < 2 || MaximumConcurrency > 1000)) + { + validationErrors.Add($"{nameof(SQSEventAttribute.MaximumConcurrency)} = {MaximumConcurrency}. It must be between 2 and 1000"); + } + if (IsMaximumBatchingWindowInSecondsSet && (MaximumBatchingWindowInSeconds < 0 || MaximumBatchingWindowInSeconds > 300)) + { + validationErrors.Add($"{nameof(SQSEventAttribute.MaximumBatchingWindowInSeconds)} = {MaximumBatchingWindowInSeconds}. It must be between 0 and 300"); + } + if (IsBatchSizeSet && BatchSize > 10 && (!IsMaximumBatchingWindowInSecondsSet || MaximumBatchingWindowInSeconds < 1)) + { + validationErrors.Add($"{nameof(SQSEventAttribute.MaximumBatchingWindowInSeconds)} is not set or set to a value less than 1. " + + $"It must be set to at least 1 when {nameof(SQSEventAttribute.BatchSize)} is greater than 10"); + } + + // The queue is FIFO if the queue ARN ends in ".fifo" + var isFifo = !Queue.StartsWith("@") && Queue.EndsWith(".fifo"); + if (isFifo) + { + if (IsMaximumBatchingWindowInSecondsSet) + { + validationErrors.Add($"{nameof(SQSEventAttribute.MaximumBatchingWindowInSeconds)} must not be set when the event source mapping is for a FIFO queue"); + } + if (IsBatchSizeSet && BatchSize > 10) + { + validationErrors.Add($"{nameof(SQSEventAttribute.BatchSize)} = {BatchSize}. It must be less than or equal to 10 when the event source mapping is for a FIFO queue"); + } + } + + if (!Queue.StartsWith("@")) + { + var arnTokens = Queue.Split(new char[] { ':' }, 6); + if (arnTokens.Length != 6) + { + validationErrors.Add($"{nameof(SQSEventAttribute.Queue)} = {Queue}. The SQS queue ARN is invalid. The ARN format is 'arn::sqs:::'"); + } + } + if (IsResourceNameSet && !_resourceNameRegex.IsMatch(ResourceName)) + { + validationErrors.Add($"{nameof(SQSEventAttribute.ResourceName)} = {ResourceName}. It must only contain alphanumeric characters and must not be an empty string"); + } + + return validationErrors; + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj index 1d89cbeb6..525f4b668 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj @@ -4,7 +4,12 @@ net6.0 true latest - + + + ..\..\..\buildtools\public.snk + true + @@ -150,6 +155,7 @@ + @@ -171,6 +177,7 @@ + diff --git a/Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error b/Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error new file mode 100644 index 000000000..0b90371a7 --- /dev/null +++ b/Libraries/test/TestServerlessApp/SQSEventExamples/InvalidSQSEvents.cs.error @@ -0,0 +1,87 @@ +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.Annotations.SQS; +using Amazon.Lambda.SQSEvents; +using System; + +namespace TestServerlessApp.SQSEventExamples +{ + // This file represents invalid usage of the SQSEventAttribute. + // This file is sent as input to the source generator unit tests and we assert that compilation errors are thrown with the appropriate diagnostic message. + // Refer to the VerifyInvalidSQSEvents_ThrowsCompilationErrors unit test. + + public class InvalidSQSEvents + { + [LambdaFunction] + [SQSEvent("@testQueue", BatchSize = 0, MaximumBatchingWindowInSeconds = 302, MaximumConcurrency = 1)] + public void ProcessMessageWithInvalidSQSEventAttributes(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction] + [SQSEvent("@testQueue")] + public void ProcessMessageWithInvalidParameters(SQSEvent evnt, bool invalidParameter1, int invalidParameter2) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction] + [SQSEvent("@testQueue")] + public bool ProcessMessageWithInvalidReturnType(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + return true; + } + + [LambdaFunction] + [RestApi(LambdaHttpMethod.Get, "/")] + [SQSEvent("@testQueue")] + public void ProcessMessageWithMultipleEventTypes(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction] + [SQSEvent("test-queue")] + public void ProcessMessageWithInvalidQueueArn(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction] + [SQSEvent("@testQueue", ResourceName = "sqs-event-source")] + public void ProcessMessageWithInvalidResourceName(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction] + [SQSEvent("@testQueue", ResourceName = "")] + public void ProcessMessageWithEmptyResourceName(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction] + [SQSEvent("@testQueue", BatchSize = 100)] + public void ProcessMessageWithMaximumBatchingWindowInSecondsNotSpecified(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction] + [SQSEvent("@testQueue", BatchSize = 100, MaximumBatchingWindowInSeconds = 0)] + public void ProcessMessageWithMaximumBatchingWindowInSecondsLessThanOne(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction] + [SQSEvent("arn:aws:sqs:us-east-2:444455556666:test-queue.fifo", BatchSize = 100, MaximumBatchingWindowInSeconds = 5)] + public void ProcessMessageWithFifoQueue(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + } +} diff --git a/Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt b/Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt new file mode 100644 index 000000000..28327c33f --- /dev/null +++ b/Libraries/test/TestServerlessApp/SQSEventExamples/ValidSQSEvents.cs.txt @@ -0,0 +1,33 @@ +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.SQS; +using Amazon.Lambda.SQSEvents; +using System; +using System.Threading.Tasks; + +namespace TestServerlessApp.SQSEventExamples +{ + // This file represents valid usage of the SQSEventAttribute. This is added as .txt file since we do not want to deploy these functions during our integration tests. + // This file is only sent as input to the source generator unit tests. + // Refer to VerifyValidSQSEvents unit test. + + public class ValidSQSEvents + { + [LambdaFunction] + [SQSEvent("arn:aws:sqs:us-east-2:444455556666:queue1", BatchSize = 50, MaximumBatchingWindowInSeconds = 2, MaximumConcurrency = 30, Filters = "My-Filter-1; My-Filter-2")] + [SQSEvent("arn:aws:sqs:us-east-2:444455556666:queue2", MaximumBatchingWindowInSeconds = 5, Enabled = false)] + [SQSEvent("arn:aws:sqs:us-east-2:444455556666:my-queue")] + [SQSEvent("@testQueue", ResourceName = "testQueueEvent")] + public void ProcessMessages(SQSEvent evnt) + { + Console.WriteLine($"Event processed: {evnt}"); + } + + [LambdaFunction] + [SQSEvent("arn:aws:sqs:us-east-2:444455556666:queue3")] + public async Task ProcessMessagesWithBatchFailureReporting(SQSEvent evnt) + { + await Console.Out.WriteLineAsync($"Event processed: {evnt}"); + return new SQSBatchResponse(); + } + } +} diff --git a/Libraries/test/TestServerlessApp/SqsMessageProcessing.cs b/Libraries/test/TestServerlessApp/SqsMessageProcessing.cs new file mode 100644 index 000000000..7562be678 --- /dev/null +++ b/Libraries/test/TestServerlessApp/SqsMessageProcessing.cs @@ -0,0 +1,18 @@ +using Amazon.Lambda.Annotations; +using Amazon.Lambda.Annotations.SQS; +using Amazon.Lambda.Core; +using Amazon.Lambda.SQSEvents; + +namespace TestServerlessApp +{ + public class SqsMessageProcessing + { + [LambdaFunction(ResourceName = "SQSMessageHandler", Policies = "AWSLambdaSQSQueueExecutionRole", PackageType = LambdaPackageType.Image)] + [SQSEvent("@TestQueue", ResourceName = "TestQueueEvent", BatchSize = 50, MaximumConcurrency = 5, MaximumBatchingWindowInSeconds = 5, Filters = "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }")] + public SQSBatchResponse HandleMessage(SQSEvent evnt, ILambdaContext lambdaContext) + { + lambdaContext.Logger.Log($"Received {evnt.Records.Count} messages"); + return new SQSBatchResponse(); + } + } +} diff --git a/Libraries/test/TestServerlessApp/TestServerlessApp.csproj b/Libraries/test/TestServerlessApp/TestServerlessApp.csproj index 37fbb5832..512ac2170 100644 --- a/Libraries/test/TestServerlessApp/TestServerlessApp.csproj +++ b/Libraries/test/TestServerlessApp/TestServerlessApp.csproj @@ -25,6 +25,7 @@ + diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index 6f9725c31..34078ba43 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -108,7 +108,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -140,7 +146,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -172,7 +184,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -204,7 +222,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -236,7 +260,14 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } }, "Properties": { "MemorySize": 512, @@ -269,7 +300,14 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } }, "Properties": { "MemorySize": 512, @@ -322,7 +360,14 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } }, "Properties": { "MemorySize": 1024, @@ -355,7 +400,14 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } }, "Properties": { "MemorySize": 512, @@ -388,7 +440,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootPost" - ] + ], + "SyncedEventProperties": { + "RootPost": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -420,7 +478,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootPost" - ] + ], + "SyncedEventProperties": { + "RootPost": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -452,7 +516,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -484,7 +554,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -516,7 +592,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -548,7 +630,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -580,7 +668,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -692,7 +786,13 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method" + ] + } }, "Properties": { "MemorySize": 512, @@ -744,7 +844,14 @@ "Tool": "Amazon.Lambda.Annotations", "SyncedEvents": [ "RootGet" - ] + ], + "SyncedEventProperties": { + "RootGet": [ + "Path", + "Method", + "PayloadFormatVersion" + ] + } }, "Properties": { "MemorySize": 512, @@ -770,6 +877,70 @@ } } } + }, + "SQSMessageHandler": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "TestQueueEvent" + ], + "SyncedEventProperties": { + "TestQueueEvent": [ + "Queue.Fn::GetAtt", + "BatchSize", + "FilterCriteria.Filters", + "FunctionResponseTypes", + "MaximumBatchingWindowInSeconds", + "ScalingConfig.MaximumConcurrency" + ] + } + }, + "Properties": { + "MemorySize": 512, + "Timeout": 30, + "Policies": [ + "AWSLambdaSQSQueueExecutionRole" + ], + "PackageType": "Image", + "ImageUri": ".", + "ImageConfig": { + "Command": [ + "TestServerlessApp::TestServerlessApp.SqsMessageProcessing_HandleMessage_Generated::HandleMessage" + ] + }, + "Events": { + "TestQueueEvent": { + "Type": "SQS", + "Properties": { + "Queue": { + "Fn::GetAtt": [ + "TestQueue", + "Arn" + ] + }, + "BatchSize": 50, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "{ \"body\" : { \"RequestCode\" : [ \"BBBB\" ] } }" + } + ] + }, + "FunctionResponseTypes": [ + "ReportBatchItemFailures" + ], + "MaximumBatchingWindowInSeconds": 5, + "ScalingConfig": { + "MaximumConcurrency": 5 + } + } + } + } + } + }, + "TestQueue": { + "Type": "AWS::SQS::Queue" } }, "Outputs": { @@ -784,6 +955,15 @@ "Value": { "Fn::Sub": "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" } + }, + "TestQueueARN": { + "Description": "ARN of the TestQueue resource", + "Value": { + "Fn::GetAtt": [ + "TestQueue", + "Arn" + ] + } } } } \ No newline at end of file