diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 000000000..6c531aa51 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,45 @@ +name: .NET + +on: + push: + branches: [ "master","feature/annotations-enchilada","feature/annotations-queue" ] +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Get Branch Name + run: echo "BRANCH_NAME=${GITHUB_REF##*/}" >> $GITHUB_ENV + - name: Setup Version + run: echo "VERSION=0.6.${{ github.run_number }}-$BRANCH_NAME" >> $GITHUB_ENV + - name: Echo Branch Name + run: echo $BRANCH_NAME + - name: Setup .NET 3.1 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 3.1.x + - name: Setup .NET 6 + uses: actions/setup-dotnet@v2 + with: + dotnet-version: 6.0.x + - name: Setup .NET 7.0 + uses: actions/setup-dotnet@v1 + with: + include-prerelease: true + dotnet-version: 7.0.100-preview.6.22352.1 + - name: Restore dependencies + run: dotnet restore Libraries/Libraries.sln + - name: Build + run: dotnet build Libraries/Libraries.sln --no-restore + - name: Test + run: dotnet test Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj + +# - cd Libraries +# - dotnet build -c Release +# - dotnet test test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj --results-directory $CODEBUILD_SRC_DIR/TestResults --logger trx +# - VERSION="0.5.1-preview-$CODEBUILD_BUILD_NUMBER-$Branch" +# - dotnet pack -c Release src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj -p:NuspecFile=$CODEBUILD_SRC_DIR/Libraries/src/Amazon.Lambda.Annotations.nuspec -p:NuspecProperties="Version=0.5.1-preview-$CODEBUILD_BUILD_NUMBER-$Branch" -p:TargetFrameworks=netstandard2.0 +# - dotnet pack -c Release src/Amazon.Lambda.Annotations.SourceGenerator/Amazon.Lambda.Annotations.SourceGenerator.csproj -p:NuspecProperties="Version=0.5.1-preview-$CODEBUILD_BUILD_NUMBER-$Branch" -p:TargetFrameworks=netstandard2.0 + diff --git a/Libraries/Libraries.sln b/Libraries/Libraries.sln index 17cdfc1ba..c70c2ab7f 100644 --- a/Libraries/Libraries.sln +++ b/Libraries/Libraries.sln @@ -107,6 +107,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestServerlessApp", "test\TestServerlessApp\TestServerlessApp.csproj", "{3D322CAB-0DDD-4C84-B3ED-0862F244AF5C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.Annotations.SourceGenerators.Tests", "test\Amazon.Lambda.Annotations.SourceGenerators.Tests\Amazon.Lambda.Annotations.SourceGenerators.Tests.csproj", "{D76F2C74-3D7F-4DB3-BA1A-F2EA14749253}" + ProjectSection(ProjectDependencies) = postProject + {3D322CAB-0DDD-4C84-B3ED-0862F244AF5C} = {3D322CAB-0DDD-4C84-B3ED-0862F244AF5C} + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.Annotations", "src\Amazon.Lambda.Annotations\Amazon.Lambda.Annotations.csproj", "{ADA9AF37-A8C1-47E6-BBBD-5C7E49C26C0E}" EndProject @@ -126,14 +129,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestMinimalAPIApp", "test\T EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.MQEvents", "src\Amazon.Lambda.MQEvents\Amazon.Lambda.MQEvents.csproj", "{BF85932E-2DFF-41CD-8090-A672468B8FBB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amazon.Lambda.LexV2Events", "src\Amazon.Lambda.LexV2Events\Amazon.Lambda.LexV2Events.csproj", "{3C6AABF5-0372-41E0-874F-DF18ECCC7FB6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Amazon.Lambda.LexV2Events", "src\Amazon.Lambda.LexV2Events\Amazon.Lambda.LexV2Events.csproj", "{3C6AABF5-0372-41E0-874F-DF18ECCC7FB6}" EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - test\EventsTests.Shared\EventsTests.Shared.projitems*{44e9d925-b61d-4234-97b7-61424c963ba6}*SharedItemsImports = 5 - test\EventsTests.Shared\EventsTests.Shared.projitems*{a2cb78bb-e54f-48ca-bbfb-9553d27ef23d}*SharedItemsImports = 13 - test\EventsTests.Shared\EventsTests.Shared.projitems*{c1bb30d2-3237-4cfc-ba93-627471148ec2}*SharedItemsImports = 5 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -412,4 +410,9 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {503678A4-B8D1-4486-8915-405A3E9CF0EB} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + test\EventsTests.Shared\EventsTests.Shared.projitems*{44e9d925-b61d-4234-97b7-61424c963ba6}*SharedItemsImports = 5 + test\EventsTests.Shared\EventsTests.Shared.projitems*{a2cb78bb-e54f-48ca-bbfb-9553d27ef23d}*SharedItemsImports = 13 + test\EventsTests.Shared\EventsTests.Shared.projitems*{c1bb30d2-3237-4cfc-ba93-627471148ec2}*SharedItemsImports = 5 + EndGlobalSection EndGlobal 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..50ca2785e 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs @@ -71,6 +71,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext Type = TypeModelBuilder.Build(att.AttributeClass, context) }; } + else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.SqsMessageAttribute), SymbolEqualityComparer.Default)) + { + var data = SqsMessageAttributeBuilder.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/SqsMessageAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SqsMessageAttributeBuilder.cs new file mode 100644 index 000000000..367ec9c63 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/SqsMessageAttributeBuilder.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Newtonsoft.Json.Linq; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes +{ + internal class SqsMessageAttributeBuilder + { + private const string RedriveAllPolicyNotValidJsonExceptionMessage = "RedriveAllPolicy must be valid Json"; + + public static SqsMessageAttribute Build(AttributeData att) + { + var data = new SqsMessageAttribute(); + foreach (var attNamedArgument in att.NamedArguments) + { + switch (attNamedArgument.Key) + { + case nameof(ISqsMessage.EventQueueARN): + data.EventQueueARN = attNamedArgument.Value.Value.ToString(); + break; + case nameof(ISqsMessage.EventBatchSize): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.EventBatchSize = uint.Parse(attNamedArgument.Value.Value.ToString()); + } + break; + case nameof(ISqsMessage.VisibilityTimeout): + data.VisibilityTimeout = uint.Parse(attNamedArgument.Value.Value.ToString()); + break; + case nameof(ISqsMessage.ContentBasedDeduplication): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value.ToString())) + { + data.ContentBasedDeduplication = bool.Parse(attNamedArgument.Value.Value.ToString()); + } + break; + case nameof(ISqsMessage.DeduplicationScope): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.DeduplicationScope = attNamedArgument.Value.Value.ToString(); + } + break; + case nameof(ISqsMessage.DelaySeconds): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.DelaySeconds = uint.Parse(attNamedArgument.Value.Value.ToString()); + } + break; + case nameof(ISqsMessage.FifoQueue): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.FifoQueue = bool.Parse(attNamedArgument.Value.Value.ToString()); + } + break; + case nameof(ISqsMessage.FifoThroughputLimit): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.FifoThroughputLimit = attNamedArgument.Value.Value.ToString(); + } + break; + case nameof(ISqsMessage.KmsDataKeyReusePeriodSeconds): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.KmsDataKeyReusePeriodSeconds = uint.Parse(attNamedArgument.Value.Value.ToString()); + } + break; + case nameof(ISqsMessage.KmsMasterKeyId): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.KmsMasterKeyId = attNamedArgument.Value.Value.ToString(); + } + break; + // MaximumMessageSize + case nameof(ISqsMessage.MaximumMessageSize): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.MaximumMessageSize = uint.Parse(attNamedArgument.Value.Value.ToString()); + } + break; + // Queue + case nameof(ISqsMessage.QueueName): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.QueueName = attNamedArgument.Value.Value.ToString(); + } + break; + // MessageRetentionPeriod + case nameof(ISqsMessage.MessageRetentionPeriod): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.MessageRetentionPeriod = uint.Parse(attNamedArgument.Value.Value.ToString()); + } + break; + //ReceiveMessageWaitTimeSeconds + case nameof(ISqsMessage.ReceiveMessageWaitTimeSeconds): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.ReceiveMessageWaitTimeSeconds = uint.Parse(attNamedArgument.Value.Value.ToString()); + } + break; + //RedriveAllowPolicy + case nameof(ISqsMessage.RedriveAllowPolicy): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + var json = attNamedArgument.Value.Value.ToString(); + try + { + JObject.Parse(json); + } + catch (Exception e) + { + + throw new ArgumentOutOfRangeException(nameof(ISqsMessage.RedriveAllowPolicy), SqsMessageAttributeBuilder.RedriveAllPolicyNotValidJsonExceptionMessage); + } + data.RedriveAllowPolicy = json; + } + break; + // RedrivePolicy + case nameof(ISqsMessage.RedrivePolicy): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.RedrivePolicy = attNamedArgument.Value.Value.ToString(); + } + break; + // Tags + case nameof(ISqsMessage.Tags): + if (attNamedArgument.Value.Values.Any()) + { + var final = new List(); + + foreach (var pair in attNamedArgument.Value.Values) + { + final.Add(pair.Value.ToString()); + } + + data.Tags = final.ToArray(); + } + break; + case nameof(ISqsMessage.EventFilterCriteria): + if (attNamedArgument.Value.Values.Any()) + { + var final = new List(); + + foreach (var pair in attNamedArgument.Value.Values) + { + final.Add(pair.Value.ToString()); + } + + data.EventFilterCriteria = final.ToArray(); + } + break; + /// MaximumBatchingWindowInSeconds + case nameof(ISqsMessage.EventMaximumBatchingWindowInSeconds): + if (!string.IsNullOrEmpty(attNamedArgument.Value.Value?.ToString())) + { + data.EventMaximumBatchingWindowInSeconds = uint.Parse(attNamedArgument.Value.Value.ToString()); + } + break; + + + default: + throw new NotSupportedException(attNamedArgument.Key); + } + } + + //if (data.FifoQueue && !string.IsNullOrEmpty(data.QueueName) && !data.QueueName.EndsWith(".fifo")) + //{ + // throw new ArgumentOutOfRangeException(nameof(SqsMessageAttribute.QueueName), $"If using {nameof(SqsMessageAttribute.FifoQueue)} = true, {nameof(SqsMessageAttribute.QueueName)} must end in '.fifo'"); + //} + + return data; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ISqsMessageSerializable.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ISqsMessageSerializable.cs new file mode 100644 index 000000000..843013452 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ISqsMessageSerializable.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models +{ + public interface ISqsMessageSerializable : ISqsMessage + { + string SourceGeneratorVersion { get; set; } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ISqsQueueSerializable.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ISqsQueueSerializable.cs new file mode 100644 index 000000000..0b39a4e48 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/ISqsQueueSerializable.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json.Linq; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models +{ + public interface ISqsQueueSerializable + { + string QueueLogicalId { get; set; } + JToken QueueName { get; set; } + uint EventBatchSize { get; set; } + string[] EventFilterCriteria { get; set; } + string EventQueueARN { get; set; } + bool ContentBasedDeduplication { get; set; } + uint EventMaximumBatchingWindowInSeconds { get; set; } + string DeduplicationScope { get; set; } + uint DelaySeconds { get; set; } + bool FifoQueue { get; set; } + string FifoThroughputLimit { get; set; } + uint KmsDataKeyReusePeriodSeconds { get; set; } + string KmsMasterKeyId { get; set; } + uint MaximumMessageSize { get; set; } + uint MessageRetentionPeriod { get; set; } + uint ReceiveMessageWaitTimeSeconds { get; set; } + string RedriveAllowPolicy { get; set; } + string RedrivePolicy { get; set; } + string[] Tags { get; set; } + uint VisibilityTimeout { get; set; } + + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/SqsQueueModel.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/SqsQueueModel.cs new file mode 100644 index 000000000..85c01e922 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/SqsQueueModel.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json.Linq; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models +{ + public class SqsQueueModel : ISqsQueueSerializable + { + public string QueueLogicalId { get; set; } + public JToken QueueName { get; set; } + public uint EventBatchSize { get; set; } + public string[] EventFilterCriteria { get; set; } + public string EventQueueARN { get; set; } + public bool ContentBasedDeduplication { get; set; } + public uint EventMaximumBatchingWindowInSeconds { get; set; } + public string DeduplicationScope { get; set; } + public uint DelaySeconds { get; set; } + public bool FifoQueue { get; set; } + public string FifoThroughputLimit { get; set; } + public uint KmsDataKeyReusePeriodSeconds { get; set; } + public string KmsMasterKeyId { get; set; } + public uint MaximumMessageSize { get; set; } + public uint MessageRetentionPeriod { get; set; } + public uint ReceiveMessageWaitTimeSeconds { get; set; } + public string RedriveAllowPolicy { get; set; } + public string RedrivePolicy { get; set; } + public string[] Tags { get; set; } + public uint VisibilityTimeout { get; set; } + } +} \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/SqsQueueModelBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/SqsQueueModelBuilder.cs new file mode 100644 index 000000000..583e5d187 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/SqsQueueModelBuilder.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json.Linq; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models +{ + public static class SqsQueueModelBuilder + { + public static ISqsQueueSerializable Build(ILambdaFunctionSerializable lambdaFunction, SqsMessageAttribute sqsMessageAttribute) + { + ISqsQueueSerializable sqsQueueModel = new SqsQueueModel() + { + DelaySeconds = sqsMessageAttribute.DelaySeconds, + DeduplicationScope = sqsMessageAttribute.DeduplicationScope, + KmsDataKeyReusePeriodSeconds = sqsMessageAttribute.KmsDataKeyReusePeriodSeconds, + FifoThroughputLimit = sqsMessageAttribute.FifoThroughputLimit, + FifoQueue = sqsMessageAttribute.FifoQueue, + VisibilityTimeout = sqsMessageAttribute.VisibilityTimeout, + Tags = sqsMessageAttribute.Tags, + ContentBasedDeduplication = sqsMessageAttribute.ContentBasedDeduplication, + EventMaximumBatchingWindowInSeconds = sqsMessageAttribute.EventMaximumBatchingWindowInSeconds, + EventFilterCriteria = sqsMessageAttribute.EventFilterCriteria, + EventBatchSize = sqsMessageAttribute.EventBatchSize, + ReceiveMessageWaitTimeSeconds = sqsMessageAttribute.ReceiveMessageWaitTimeSeconds, + MessageRetentionPeriod = sqsMessageAttribute.MessageRetentionPeriod, + MaximumMessageSize = sqsMessageAttribute.MaximumMessageSize, + RedriveAllowPolicy = sqsMessageAttribute.RedriveAllowPolicy, + RedrivePolicy = sqsMessageAttribute.RedrivePolicy, + KmsMasterKeyId = sqsMessageAttribute.KmsMasterKeyId, + EventQueueARN = sqsMessageAttribute.EventQueueARN, + // Only set the QueueLogicalId if the EventQueueArn is not set + // because the QueueLogicalId becomes irrelevent if + // the function is using an existing queue + QueueLogicalId = string.IsNullOrEmpty(sqsMessageAttribute.EventQueueARN) ? lambdaFunction.Name + "Queue" : null + }; + if (!string.IsNullOrEmpty(sqsMessageAttribute.QueueName)) + { + if (sqsMessageAttribute.QueueName.TrimStart(' ','\t').StartsWith("{") + && sqsMessageAttribute.QueueName.TrimEnd(' ', '\t').EndsWith("}")) + { + sqsQueueModel.QueueName = JObject.Parse(sqsMessageAttribute.QueueName); + } + else + { + sqsQueueModel.QueueName = new JValue(sqsMessageAttribute.QueueName); + + } + } + else if (sqsMessageAttribute.FifoQueue) + { + sqsQueueModel.QueueName = new JValue(lambdaFunction.Name + "Queue.fifo"); + } + + return sqsQueueModel; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs index b521f6c00..552f87494 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs @@ -19,6 +19,7 @@ public static class TypeFullNames public const string APIGatewayHttpApiV2ProxyResponse = "Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse"; public const string LambdaFunctionAttribute = "Amazon.Lambda.Annotations.LambdaFunctionAttribute"; + public const string SqsMessageAttribute = "Amazon.Lambda.Annotations.SqsMessageAttribute"; public const string FromServiceAttribute = "Amazon.Lambda.Annotations.FromServicesAttribute"; public const string HttpApiVersion = "Amazon.Lambda.Annotations.APIGateway.HttpApiVersion"; diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationJsonWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationJsonWriter.cs index dd06c4f4c..38d276c7a 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationJsonWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationJsonWriter.cs @@ -49,6 +49,8 @@ public void ApplyReport(AnnotationReport report) } RemoveOrphanedLambdaFunctions(processedLambdaFunctions); + RemoveOrphanedResources(); + var json = _jsonWriter.GetPrettyJson(); _fileManager.WriteAllText(report.CloudFormationTemplatePath, json); @@ -131,6 +133,7 @@ private void ProcessPackageTypeProperty(ILambdaFunctionSerializable lambdaFuncti private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable lambdaFunction) { var currentSyncedEvents = new List(); + var currentSyncedResources = new List(); foreach (var attributeModel in lambdaFunction.Attributes) { @@ -145,11 +148,24 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la eventName = ProcessRestApiAttribute(lambdaFunction, restApiAttributeModel.Data); currentSyncedEvents.Add(eventName); break; + case AttributeModel sqsAttributeModel: + ISqsQueueSerializable sqsQueue = SqsQueueModelBuilder.Build(lambdaFunction, sqsAttributeModel.Data); + eventName = ProcessSqsMessageAttribute(lambdaFunction, sqsQueue); + currentSyncedEvents.Add(eventName); + if (ShouldProcessQueue(lambdaFunction, sqsQueue)) + { + string queueLogicalId = ProcessQueue(sqsQueue); + currentSyncedResources.Add(queueLogicalId); + _processedResources.Add(queueLogicalId); + } + break; } } var eventsPath = $"Resources.{lambdaFunction.Name}.Properties.Events"; - var syncedEventsMetadataPath = $"Resources.{lambdaFunction.Name}.Metadata.SyncedEvents"; + var metadataPath = $"Resources.{lambdaFunction.Name}.Metadata"; + var syncedEventsMetadataPath = $"{metadataPath}.SyncedEvents"; + var syncedResourcesMetadataPath = $"{metadataPath}.SyncedResources"; if (_jsonWriter.GetToken(syncedEventsMetadataPath, new JArray()) is JArray previousSyncedEvents) { @@ -160,12 +176,25 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la } } + if (_jsonWriter.GetToken(syncedResourcesMetadataPath, new JArray()) is JArray previousSyncedResources) + { + foreach (var previousSyncedResource in previousSyncedResources.Select(x => x.ToObject())) + { + if (!currentSyncedResources.Contains(previousSyncedResource)) + _jsonWriter.RemoveToken($"{eventsPath}.{previousSyncedResources}"); + } + } + if (currentSyncedEvents.Any()) _jsonWriter.SetToken(syncedEventsMetadataPath, new JArray(currentSyncedEvents)); else _jsonWriter.RemoveToken(syncedEventsMetadataPath); - } + if (currentSyncedResources.Any()) + _jsonWriter.SetToken(syncedResourcesMetadataPath, new JArray(currentSyncedResources)); + else + _jsonWriter.RemoveToken(syncedResourcesMetadataPath); + } private string ProcessRestApiAttribute(ILambdaFunctionSerializable lambdaFunction, RestApiAttribute restApiAttribute) { var eventPath = $"Resources.{lambdaFunction.Name}.Properties.Events"; @@ -193,7 +222,6 @@ private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunctio return $"Root{methodName}"; } - private void ApplyLambdaFunctionDefaults(string lambdaFunctionPath, string propertiesPath) { _jsonWriter.SetToken($"{lambdaFunctionPath}.Type", "AWS::Serverless::Function"); @@ -205,13 +233,11 @@ private void ApplyLambdaFunctionDefaults(string lambdaFunctionPath, string prope _jsonWriter.SetToken($"{propertiesPath}.Timeout", 30); _jsonWriter.SetToken($"{propertiesPath}.Policies", new JArray("AWSLambdaBasicExecutionRole")); } - private void CreateNewTemplate() { var content = @"{'AWSTemplateFormatVersion' : '2010-09-09', 'Transform' : 'AWS::Serverless-2016-10-31'}"; _jsonWriter.Parse(content); } - private void RemoveOrphanedLambdaFunctions(HashSet processedLambdaFunctions) { var resourceToken = _jsonWriter.GetToken("Resources") as JObject; @@ -248,5 +274,405 @@ private JToken GetValueOrRef(string value) refNode["Ref"] = value.Substring(1); return refNode; } + + // I don't like this being a field member, but given + // the previous patterns, I can't find a better method + private readonly HashSet _processedResources = new HashSet(); + private string ProcessQueue(ISqsQueueSerializable data) + { + //var sqsQueueTemplateInfo = GetSqsQueueLogicalIdAndPath(data); + var queueResourcePath = $"Resources.{data.QueueLogicalId}"; + var propertiesPath = $"{queueResourcePath}.Properties"; + + if (!_jsonWriter.Exists(queueResourcePath)) + ApplyQueueDefaults(queueResourcePath, propertiesPath); + + ProcessQueueAttributes(data, propertiesPath); + + return data.QueueLogicalId; + } + private void ProcessQueueAttributes(ISqsQueueSerializable sqsMessageAttribute, string propertiesPath) + { + try + { + try + { + WriteOrRemove($"{propertiesPath}.{nameof(ISqsMessage.ContentBasedDeduplication)}", sqsMessageAttribute.ContentBasedDeduplication, SqsMessageAttribute.ContentBasedDeduplicationDefault); + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.ContentBasedDeduplication)}", e); + } + + try + { + WriteOrRemove($"{propertiesPath}.{nameof(ISqsMessage.DeduplicationScope)}", sqsMessageAttribute.DeduplicationScope, string.Empty); + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.DeduplicationScope)}", e); + } + + try + { + WriteOrRemove($"{propertiesPath}.{nameof(ISqsMessage.DelaySeconds)}", sqsMessageAttribute.DelaySeconds, SqsMessageAttribute.DelaySecondsDefault); + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.DelaySeconds)}", e); + } + + try + { + WriteOrRemove($"{propertiesPath}.{nameof(ISqsMessage.FifoQueue)}", sqsMessageAttribute.FifoQueue, SqsMessageAttribute.FifoQueueDefault); + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.FifoQueue)}", e); + } + + try + { + WriteOrRemove($"{propertiesPath}.{nameof(ISqsMessage.FifoThroughputLimit)}", sqsMessageAttribute.FifoThroughputLimit, string.Empty); + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.FifoThroughputLimit)}", e); + } + + try + { + WriteOrRemove($"{propertiesPath}.{nameof(ISqsMessage.KmsDataKeyReusePeriodSeconds)}", sqsMessageAttribute.KmsDataKeyReusePeriodSeconds, SqsMessageAttribute.KmsDataKeyReusePeriodSecondsDefault); + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.KmsDataKeyReusePeriodSeconds)}", e); + } + + try + { + WriteOrRemove($"{propertiesPath}.{nameof(ISqsMessage.KmsMasterKeyId)}", sqsMessageAttribute.KmsMasterKeyId, string.Empty); + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.KmsDataKeyReusePeriodSeconds)}", e); + } + + try + { + WriteOrRemove($"{propertiesPath}.{nameof(ISqsMessage.MaximumMessageSize)}", sqsMessageAttribute.MaximumMessageSize, SqsMessageAttribute.MaximumMessageSizeDefault); + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.MaximumMessageSize)}", e); + } + + // MessageRetentionPeriod + try + { + WriteOrRemove($"{propertiesPath}.{nameof(ISqsMessage.MessageRetentionPeriod)}", sqsMessageAttribute.MessageRetentionPeriod, SqsMessageAttribute.MessageRetentionPeriodDefault); + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.MessageRetentionPeriod)}", e); + } + + // QueueName + try + { + var queueNamePath = $"{propertiesPath}.{nameof(ISqsMessage.QueueName)}"; + + if (sqsMessageAttribute.QueueName!= null) + { + _jsonWriter.SetToken(queueNamePath, sqsMessageAttribute.QueueName); + } + else + { + _jsonWriter.RemoveToken(queueNamePath); + } + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.QueueName)}", e); + } + + //ReceiveMessageWaitTimeSeconds + try + { + WriteOrRemove($"{propertiesPath}.{nameof(ISqsMessage.ReceiveMessageWaitTimeSeconds)}", sqsMessageAttribute.ReceiveMessageWaitTimeSeconds, SqsMessageAttribute.ReceiveMessageWaitTimeSecondsDefault); + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.ReceiveMessageWaitTimeSeconds)}", e); + } + + //RedriveAllowPolicy + try + { + WriteOrRemoveAsJson($"{propertiesPath}.{nameof(ISqsMessage.RedriveAllowPolicy)}", sqsMessageAttribute.RedriveAllowPolicy); + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.RedriveAllowPolicy)}", e); + } + + //RedrivePolicy + try + { + WriteOrRemoveAsJson($"{propertiesPath}.{nameof(ISqsMessage.RedrivePolicy)}", sqsMessageAttribute.RedrivePolicy); + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.RedrivePolicy)}", e); + } + + // Tags + try + { + var tagArray = new JArray(); + if (sqsMessageAttribute.Tags != default) + { + foreach (var tag in sqsMessageAttribute.Tags) + { + var tagParts = tag.Split('='); + var key = tagParts.FirstOrDefault(); + var value = tagParts.LastOrDefault(); + if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value)) + { + var tagObject = new JObject(); + tagObject.Add(new JProperty("Key", key)); + tagObject.Add(new JProperty("Value", value)); + tagArray.Add(tagObject); + } + } + } + + if (tagArray.Any()) + { + _jsonWriter.SetToken($"{propertiesPath}.{nameof(ISqsMessage.Tags)}", tagArray); + } + else + { + _jsonWriter.RemoveToken($"{propertiesPath}.{nameof(ISqsMessage.Tags)}"); + } + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.Tags)}", e); + } + + try + { + WriteOrRemove($"{propertiesPath}.{nameof(ISqsMessage.VisibilityTimeout)}", sqsMessageAttribute.VisibilityTimeout, SqsMessageAttribute.VisibilityTimeoutDefault); + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.VisibilityTimeout)}", e); + } + } + catch (Exception e) + { + throw new Exception($"Failed to write AWS::SQS::Queue: {e.Message} {e.InnerException?.Message}", e); + } + } + + private void RemoveOrphanedResources() + { + var resourceToken = _jsonWriter.GetToken("Resources") as JObject; + if (resourceToken == null) + return; + + var toRemove = new List(); + foreach (var resource in resourceToken.Properties()) + { + var resourcePath = $"Resources.{resource.Name}"; + var type = _jsonWriter.GetToken($"{resourcePath}.Type", string.Empty); + var creationTool = _jsonWriter.GetToken($"{resourcePath}.Metadata.Tool", string.Empty); + + if (string.Equals(type.ToObject(), "AWS::SQS::Queue", StringComparison.Ordinal) + && string.Equals(creationTool.ToObject(), "Amazon.Lambda.Annotations", StringComparison.Ordinal) + && !_processedResources.Contains(resource.Name)) + { + toRemove.Add(resource.Name); + } + } + + foreach (var resourceName in toRemove) + { + _jsonWriter.RemoveToken($"Resources.{resourceName}"); + } + } + private void WriteOrRemove(string path, bool value, bool defaultValue) + { + if (value != defaultValue) + { + _jsonWriter.SetToken(path, value); + } + else + { + _jsonWriter.RemoveToken(path); + } + } + private void WriteOrRemoveAsJson(string path, string value) + { + + if (!string.IsNullOrEmpty(value)) + { + _jsonWriter.SetToken(path, JObject.Parse(value)); + } + else + { + _jsonWriter.RemoveToken(path); + } + } + private void WriteOrRemove(string path, string value, string defaultValue) + { + if (value == null) + { + value = string.Empty; + } + + if (defaultValue == null) + { + defaultValue = string.Empty; + } + if (value != defaultValue) + { + _jsonWriter.SetToken(path, value); + } + else + { + _jsonWriter.RemoveToken(path); + } + } + private void WriteOrRemove(string path, uint value, uint defaultValue) + { + if (value != defaultValue) + { + _jsonWriter.SetToken(path, value); + } + else + { + _jsonWriter.RemoveToken(path); + } + } + private void ApplyQueueDefaults(string sqsQueuePath, string propertiesPath) + { + _jsonWriter.SetToken($"{sqsQueuePath}.Type", "AWS::SQS::Queue"); + _jsonWriter.SetToken($"{sqsQueuePath}.Metadata.Tool", "Amazon.Lambda.Annotations"); + + } + private bool ShouldProcessQueue(ILambdaFunctionSerializable lambdaFunctionSerializable, ISqsQueueSerializable data) + { + if (!string.IsNullOrEmpty(data.EventQueueARN)) return false; + + var sqsInfo = GetSqsQueueLogicalIdAndPath(data); + var sqsQueuePath = sqsInfo.Item2; + + + if (!_jsonWriter.Exists(sqsQueuePath)) + return true; + + var creationTool = _jsonWriter.GetToken($"{sqsQueuePath}.Metadata.Tool", string.Empty); + return string.Equals(creationTool.ToObject(), "Amazon.Lambda.Annotations", StringComparison.Ordinal); + } + + internal static (string, string) GetSqsQueueLogicalIdAndPath(ISqsQueueSerializable data) + { + return (data.QueueLogicalId, $"Resources.{data.QueueLogicalId}"); + } + private string ProcessSqsMessageAttribute(ILambdaFunctionSerializable lambdaFunction, ISqsQueueSerializable sqsQueueSerializable) + { + string eventHandle = "Sqs"; + + var eventPath = $"Resources.{lambdaFunction.Name}.Properties.Events"; + var methodPath = $"{eventPath}.{eventHandle}"; + + _jsonWriter.SetToken($"{methodPath}.Type", "SQS"); + + var batchSizePropertyPath = $"{methodPath}.Properties.BatchSize"; + + if (sqsQueueSerializable.EventBatchSize != SqsMessageAttribute.EventBatchSizeDefault) + { + _jsonWriter.SetToken(batchSizePropertyPath, sqsQueueSerializable.EventBatchSize); + } + else + { + _jsonWriter.RemoveToken(batchSizePropertyPath); + } + + var queueNamePath = $"{methodPath}.Properties.Queue"; + if (!string.IsNullOrEmpty(sqsQueueSerializable.EventQueueARN)) + { + _jsonWriter.SetToken(queueNamePath, sqsQueueSerializable.EventQueueARN); + } + else if (!string.IsNullOrEmpty(sqsQueueSerializable.QueueLogicalId)) + { + _jsonWriter.SetToken(queueNamePath, new JObject(new JProperty("Fn::GetAtt", + new JArray(sqsQueueSerializable.QueueLogicalId, "Arn")))); + } + + try + { + var filterCriteriaPropertiesPath = $"{methodPath}.Properties.FilterCriteria"; + if (sqsQueueSerializable.EventFilterCriteria.Any()) + { + var filterCriteriaPayload = new JObject(); + var filtersArray = new JArray(); + + foreach (var eventFilterCriterion in sqsQueueSerializable.EventFilterCriteria) + { + var jObject = new JObject(); + jObject.Add("Pattern", eventFilterCriterion); + filtersArray.Add(jObject); + } + + filterCriteriaPayload.Add("Filters", filtersArray); + + if (filtersArray.Any()) + { + _jsonWriter.SetToken(filterCriteriaPropertiesPath, filterCriteriaPayload); + } + else + { + _jsonWriter.RemoveToken(filterCriteriaPropertiesPath); + } + } + else + { + _jsonWriter.RemoveToken(filterCriteriaPropertiesPath); + } + + } + catch (Exception e) + { + throw new Exception($"Failed to write {nameof(ISqsMessageSerializable.Tags)}", e); + } + + var eventMaximumBatchingWindowInSecondsPropertyPath = $"{methodPath}.Properties.MaximumBatchingWindowInSeconds"; + if (sqsQueueSerializable.EventMaximumBatchingWindowInSeconds != SqsMessageAttribute.MaximumBatchingWindowInSecondsDefault) + { + _jsonWriter.SetToken(eventMaximumBatchingWindowInSecondsPropertyPath, sqsQueueSerializable.EventMaximumBatchingWindowInSeconds); + } + else + { + _jsonWriter.RemoveToken(eventMaximumBatchingWindowInSecondsPropertyPath); + } + + + return eventHandle; + } } } \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.nuspec b/Libraries/src/Amazon.Lambda.Annotations.nuspec index 57318bc55..a351c48f0 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.nuspec +++ b/Libraries/src/Amazon.Lambda.Annotations.nuspec @@ -9,7 +9,7 @@ https://github.com/aws/aws-lambda-dotnet Apache-2.0 - + images\icon.png docs\README.md @@ -19,8 +19,10 @@ + + - + diff --git a/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj b/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj index 6631c4f3d..66ee9b683 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj +++ b/Libraries/src/Amazon.Lambda.Annotations/Amazon.Lambda.Annotations.csproj @@ -5,4 +5,8 @@ netstandard2.0 + + true + + \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations/ISqsMessage.cs b/Libraries/src/Amazon.Lambda.Annotations/ISqsMessage.cs new file mode 100644 index 000000000..84930e246 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/ISqsMessage.cs @@ -0,0 +1,193 @@ +namespace Amazon.Lambda.Annotations +{ + public interface ISqsMessage + { + /// + /// For Events: The maximum number of items to retrieve in a single batch. + /// Type: Integer + /// Required: No + /// Default: 10 + /// AWS CloudFormation compatibility: This property is passed directly to the BatchSize property of an AWS::Lambda::EventSourceMapping resource. + /// Minimum: 1 + /// Maximum: 10000 + /// + uint EventBatchSize { get; set; } + + /// + /// A object that defines the criteria to determine whether Lambda should process an event. For more information, see AWS Lambda event filtering in the AWS Lambda Developer Guide. + /// Type: FilterCriteria + /// Required: No + /// AWS CloudFormation compatibility: This property is passed directly to the FilterCriteria property of an AWS::Lambda::EventSourceMapping resource. + /// + string[] EventFilterCriteria { get; set; } + /// + /// For Events: The ARN of the queue. + /// Type: String + /// Required: Yes (If not using the auto-create feature via QueueLogicalId + /// AWS CloudFormation compatibility: This property is passed directly to the EventSourceArn property of an AWS::Lambda::EventSourceMapping resource. + /// + /// + string EventQueueARN { get; set; } + + /// + /// For first-in-first-out (FIFO) queues, specifies whether to enable content-based deduplication. During the deduplication interval, Amazon SQS treats messages that are sent with identical content as duplicates and delivers only one copy of the message. For more information, see the ContentBasedDeduplication attribute for the CreateQueue action in the Amazon SQS API Reference. + /// Required: No + /// Type: Boolean + /// Update requires: No interruption + /// + bool ContentBasedDeduplication { get; set; } + + + /// + /// The maximum amount of time, in seconds, to gather records before invoking the function. + /// Type: Integer + /// Required: No + /// AWS CloudFormation compatibility: This property is passed directly to the MaximumBatchingWindowInSeconds property of an AWS::Lambda::EventSourceMapping resource. + /// + uint EventMaximumBatchingWindowInSeconds { get; set; } + + + /// + /// For high throughput for FIFO queues, specifies whether message deduplication occurs at the message group or queue level. Valid values are messageGroup and queue. + /// To enable high throughput for a FIFO queue, set this attribute to messageGroup and set the FifoThroughputLimit attribute to perMessageGroupId. If you set these attributes to anything other than these values, normal throughput is in effect and deduplication occurs as specified.For more information, see High throughput for FIFO queues and Quotas related to messages in the Amazon SQS Developer Guide. + /// + /// Required: No + /// + /// Type: String + /// Update requires: No interruption + /// + string DeduplicationScope { get; set; } + + /// + /// If set to true, creates a FIFO queue. If you don't specify this property, Amazon SQS creates a standard queue. For more information, see FIFO queues in the Amazon SQS Developer Guide. + /// Required: No + /// Type: Boolean + /// Update requires: Replacement + /// + uint DelaySeconds { get; set; } + + /// + /// If set to true, creates a FIFO queue. If you don't specify this property, Amazon SQS creates a standard queue. For more information, see FIFO queues in the Amazon SQS Developer Guide. + /// Required: No + /// Type: Boolean + /// Update requires: Replacement + /// + bool FifoQueue { get; set; } + + /// + /// For high throughput for FIFO queues, specifies whether the FIFO queue throughput quota applies to the entire queue or per message group. Valid values are perQueue and perMessageGroupId. + /// To enable high throughput for a FIFO queue, set this attribute to perMessageGroupId and set the DeduplicationScope attribute to messageGroup. If you set these attributes to anything other than these values, normal throughput is in effect and deduplication occurs as specified. For more information, see High throughput for FIFO queues and Quotas related to messages in the Amazon SQS Developer Guide. + /// Required: No + /// Type: String + /// Update requires: No interruption + /// + string FifoThroughputLimit { get; set; } + + + /// + /// The length of time in seconds for which Amazon SQS can reuse a data key to encrypt or decrypt messages before calling AWS KMS again. The value must be an integer between 60 (1 minute) and 86,400 (24 hours). The default is 300 (5 minutes). + /// Note: A shorter time period provides better security, but results in more calls to AWS KMS, which might incur charges after Free Tier. For more information, see Encryption at rest in the Amazon SQS Developer Guide. + /// Required: No + /// Type: Integer + /// Update requires: No interruption + /// + uint KmsDataKeyReusePeriodSeconds { get; set; } + + /// + /// The ID of an AWS managed customer master key (CMK) for Amazon SQS or a custom CMK. To use the AWS managed CMK for Amazon SQS, specify the (default) alias alias/aws/sqs. For more information, see the following: + /// 1. Encryption at rest in the Amazon SQS Developer Guide + /// 2. CreateQueue in the Amazon SQS API Reference + /// 3. The Customer Master Keys section of the AWS Key Management Service Best Practices whitepaper + /// Required: No + /// Type: String + /// Update requires: No interruption + /// + string KmsMasterKeyId { get; set; } + + /// + /// The limit of how many bytes that a message can contain before Amazon SQS rejects it. You can specify an integer value from 1,024 bytes (1 KiB) to 262,144 bytes (256 KiB). The default value is 262,144 (256 KiB). + /// Required: No + /// Type: Integer + /// Update requires: No interruption + /// + uint MaximumMessageSize { get; set; } + + /// + /// The number of seconds that Amazon SQS retains a message. You can specify an integer value from 60 seconds (1 minute) to 1,209,600 seconds (14 days). The default value is 345,600 seconds (4 days). + /// Required: No + /// Type: Integer + /// Update requires: No interruption + /// + uint MessageRetentionPeriod { get; set; } + + /// + /// A name for the queue. To create a FIFO queue, the name of your FIFO queue must end with the .fifo suffix. For more information, see FIFO queues in the Amazon SQS Developer Guide. + /// If you don't specify a name, AWS CloudFormation generates a unique physical ID and uses that ID for the queue name. For more information, see Name type in the AWS CloudFormation User Guide. + /// Important: If you specify a name, you can't perform updates that require replacement of this resource. You can perform updates that require no or some interruption. If you must replace the resource, specify a new name. + /// Required: No + /// Type: String + /// Update requires: Replacement + /// + string QueueName { get; set; } + + + /// + /// Specifies the duration, in seconds, that the ReceiveMessage action call waits until a message is in the queue in order to include it in the response, rather than returning an empty response if a message isn't yet available. You can specify an integer from 1 to 20. Short polling is used as the default or when you specify 0 for this property. For more information, see Consuming messages using long polling in the Amazon SQS Developer Guide. + /// Required: No + /// Type: Integer + /// Update requires: No interruption + + /// + uint ReceiveMessageWaitTimeSeconds { get; set; } + + /// + /// The string that includes the parameters for the permissions for the dead-letter queue redrive permission and which source queues can specify dead-letter queues as a JSON object. The parameters are as follows: + /// redrivePermission: The permission type that defines which source queues can specify the current queue as the dead-letter queue. Valid values are: + /// allowAll: (Default) Any source queues in this AWS account in the same Region can specify this queue as the dead-letter queue. + /// denyAll: No source queues can specify this queue as the dead-letter queue. + /// byQueue: Only queues specified by the sourceQueueArns parameter can specify this queue as the dead-letter queue. + /// sourceQueueArns: The Amazon Resource Names (ARN)s of the source queues that can specify this queue as the dead-letter queue and redrive messages. You can specify this parameter only when the redrivePermission parameter is set to byQueue. You can specify up to 10 source queue ARNs. To allow more than 10 source queues to specify dead-letter queues, set the redrivePermission parameter to allowAll. + /// Required: No + /// Type: Json + /// Update requires: No interruption + /// + string RedriveAllowPolicy { get; set; } + + /// + /// The string that includes the parameters for the dead-letter queue functionality of the source queue as a JSON object. The parameters are as follows: + /// deadLetterTargetArn: The Amazon Resource Name (ARN) of the dead-letter queue to which Amazon SQS moves messages after the value of maxReceiveCount is exceeded. + /// maxReceiveCount: The number of times a message is delivered to the source queue before being moved to the dead-letter queue. When the ReceiveCount for a message exceeds the maxReceiveCount for a queue, Amazon SQS moves the message to the dead-letter-queue. + /// Note: The dead-letter queue of a FIFO queue must also be a FIFO queue. Similarly, the dead-letter queue of a standard queue must also be a standard queue. + /// JSON: { "deadLetterTargetArn" : String, "maxReceiveCount" : Integer } + /// YAML: NOT SUPPORTED + /// Required: No + /// Type: Json + /// Update requires: No interruption + /// + string RedrivePolicy { get; set; } + + /// + /// Key value pairs of tags + /// The tags that you attach to this queue. For more information, see Resource tag in the AWS CloudFormation User Guide. + /// Required: No + /// Type: List of Tag + /// Update requires: No interruption + /// + /// Tags = new string[] {"Tag1=Value1", "Tag2=Value"} + /// + /// + string[] Tags { get; set; } + + /// + /// The length of time during which a message will be unavailable after a message is delivered from the queue. This blocks other components from receiving the same message and gives the initial component time to process and delete the message from the queue. + /// Values must be from 0 to 43,200 seconds (12 hours). If you don't specify a value, AWS CloudFormation uses the default value of 30 seconds. + /// For more information about Amazon SQS queue visibility timeouts, see Visibility timeout in the Amazon SQS Developer Guide. + /// Required: No + /// Type: Integer + /// Update requires: No interruption + /// + uint VisibilityTimeout { get; set; } + + + } +} \ 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 d007129fe..0b1d81120 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/README.md +++ b/Libraries/src/Amazon.Lambda.Annotations/README.md @@ -167,6 +167,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. +* SqsMessage + * Configures the Lambda function to be an event handler for a Sqs message. Optionally, allows the creation of an AWS::SQS::Queue in the template ### Parameter Attributes @@ -179,4 +181,62 @@ parameter to the `LambdaFunction` must be the event object and the event source * FromBody * Map method parameter to HTTP request body. If parameter is a complex type then request body will be assumed to be JSON and deserialized into the type. * FromServices - * Map method parameter to registered service in IServiceProvider \ No newline at end of file + * Map method parameter to registered service in IServiceProvider + +### SqsMessage Attribute Example + +Below is sample class containing two functions. One function will use an preexisting queue and the other adds the queue to the template. + +``` + public class Messaging + { + /// + /// This function will use a preexisting queue, whether the queue is defined manually in the template or external to the template. + /// + /// + /// + /// + [LambdaFunction] + [SqsMessage(EventQueueARN = "arn:aws:sqs:us-east-1:968993296699:app-deploy-blue-LAVETRYB3JKX-SomeQueueName", EventBatchSize = 11)] + public Task MessageHandlerForPreExistingQueue(SQSEvent.SQSMessage message, ILambdaContext context) + { + LambdaLogger.Log($"Message Received: {message.MessageId}"); + return Task.CompletedTask; + } + + /// + /// This function will add the AWS::SQS::Queue to the template and use it for the function. + /// All Serverless Event attributes start with the word Event. + /// All others are the AWS::SQS::Queue properties. + /// + /// + /// + /// + [LambdaFunction] + [SqsMessage( + EventBatchSize = 12, + EventFilterCriteria = new string[] { "Filter1", "Filter2" }, + EventMaximumBatchingWindowInSeconds = 31, + VisibilityTimeout = 100, + ContentBasedDeduplication = true, + DeduplicationScope = "queue", + DelaySeconds = 5, + FifoQueue = true, + FifoThroughputLimit = "perQueue", + KmsDataKeyReusePeriodSeconds = 299, + KmsMasterKeyId = "alias/aws/sqs", + MaximumMessageSize = 1024, + MessageRetentionPeriod = 60, + QueueName = "thisismyqueuename.fifo", + ReceiveMessageWaitTimeSeconds =5, + RedriveAllowPolicy = "{ 'redrivePermission' : 'denyAll' }", + RedrivePolicy = "{ 'deadLetterTargetArn': 'arn:somewhere', 'maxReceiveCount': 5 }", + Tags = new string[]{ "keyname1=value1", "keyname2=value2" })] + public Task MessageHandlerForNewQueue(SQSEvent.SQSMessage message, ILambdaContext context) + { + LambdaLogger.Log($"Message Received: {message.MessageId}"); + return Task.CompletedTask; + } + } +} +``` \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations/SQSMessageAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/SQSMessageAttribute.cs new file mode 100644 index 000000000..7ba85291e --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/SQSMessageAttribute.cs @@ -0,0 +1,303 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; + + +// TODO: I see that there is some linking strategy to avoid this, but I cannot get it to work. Need advise. +[assembly: InternalsVisibleTo("Amazon.Lambda.Annotations.SourceGenerators.Tests")] + +namespace Amazon.Lambda.Annotations +{ + [AttributeUsage(AttributeTargets.Method)] + public class SqsMessageAttribute : Attribute, ISqsMessage, INotifyPropertyChanged + { + public const bool ContentBasedDeduplicationDefault = false; + + internal const string DeduplicationScopeMessageGroup = "messageGroup"; + internal const string DeduplicationScopeMessageQueue = "queue"; + internal const string DeduplicationScopeArgumentOutOfRangeExceptionMessage = "{0} must be one of {1}."; + + public const uint MaximumBatchingWindowInSecondsDefault = 0; + public const uint MaximumBatchingWindowInSecondsMinimum = 0; + public const uint MaximumBatchingWindowInSecondsMaximum = 300; + + + public const uint VisibilityTimeoutDefault = 30; + internal const uint VisibilityTimeoutMinimum = 0; + internal const uint VisibilityTimeoutMaximum = 43200; + + internal const uint EventBatchSizeMinimum = 1; + internal const uint EventBatchSizeMaximum = 10000; + public const uint EventBatchSizeDefault = 10; + + internal const string UintPropertyBetweenExceptionMessage = "{0} must be => {1} && <= {2}"; + + public const uint DelaySecondsDefault = 0; + public const bool FifoQueueDefault = false; + public const uint DelaySecondsMinimum = 0; + public const uint DelaySecondsMaximum = 900; + + public const uint KmsDataKeyReusePeriodSecondsDefault = 300; + public const uint KmsDataKeyReusePeriodSecondsMinimum = 60; + public const uint KmsDataKeyReusePeriodSecondsMaximum = 86400; + + + internal const uint MaximumMessageSizeMinimum = 1024; + internal const uint MaximumMessageSizeMaximum = 262144; + public const uint MaximumMessageSizeDefault = 262144; + + internal const uint MessageRetentionPeriodMinimum = 60; + internal const uint MessageRetentionPeriodMaximum = 345600; + public const uint MessageRetentionPeriodDefault = 345600; + + public const uint ReceiveMessageWaitTimeSecondsDefault = 0; + internal const uint ReceiveMessageWaitTimeSecondsMinimum = 0; + internal const uint ReceiveMessageWaitTimeSecondsMaximum = 20; + + private string _queueName; + private string _queueLogicalId; + private string _eventQueueArn; + private string _deduplicationScope; + private uint _delaySeconds = DelaySecondsDefault; + private string _fifoThroughputLimit; + private uint _kmsDataKeyReusePeriodSeconds = KmsDataKeyReusePeriodSecondsDefault; + private uint _maximumMessageSize = MaximumMessageSizeDefault; + private uint _messageRetentionPeriod = MessageRetentionPeriodDefault; + private uint _eventBatchSize = EventBatchSizeDefault; + private uint _visibilityTimeout = VisibilityTimeoutDefault; + private uint _receiveMessageWaitTimeSeconds = ReceiveMessageWaitTimeSecondsDefault; + private string _redrivePolicy; + private uint _eventMaximumBatchingWindowInSeconds = MaximumBatchingWindowInSecondsDefault; + + + // event handler values + public string[] EventFilterCriteria { get; set; } = new string[] { }; + + public string EventQueueARN + { + get => _eventQueueArn; + set + { + if(_eventQueueArn==value) return; + _eventQueueArn = value; + OnPropertyChanged(); + } + } + + public uint EventBatchSize + { + get => _eventBatchSize; + set + { + if (_eventBatchSize==value) return; + if (value < EventBatchSizeMinimum || value > EventBatchSizeMaximum) + { + throw new ArgumentOutOfRangeException(nameof(EventBatchSize), string.Format(UintPropertyBetweenExceptionMessage, nameof(EventBatchSize), EventBatchSizeMinimum, EventBatchSizeMaximum)); + } + _eventBatchSize = value; + OnPropertyChanged(); + } + } + + // sqs queue values + + public string[] Tags { get; set; } = new string[] {}; + + public uint VisibilityTimeout + { + get => _visibilityTimeout; + set + { + if (value == _visibilityTimeout) return; + if (value > VisibilityTimeoutMaximum) + { + throw new ArgumentOutOfRangeException( + nameof(VisibilityTimeout), + string.Format(UintPropertyBetweenExceptionMessage, nameof(VisibilityTimeout), VisibilityTimeoutMinimum, VisibilityTimeoutMaximum)); + } + _visibilityTimeout = value; + OnPropertyChanged(); + } + } + + public uint ReceiveMessageWaitTimeSeconds + { + get => _receiveMessageWaitTimeSeconds; + set + { + if ( value == _receiveMessageWaitTimeSeconds) return; + if (value > ReceiveMessageWaitTimeSecondsMaximum) + { + throw new ArgumentOutOfRangeException(nameof(ReceiveMessageWaitTimeSeconds), + string.Format(UintPropertyBetweenExceptionMessage,nameof(ReceiveMessageWaitTimeSeconds), ReceiveMessageWaitTimeSecondsMinimum, ReceiveMessageWaitTimeSecondsMaximum)); + } + _receiveMessageWaitTimeSeconds = value; + OnPropertyChanged(); + } + } + + public bool ContentBasedDeduplication { get; set; } = ContentBasedDeduplicationDefault; + + public uint EventMaximumBatchingWindowInSeconds + { + get => _eventMaximumBatchingWindowInSeconds; + set + { + if ( _eventMaximumBatchingWindowInSeconds == value ) return; + if (value < MaximumBatchingWindowInSecondsMinimum || value > MaximumBatchingWindowInSecondsMaximum) + { + throw new ArgumentOutOfRangeException(nameof(EventMaximumBatchingWindowInSeconds), + string.Format(UintPropertyBetweenExceptionMessage, nameof(EventMaximumBatchingWindowInSeconds), MaximumBatchingWindowInSecondsMinimum, MaximumBatchingWindowInSecondsMaximum)); + + } + _eventMaximumBatchingWindowInSeconds = value; + OnPropertyChanged(); + } + } + + internal static string[] ValidDeduplicationScopes = new string[] { DeduplicationScopeMessageGroup, DeduplicationScopeMessageQueue, null, string.Empty }; + + public string DeduplicationScope + { + get => _deduplicationScope; + set + { + if(_deduplicationScope==value) return; + if (!ValidDeduplicationScopes.Contains(value)) + { + throw new ArgumentOutOfRangeException(nameof(DeduplicationScope), + string.Format(DeduplicationScopeArgumentOutOfRangeExceptionMessage, nameof(DeduplicationScope), string.Join(",", ValidDeduplicationScopes))); + } + + _deduplicationScope = value; + OnPropertyChanged(); + } + } + + public uint DelaySeconds + { + get => _delaySeconds; + set + { + if (_delaySeconds==value) return; + if (value > DelaySecondsMaximum) + { + throw new ArgumentOutOfRangeException(); + } + _delaySeconds = value; + OnPropertyChanged(); + } + } + + public bool FifoQueue { get; set; } + + public string FifoThroughputLimit + { + get => _fifoThroughputLimit; + set + { + if(_fifoThroughputLimit==value ) return; + switch (value) + { + case "perMessageGroupId": + case "perQueue": + case "": + case null: + _fifoThroughputLimit = value; + OnPropertyChanged(); + break; + default: + throw new ArgumentOutOfRangeException(); + + } + } + } + + public uint KmsDataKeyReusePeriodSeconds + { + get => _kmsDataKeyReusePeriodSeconds; + set + { + if (_kmsDataKeyReusePeriodSeconds==value) return; + if (value < KmsDataKeyReusePeriodSecondsMinimum || value > KmsDataKeyReusePeriodSecondsMaximum) + { + throw new ArgumentOutOfRangeException(nameof(KmsDataKeyReusePeriodSeconds), + string.Format(UintPropertyBetweenExceptionMessage, + nameof(KmsDataKeyReusePeriodSeconds), + KmsDataKeyReusePeriodSecondsMinimum, + KmsDataKeyReusePeriodSecondsMaximum)); + } + _kmsDataKeyReusePeriodSeconds = value; + OnPropertyChanged(); + } + } + + + public string KmsMasterKeyId { get; set; } + + public uint MaximumMessageSize + { + get => _maximumMessageSize; + set + { + if (_maximumMessageSize == value) return; + if (value < MaximumMessageSizeMinimum || value > MaximumMessageSizeMaximum) + { + throw new ArgumentOutOfRangeException(nameof(MaximumMessageSize), + string.Format(UintPropertyBetweenExceptionMessage, nameof(MaximumMessageSize), MaximumMessageSizeMinimum, MaximumMessageSizeMaximum)); + } + _maximumMessageSize = value; + OnPropertyChanged(); + } + } + + public uint MessageRetentionPeriod + { + get => _messageRetentionPeriod; + set + { + if (_messageRetentionPeriod==value) return; + if (value < MessageRetentionPeriodMinimum || value > MessageRetentionPeriodMaximum) + { + throw new ArgumentOutOfRangeException(nameof(MessageRetentionPeriod), + string.Format(UintPropertyBetweenExceptionMessage, nameof(MessageRetentionPeriod), MessageRetentionPeriodMinimum, MessageRetentionPeriodMaximum)); + } + _messageRetentionPeriod = value; + OnPropertyChanged(); + } + } + + public string RedriveAllowPolicy { get; set; } + + public string RedrivePolicy + { + get => _redrivePolicy; + set + { + if (_redrivePolicy == value) return; + _redrivePolicy = value; + OnPropertyChanged(); + } + } + + public string QueueName + { + get => _queueName; + set + { + if (_queueName==value) return; + _queueName = value; + this.OnPropertyChanged(); + } + } + + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} 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 125be7004..e16cb1898 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 @@ -1,12 +1,12 @@  - - netcoreapp3.1 - true - latest - + + netcoreapp3.1 + true + latest + - + @@ -40,7 +40,8 @@ Always - + + @@ -52,6 +53,7 @@ + @@ -64,7 +66,6 @@ - + + + + + diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/AnnotationTests/SqsMessageAttributeTest.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/AnnotationTests/SqsMessageAttributeTest.cs new file mode 100644 index 000000000..62fcd4f5f --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/AnnotationTests/SqsMessageAttributeTest.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests.AnnotationTests +{ + // I am putting this here because there is no Amazon.Lambda.Annotations.Tests project + // I did not want to add a project in a pull request without asking first + + public class SqsMessageAttributeTest + { + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData(SqsMessageAttribute.DeduplicationScopeMessageGroup)] + [InlineData(SqsMessageAttribute.DeduplicationScopeMessageQueue)] + [InlineData("invalidValue", typeof(ArgumentOutOfRangeException), SqsMessageAttribute.DeduplicationScopeArgumentOutOfRangeExceptionMessage)] + public void DeduplicationScopeValidation(string value, Type expectedException = null, string messageFormat = null) + { + var target = new SqsMessageAttribute(); + if (expectedException == null) + { + target.DeduplicationScope = value; + // no exception, all good + } + else + { + var error = Assert.Throws(expectedException, () => target.DeduplicationScope = value) as ArgumentException; + Assert.Equal(nameof(SqsMessageAttribute.DeduplicationScope), error.ParamName); + Assert.Equal(string.Format(SqsMessageAttribute.DeduplicationScopeArgumentOutOfRangeExceptionMessage, + nameof(SqsMessageAttribute.DeduplicationScope), + string.Join(',', SqsMessageAttribute.ValidDeduplicationScopes)) + + $" (Parameter '{nameof(SqsMessageAttribute.DeduplicationScope)}')", error.Message); + + } + } + + [Fact] + public void DelaySecondsValidation() + { + var target = new SqsMessageAttribute(); + target.DelaySeconds = SqsMessageAttribute.DelaySecondsMinimum; + target.DelaySeconds = SqsMessageAttribute.DelaySecondsMaximum; + Assert.Throws(() => target.DelaySeconds = SqsMessageAttribute.DelaySecondsMaximum + 1); + } + + [Fact] + public void FifoThroughputLimit() + { + var target = new SqsMessageAttribute(); + target.FifoThroughputLimit = "perQueue"; + target.FifoThroughputLimit = "perMessageGroupId"; + Assert.Throws(() => target.FifoThroughputLimit = "notValid"); + } + + [Theory] + [InlineData(SqsMessageAttribute.KmsDataKeyReusePeriodSecondsMinimum)] + [InlineData(SqsMessageAttribute.KmsDataKeyReusePeriodSecondsMaximum)] + [InlineData(SqsMessageAttribute.KmsDataKeyReusePeriodSecondsMinimum - 1, typeof(ArgumentOutOfRangeException), SqsMessageAttribute.UintPropertyBetweenExceptionMessage)] + [InlineData(SqsMessageAttribute.KmsDataKeyReusePeriodSecondsMaximum + 1, typeof(ArgumentOutOfRangeException), SqsMessageAttribute.UintPropertyBetweenExceptionMessage)] + public void KmsDataKeyReusePeriodSecondsValidation(uint value, Type throws = null, string messageFormat = null) + { + var target = new SqsMessageAttribute(); + if (throws == null) + { + target.KmsDataKeyReusePeriodSeconds = value; + } + else + { + var error = Assert.Throws(throws, () => target.KmsDataKeyReusePeriodSeconds = value) as ArgumentException; + Assert.Equal(nameof(SqsMessageAttribute.KmsDataKeyReusePeriodSeconds), error.ParamName); + Assert.Equal( string.Format(messageFormat, nameof(SqsMessageAttribute.KmsDataKeyReusePeriodSeconds), SqsMessageAttribute.KmsDataKeyReusePeriodSecondsMinimum, SqsMessageAttribute.KmsDataKeyReusePeriodSecondsMaximum) + $" (Parameter '{nameof(SqsMessageAttribute.KmsDataKeyReusePeriodSeconds)}')", + error.Message); + + } + } + + [Theory] + [InlineData(SqsMessageAttribute.MaximumMessageSizeMinimum)] + [InlineData(SqsMessageAttribute.MaximumMessageSizeMaximum)] + [InlineData(SqsMessageAttribute.MaximumMessageSizeMinimum - 1, typeof(ArgumentOutOfRangeException), SqsMessageAttribute.UintPropertyBetweenExceptionMessage)] + [InlineData(SqsMessageAttribute.MaximumMessageSizeMaximum + 1, typeof(ArgumentOutOfRangeException), SqsMessageAttribute.UintPropertyBetweenExceptionMessage)] + public void MaximumMessageSizeValidation(uint value, Type expectedException = null, string expectedErrorFormat = null) + { + var target = new SqsMessageAttribute(); + if (expectedException == null) + { + target.MaximumMessageSize = value; + } + else + { + var error = Assert.Throws(expectedException, () => target.MaximumMessageSize = value) as ArgumentException; + Assert.Equal(nameof(SqsMessageAttribute.MaximumMessageSize), error.ParamName); + Assert.Equal(string.Format(expectedErrorFormat, nameof(SqsMessageAttribute.MaximumMessageSize), SqsMessageAttribute.MaximumMessageSizeMinimum, SqsMessageAttribute.MaximumMessageSizeMaximum) + $" (Parameter '{nameof(SqsMessageAttribute.MaximumMessageSize)}')", error.Message); + + } + } + + [Theory] + [InlineData(SqsMessageAttribute.MessageRetentionPeriodMinimum)] + [InlineData(SqsMessageAttribute.MessageRetentionPeriodMaximum)] + [InlineData(SqsMessageAttribute.MessageRetentionPeriodMinimum - 1, typeof(ArgumentOutOfRangeException), SqsMessageAttribute.UintPropertyBetweenExceptionMessage)] + [InlineData(SqsMessageAttribute.MessageRetentionPeriodMaximum + 1, typeof(ArgumentOutOfRangeException), SqsMessageAttribute.UintPropertyBetweenExceptionMessage)] + public void MessageRetentionPeriodValidation(uint value, Type expectedException = null, string expectedMessageFormat = null) + { + var target = new SqsMessageAttribute(); + if (expectedException == null) + { + target.MessageRetentionPeriod = value; + } + else + { + var error = Assert.Throws(expectedException, () => target.MessageRetentionPeriod = SqsMessageAttribute.MessageRetentionPeriodMinimum - 1) as ArgumentException; + Assert.Equal(nameof(SqsMessageAttribute.MessageRetentionPeriod), error.ParamName); + Assert.Equal(string.Format(expectedMessageFormat, nameof(SqsMessageAttribute.MessageRetentionPeriod), SqsMessageAttribute.MessageRetentionPeriodMinimum, SqsMessageAttribute.MessageRetentionPeriodMaximum) + $" (Parameter '{nameof(SqsMessageAttribute.MessageRetentionPeriod)}')", error.Message); + } + } + + [Fact] + public void QueueNameValidation() + { + var target = new SqsMessageAttribute(); + target.QueueName = "MyQueueName"; + target.QueueName = "MyQueueName.fifo"; + // no exception, no problem + } + + [Theory] + [InlineData(SqsMessageAttribute.EventBatchSizeMinimum)] + [InlineData(SqsMessageAttribute.EventBatchSizeMaximum)] + [InlineData(SqsMessageAttribute.EventBatchSizeMinimum - 1, typeof(ArgumentOutOfRangeException), SqsMessageAttribute.UintPropertyBetweenExceptionMessage)] + [InlineData(SqsMessageAttribute.EventBatchSizeMaximum + 1, typeof(ArgumentOutOfRangeException), SqsMessageAttribute.UintPropertyBetweenExceptionMessage)] + public void EventBatchSizeValidation(uint value, Type throws = null, string messageFormat = null) + { + var target = new SqsMessageAttribute(); + if (throws == null) + { + target.EventBatchSize = value; + } + else + { + var error = Assert.Throws(throws, () => target.EventBatchSize = value) as ArgumentException; + Assert.Equal(nameof(SqsMessageAttribute.EventBatchSize), error.ParamName); + Assert.Equal(string.Format(messageFormat, nameof(SqsMessageAttribute.EventBatchSize), SqsMessageAttribute.EventBatchSizeMinimum, SqsMessageAttribute.EventBatchSizeMaximum) + $" (Parameter '{nameof(SqsMessageAttribute.EventBatchSize)}')", error.Message); + + } + } + + [Theory] + [InlineData(SqsMessageAttribute.VisibilityTimeoutMinimum)] + [InlineData(SqsMessageAttribute.VisibilityTimeoutMaximum)] + [InlineData(SqsMessageAttribute.VisibilityTimeoutMaximum+1, typeof(ArgumentOutOfRangeException), SqsMessageAttribute.UintPropertyBetweenExceptionMessage)] + public void VisibilityTimeoutValidation(uint value, Type throws = null, string message = null) + { + var target = new SqsMessageAttribute(); + if (throws == null) + { + target.VisibilityTimeout = value; + } + else + { + var error = Assert.Throws(throws, () => target.VisibilityTimeout = value) as ArgumentException; + Assert.Equal(nameof(SqsMessageAttribute.VisibilityTimeout), error.ParamName); + Assert.Equal( + string.Format(message, nameof(SqsMessageAttribute.VisibilityTimeout), SqsMessageAttribute.VisibilityTimeoutMinimum, SqsMessageAttribute.VisibilityTimeoutMaximum) + $" (Parameter '{nameof(SqsMessageAttribute.VisibilityTimeout)}')", + error.Message); + + } + + + } + + [Theory] + [InlineData(SqsMessageAttribute.ReceiveMessageWaitTimeSecondsMinimum)] + [InlineData(SqsMessageAttribute.ReceiveMessageWaitTimeSecondsMaximum + 1, typeof(ArgumentOutOfRangeException), SqsMessageAttribute.UintPropertyBetweenExceptionMessage)] + public void ReceiveMessageWaitTimeSecondsValidation(uint value, Type throws = null, string messageFormat = null) + { + var target = new SqsMessageAttribute(); + if (throws == null) + { + target.ReceiveMessageWaitTimeSeconds = value; + } + else + { + var error = Assert.Throws(throws, () => target.ReceiveMessageWaitTimeSeconds = value) as ArgumentException; + Assert.Equal(nameof(SqsMessageAttribute.ReceiveMessageWaitTimeSeconds), error.ParamName); + Assert.Equal(string.Format(messageFormat, nameof(SqsMessageAttribute.ReceiveMessageWaitTimeSeconds), SqsMessageAttribute.ReceiveMessageWaitTimeSecondsMinimum, SqsMessageAttribute.ReceiveMessageWaitTimeSecondsMaximum) + $" (Parameter '{nameof(SqsMessageAttribute.ReceiveMessageWaitTimeSeconds)}')", error.Message); + + } + } + + [Theory] + [InlineData("{ 'deadLetterTargetArn': 'arn:somewhere', 'maxReceiveCount': 5 }")] + public void RedriveAllowPolicyValidation(string value, Type exceptionType = null, string exceptionMessage = null) + { + var target = new SqsMessageAttribute(); + if (exceptionType == null) + { + target.RedrivePolicy = value; + } + else + { + var error = Assert.Throws(exceptionType, () => target.RedrivePolicy = value) as ArgumentException; + Assert.Equal(nameof(SqsMessageAttribute.RedrivePolicy), error.ParamName); + Assert.Equal(exceptionMessage, error.Message); + } + + } + + [Theory] + [InlineData(SqsMessageAttribute.MaximumBatchingWindowInSecondsDefault)] + [InlineData(SqsMessageAttribute.MaximumBatchingWindowInSecondsMinimum)] + [InlineData(SqsMessageAttribute.MaximumBatchingWindowInSecondsMaximum)] + [InlineData(SqsMessageAttribute.MaximumBatchingWindowInSecondsMaximum + 1, typeof(ArgumentOutOfRangeException), SqsMessageAttribute.UintPropertyBetweenExceptionMessage)] + public void MaximumBatchingWindowInSecondsValidation(uint value, Type throws = null, string messageFormat = null) + { + var target = new SqsMessageAttribute(); + if (throws == null) + { + target.EventMaximumBatchingWindowInSeconds = value; + } + else + { + var error = Assert.Throws(throws, () => target.EventMaximumBatchingWindowInSeconds = value) as ArgumentException; + Assert.Equal(nameof(SqsMessageAttribute.EventMaximumBatchingWindowInSeconds), error.ParamName); + Assert.Equal(string.Format(messageFormat, nameof(SqsMessageAttribute.EventMaximumBatchingWindowInSeconds), SqsMessageAttribute.MaximumBatchingWindowInSecondsMinimum, SqsMessageAttribute.MaximumBatchingWindowInSecondsMaximum) + $" (Parameter '{nameof(SqsMessageAttribute.EventMaximumBatchingWindowInSeconds)}')", error.Message); + + } + } + + + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs index 0de6ffe15..5688ff3d5 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/CSharpSourceGeneratorVerifier.cs @@ -4,6 +4,7 @@ using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; using Amazon.Lambda.Serialization.SystemTextJson; +using Amazon.Lambda.SQSEvents; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Testing; @@ -29,7 +30,8 @@ public Test() .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location)) .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(ServiceProvider).Assembly.Location)) .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(RestApiAttribute).Assembly.Location)) - .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(DefaultLambdaJsonSerializer).Assembly.Location)); + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(DefaultLambdaJsonSerializer).Assembly.Location)) + .AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(SQSEvent.SQSMessage).Assembly.Location)); }); } diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForNewFifoQueueUsingFnSubForQueueName_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForNewFifoQueueUsingFnSubForQueueName_Generated.g.cs new file mode 100644 index 000000000..770ef5f0d --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForNewFifoQueueUsingFnSubForQueueName_Generated.g.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using Amazon.Lambda.Core; + +namespace TestServerlessApp +{ + public class Messaging_MessageHandlerForNewFifoQueueUsingFnSubForQueueName_Generated + { + private readonly Messaging messaging; + + public Messaging_MessageHandlerForNewFifoQueueUsingFnSubForQueueName_Generated() + { + SetExecutionEnvironment(); + messaging = new Messaging(); + } + + public async System.Threading.Tasks.Task MessageHandlerForNewFifoQueueUsingFnSubForQueueName(Amazon.Lambda.SQSEvents.SQSEvent.SQSMessage message, Amazon.Lambda.Core.ILambdaContext __context__) + { + return await messaging.MessageHandlerForNewFifoQueueUsingFnSubForQueueName(message, __context__); + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("amazon-lambda-annotations_0.6.0.0"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForNewFifoQueueWithoutAQueueName_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForNewFifoQueueWithoutAQueueName_Generated.g.cs new file mode 100644 index 000000000..550f0acbf --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForNewFifoQueueWithoutAQueueName_Generated.g.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using Amazon.Lambda.Core; + +namespace TestServerlessApp +{ + public class Messaging_MessageHandlerForNewFifoQueueWithoutAQueueName_Generated + { + private readonly Messaging messaging; + + public Messaging_MessageHandlerForNewFifoQueueWithoutAQueueName_Generated() + { + SetExecutionEnvironment(); + messaging = new Messaging(); + } + + public async System.Threading.Tasks.Task MessageHandlerForNewFifoQueueWithoutAQueueName(Amazon.Lambda.SQSEvents.SQSEvent.SQSMessage message, Amazon.Lambda.Core.ILambdaContext __context__) + { + return await messaging.MessageHandlerForNewFifoQueueWithoutAQueueName(message, __context__); + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("amazon-lambda-annotations_0.6.0.0"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForNewQueue_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForNewQueue_Generated.g.cs new file mode 100644 index 000000000..772bd30a9 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForNewQueue_Generated.g.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using Amazon.Lambda.Core; + +namespace TestServerlessApp +{ + public class Messaging_MessageHandlerForNewQueue_Generated + { + private readonly Messaging messaging; + + public Messaging_MessageHandlerForNewQueue_Generated() + { + SetExecutionEnvironment(); + messaging = new Messaging(); + } + + public async System.Threading.Tasks.Task MessageHandlerForNewQueue(Amazon.Lambda.SQSEvents.SQSEvent.SQSMessage message, Amazon.Lambda.Core.ILambdaContext __context__) + { + return await messaging.MessageHandlerForNewQueue(message, __context__); + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("amazon-lambda-annotations_0.6.0.0"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForPreExistingQueue_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForPreExistingQueue_Generated.g.cs new file mode 100644 index 000000000..20d2019d2 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/Messaging_MessageHandlerForPreExistingQueue_Generated.g.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using Amazon.Lambda.Core; + +namespace TestServerlessApp +{ + public class Messaging_MessageHandlerForPreExistingQueue_Generated + { + private readonly Messaging messaging; + + public Messaging_MessageHandlerForPreExistingQueue_Generated() + { + SetExecutionEnvironment(); + messaging = new Messaging(); + } + + public async System.Threading.Tasks.Task MessageHandlerForPreExistingQueue(Amazon.Lambda.SQSEvents.SQSEvent.SQSMessage message, Amazon.Lambda.Core.ILambdaContext __context__) + { + return await messaging.MessageHandlerForPreExistingQueue(message, __context__); + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("amazon-lambda-annotations_0.6.0.0"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/messaging.template b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/messaging.template new file mode 100644 index 000000000..6bb9c822e --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/messaging.template @@ -0,0 +1,214 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Resources": { + "TestServerlessAppMessagingMessageHandlerForPreExistingQueueGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "Sqs" + ] + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 256, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestServerlessApp.Messaging_MessageHandlerForPreExistingQueue_Generated::MessageHandlerForPreExistingQueue", + "Events": { + "Sqs": { + "Type": "SQS", + "Properties": { + "BatchSize": 11, + "Queue": "arn:aws:sqs:us-east-1:968993296699:app-deploy-blue-LAVETRYB3JKX-SomeQueueName" + } + } + } + } + }, + "TestServerlessAppMessagingMessageHandlerForNewQueueGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "Sqs" + ], + "SyncedResources": [ + "TestServerlessAppMessagingMessageHandlerForNewQueueGeneratedQueue" + ] + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 256, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestServerlessApp.Messaging_MessageHandlerForNewQueue_Generated::MessageHandlerForNewQueue", + "Events": { + "Sqs": { + "Type": "SQS", + "Properties": { + "BatchSize": 12, + "Queue": { + "Fn::GetAtt": [ + "TestServerlessAppMessagingMessageHandlerForNewQueueGeneratedQueue", + "Arn" + ] + }, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "Filter1" + }, + { + "Pattern": "Filter2" + } + ] + }, + "MaximumBatchingWindowInSeconds": 31 + } + } + } + } + }, + "TestServerlessAppMessagingMessageHandlerForNewQueueGeneratedQueue": { + "Type": "AWS::SQS::Queue", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "ContentBasedDeduplication": true, + "DeduplicationScope": "queue", + "DelaySeconds": 5, + "FifoQueue": true, + "FifoThroughputLimit": "perQueue", + "KmsDataKeyReusePeriodSeconds": 299, + "KmsMasterKeyId": "alias/aws/sqs", + "MaximumMessageSize": 1024, + "MessageRetentionPeriod": 60, + "QueueName": "thisismyqueuename.fifo", + "ReceiveMessageWaitTimeSeconds": 5, + "RedriveAllowPolicy": { + "redrivePermission": "denyAll" + }, + "RedrivePolicy": { + "deadLetterTargetArn": "arn:somewhere", + "maxReceiveCount": 5 + }, + "Tags": [ + { + "Key": "keyname1", + "Value": "value1" + }, + { + "Key": "keyname2", + "Value": "value2" + } + ], + "VisibilityTimeout": 100 + } + }, + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueUsingFnSubForQueueNameGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "Sqs" + ], + "SyncedResources": [ + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueUsingFnSubForQueueNameGeneratedQueue" + ] + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 256, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestServerlessApp.Messaging_MessageHandlerForNewFifoQueueUsingFnSubForQueueName_Generated::MessageHandlerForNewFifoQueueUsingFnSubForQueueName", + "Events": { + "Sqs": { + "Type": "SQS", + "Properties": { + "Queue": { + "Fn::GetAtt": [ + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueUsingFnSubForQueueNameGeneratedQueue", + "Arn" + ] + } + } + } + } + } + }, + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueUsingFnSubForQueueNameGeneratedQueue": { + "Type": "AWS::SQS::Queue", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "FifoQueue": true, + "QueueName": { + "Fn::Sub": "${AWS::Stack}MyFifoQueueWithStackEmbedded.fifo" + } + } + }, + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueWithoutAQueueNameGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "Sqs" + ], + "SyncedResources": [ + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueWithoutAQueueNameGeneratedQueue" + ] + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 256, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestProject::TestServerlessApp.Messaging_MessageHandlerForNewFifoQueueWithoutAQueueName_Generated::MessageHandlerForNewFifoQueueWithoutAQueueName", + "Events": { + "Sqs": { + "Type": "SQS", + "Properties": { + "Queue": { + "Fn::GetAtt": [ + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueWithoutAQueueNameGeneratedQueue", + "Arn" + ] + } + } + } + } + } + }, + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueWithoutAQueueNameGeneratedQueue": { + "Type": "AWS::SQS::Queue", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "FifoQueue": true, + "QueueName": "TestServerlessAppMessagingMessageHandlerForNewFifoQueueWithoutAQueueNameGeneratedQueue.fifo" + } + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SimpleCalculator_SqsMessageEventHandler_Generated.g.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SimpleCalculator_SqsMessageEventHandler_Generated.g.cs new file mode 100644 index 000000000..b50e963d8 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/SimpleCalculator_SqsMessageEventHandler_Generated.g.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Amazon.Lambda.Core; + +namespace TestServerlessApp +{ + public class SimpleCalculator_SqsMessageEventHandler_Generated + { + private readonly ServiceProvider serviceProvider; + + public SimpleCalculator_SqsMessageEventHandler_Generated() + { + SetExecutionEnvironment(); + var services = new ServiceCollection(); + + // By default, Lambda function class is added to the service container using the singleton lifetime + // To use a different lifetime, specify the lifetime in Startup.ConfigureServices(IServiceCollection) method. + services.AddSingleton(); + + var startup = new TestServerlessApp.Startup(); + startup.ConfigureServices(services); + serviceProvider = services.BuildServiceProvider(); + } + + public System.Threading.Tasks.Task SqsMessageEventHandler(Amazon.Lambda.SQSEvents.SQSEvent.SQSMessage sqsMessage, Amazon.Lambda.Core.ILambdaContext __context__) + { + // Create a scope for every request, + // this allows creating scoped dependencies without creating a scope manually. + using var scope = serviceProvider.CreateScope(); + var simpleCalculator = scope.ServiceProvider.GetRequiredService(); + + return simpleCalculator.SqsMessageEventHandler(sqsMessage, __context__); + } + + private static void SetExecutionEnvironment() + { + const string envName = "AWS_EXECUTION_ENV"; + + var envValue = new StringBuilder(); + + // If there is an existing execution environment variable add the annotations package as a suffix. + if(!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envName))) + { + envValue.Append($"{Environment.GetEnvironmentVariable(envName)}_"); + } + + envValue.Append("amazon-lambda-annotations_0.6.0.0"); + + Environment.SetEnvironmentVariable(envName, envValue.ToString()); + } + } +} \ No newline at end of file diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.Messaging.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.Messaging.cs new file mode 100644 index 000000000..62cd0bb43 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.Messaging.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Amazon.Lambda.Annotations.SourceGenerator.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.Text; +using Xunit; +using Xunit.Sdk; +using VerifyCS = Amazon.Lambda.Annotations.SourceGenerators.Tests.CSharpSourceGeneratorVerifier; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests +{ + public partial class SourceGeneratorTests + { + [Fact(DisplayName = nameof(MessageHandlerForPreExistingQueue))] + public async Task MessageHandlerForPreExistingQueue() + { + const string generatedNewFifoQueueUsingFnSubForQueueName = "Messaging_MessageHandlerForNewFifoQueueUsingFnSubForQueueName_Generated.g.cs"; + + const string generatedNewFifoQueueWithoutAQueueName = "Messaging_MessageHandlerForNewFifoQueueWithoutAQueueName_Generated.g.cs"; + + var expectedTemplateContent = File.ReadAllText(Path.Combine("Snapshots", "ServerlessTemplates", "messaging.template")).ToEnvironmentLineEndings(); + var expectedMessageHandlerForPreExistingQueueGenerated = File.ReadAllText(Path.Combine("Snapshots", "Messaging_MessageHandlerForPreExistingQueue_Generated.g.cs")).ToEnvironmentLineEndings(); + var expectedMessageHandlerForNewQueueGenerated = File.ReadAllText(Path.Combine("Snapshots", "Messaging_MessageHandlerForNewQueue_Generated.g.cs")).ToEnvironmentLineEndings(); + var expectedMessageHandlerForNewFifoQueueUsingFnSubForQueueName = File.ReadAllText(Path.Combine("Snapshots", generatedNewFifoQueueUsingFnSubForQueueName)).ToEnvironmentLineEndings(); + + var expectedNewFifoQueueWithoutAQueueName = File.ReadAllText(Path.Combine("Snapshots", generatedNewFifoQueueWithoutAQueueName)).ToEnvironmentLineEndings(); + + try + { + await new VerifyCS.Test + { + TestState = + { + Sources = + { + (Path.Combine("TestServerlessApp", "Messaging.cs"), File.ReadAllText(Path.Combine("TestServerlessApp", "Messaging.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "LambdaFunctionAttribute.cs"))), + (Path.Combine("Amazon.Lambda.Annotations", "LambdaStartupAttribute.cs"), File.ReadAllText(Path.Combine("Amazon.Lambda.Annotations", "LambdaStartupAttribute.cs"))), + }, + GeneratedSources = + { + ( + typeof(SourceGenerator.Generator), + "Messaging_MessageHandlerForPreExistingQueue_Generated.g.cs", + SourceText.From(expectedMessageHandlerForPreExistingQueueGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + "Messaging_MessageHandlerForNewQueue_Generated.g.cs", + SourceText.From(expectedMessageHandlerForNewQueueGenerated, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + generatedNewFifoQueueUsingFnSubForQueueName, + SourceText.From(expectedMessageHandlerForNewFifoQueueUsingFnSubForQueueName, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ), + ( + typeof(SourceGenerator.Generator), + generatedNewFifoQueueWithoutAQueueName, + SourceText.From(expectedNewFifoQueueWithoutAQueueName, Encoding.UTF8, SourceHashAlgorithm.Sha256) + ) + }, + ExpectedDiagnostics = + { + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("Messaging_MessageHandlerForPreExistingQueue_Generated.g.cs", expectedMessageHandlerForPreExistingQueueGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments("Messaging_MessageHandlerForNewQueue_Generated.g.cs", expectedMessageHandlerForNewQueueGenerated), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments(generatedNewFifoQueueUsingFnSubForQueueName, expectedMessageHandlerForNewFifoQueueUsingFnSubForQueueName), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments(generatedNewFifoQueueWithoutAQueueName, expectedNewFifoQueueWithoutAQueueName), + new DiagnosticResult("AWSLambda0103", DiagnosticSeverity.Info).WithArguments($"TestServerlessApp{Path.DirectorySeparatorChar}serverless.template", expectedTemplateContent) + } + } + }.RunAsync(); + + } + finally + { + var actualTemplateContent = File.ReadAllText(Path.Combine("TestServerlessApp", "serverless.template")); + Assert.Equal(expectedTemplateContent, actualTemplateContent); + } + } + } +} diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs index 9d83ca180..08f415bc0 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs @@ -11,7 +11,7 @@ namespace Amazon.Lambda.Annotations.SourceGenerators.Tests { - public class SourceGeneratorTests + public partial class SourceGeneratorTests { [Fact] public async Task Greeter() diff --git a/Libraries/test/TestServerlessApp/Messaging.cs b/Libraries/test/TestServerlessApp/Messaging.cs new file mode 100644 index 000000000..4e1881356 --- /dev/null +++ b/Libraries/test/TestServerlessApp/Messaging.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.Lambda.Annotations; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; +using Amazon.Lambda.SQSEvents; + +namespace TestServerlessApp +{ + public class Messaging + { + /// + /// This function will use a preexisting queue, whether the queue is defined manually in the template or external to the template. + /// + /// + /// + /// + [LambdaFunction] + [SqsMessage(EventQueueARN = "arn:aws:sqs:us-east-1:968993296699:app-deploy-blue-LAVETRYB3JKX-SomeQueueName", EventBatchSize = 11)] + public async Task MessageHandlerForPreExistingQueue(SQSEvent.SQSMessage message, ILambdaContext context) + { + LambdaLogger.Log($"Message Received: {message.MessageId}"); + // the Task is a hack to work with the current generator + return 0; + } + + /// + /// This function will add the AWS::SQS::Queue to the template and use it for the function. + /// All Serverless Event attributes start with the word Event. + /// All others are the AWS::SQS::Queue properties. + /// + /// + /// + /// + [LambdaFunction] + [SqsMessage( + EventBatchSize = 12, + EventFilterCriteria = new string[] { "Filter1", "Filter2" }, + EventMaximumBatchingWindowInSeconds = 31, + VisibilityTimeout = 100, + ContentBasedDeduplication = true, + DeduplicationScope = "queue", + DelaySeconds = 5, + FifoQueue = true, + FifoThroughputLimit = "perQueue", + KmsDataKeyReusePeriodSeconds = 299, + KmsMasterKeyId = "alias/aws/sqs", + MaximumMessageSize = 1024, + MessageRetentionPeriod = 60, + QueueName = "thisismyqueuename.fifo", + ReceiveMessageWaitTimeSeconds =5, + RedriveAllowPolicy = "{ 'redrivePermission' : 'denyAll' }", + RedrivePolicy = "{ 'deadLetterTargetArn': 'arn:somewhere', 'maxReceiveCount': 5 }", + Tags = new string[]{ "keyname1=value1", "keyname2=value2" })] + public async Task MessageHandlerForNewQueue(SQSEvent.SQSMessage message, ILambdaContext context) + { + LambdaLogger.Log($"Message Received: {message.MessageId}"); + // the Task is a hack to work with the current generator + return 0; + } + + [LambdaFunction] + [SqsMessage( + FifoQueue = true, + QueueName = "{ 'Fn::Sub' : '${AWS::Stack}MyFifoQueueWithStackEmbedded.fifo' }")] + public async Task MessageHandlerForNewFifoQueueUsingFnSubForQueueName(SQSEvent.SQSMessage message, ILambdaContext context) + { + LambdaLogger.Log($"Message Received: {message.MessageId}"); + // the Task is a hack to work with the current generator + return 0; + } + + /// + /// This demonstrates (and tests) the ability to create + /// a Fifo queue without specifying a QueueName + /// + /// + /// + /// + [LambdaFunction] + [SqsMessage(FifoQueue = true)] + public async Task MessageHandlerForNewFifoQueueWithoutAQueueName(SQSEvent.SQSMessage message, ILambdaContext context) + { + LambdaLogger.Log($"Message Received: {message.MessageId}"); + // the Task is a hack to work with the current generator + return 0; + } + + } +} \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/TestServerlessApp.csproj b/Libraries/test/TestServerlessApp/TestServerlessApp.csproj index 463ba0d4f..e3f8d5603 100644 --- a/Libraries/test/TestServerlessApp/TestServerlessApp.csproj +++ b/Libraries/test/TestServerlessApp/TestServerlessApp.csproj @@ -21,5 +21,6 @@ + \ No newline at end of file diff --git a/Libraries/test/TestServerlessApp/serverless.template b/Libraries/test/TestServerlessApp/serverless.template index b388d8685..eed3cb059 100644 --- a/Libraries/test/TestServerlessApp/serverless.template +++ b/Libraries/test/TestServerlessApp/serverless.template @@ -387,6 +387,214 @@ ] } } + }, + "TestServerlessAppMessagingMessageHandlerForPreExistingQueueGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "Sqs" + ] + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 256, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp::TestServerlessApp.Messaging_MessageHandlerForPreExistingQueue_Generated::MessageHandlerForPreExistingQueue", + "Events": { + "Sqs": { + "Type": "SQS", + "Properties": { + "BatchSize": 11, + "Queue": "arn:aws:sqs:us-east-1:968993296699:app-deploy-blue-LAVETRYB3JKX-SomeQueueName" + } + } + } + } + }, + "TestServerlessAppMessagingMessageHandlerForNewQueueGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "Sqs" + ], + "SyncedResources": [ + "TestServerlessAppMessagingMessageHandlerForNewQueueGeneratedQueue" + ] + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 256, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp::TestServerlessApp.Messaging_MessageHandlerForNewQueue_Generated::MessageHandlerForNewQueue", + "Events": { + "Sqs": { + "Type": "SQS", + "Properties": { + "BatchSize": 12, + "Queue": { + "Fn::GetAtt": [ + "TestServerlessAppMessagingMessageHandlerForNewQueueGeneratedQueue", + "Arn" + ] + }, + "FilterCriteria": { + "Filters": [ + { + "Pattern": "Filter1" + }, + { + "Pattern": "Filter2" + } + ] + }, + "MaximumBatchingWindowInSeconds": 31 + } + } + } + } + }, + "TestServerlessAppMessagingMessageHandlerForNewQueueGeneratedQueue": { + "Type": "AWS::SQS::Queue", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "ContentBasedDeduplication": true, + "DeduplicationScope": "queue", + "DelaySeconds": 5, + "FifoQueue": true, + "FifoThroughputLimit": "perQueue", + "KmsDataKeyReusePeriodSeconds": 299, + "KmsMasterKeyId": "alias/aws/sqs", + "MaximumMessageSize": 1024, + "MessageRetentionPeriod": 60, + "QueueName": "thisismyqueuename.fifo", + "ReceiveMessageWaitTimeSeconds": 5, + "RedriveAllowPolicy": { + "redrivePermission": "denyAll" + }, + "RedrivePolicy": { + "deadLetterTargetArn": "arn:somewhere", + "maxReceiveCount": 5 + }, + "Tags": [ + { + "Key": "keyname1", + "Value": "value1" + }, + { + "Key": "keyname2", + "Value": "value2" + } + ], + "VisibilityTimeout": 100 + } + }, + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueUsingFnSubForQueueNameGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "Sqs" + ], + "SyncedResources": [ + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueUsingFnSubForQueueNameGeneratedQueue" + ] + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 256, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp::TestServerlessApp.Messaging_MessageHandlerForNewFifoQueueUsingFnSubForQueueName_Generated::MessageHandlerForNewFifoQueueUsingFnSubForQueueName", + "Events": { + "Sqs": { + "Type": "SQS", + "Properties": { + "Queue": { + "Fn::GetAtt": [ + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueUsingFnSubForQueueNameGeneratedQueue", + "Arn" + ] + } + } + } + } + } + }, + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueUsingFnSubForQueueNameGeneratedQueue": { + "Type": "AWS::SQS::Queue", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "FifoQueue": true, + "QueueName": { + "Fn::Sub": "${AWS::Stack}MyFifoQueueWithStackEmbedded.fifo" + } + } + }, + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueWithoutAQueueNameGenerated": { + "Type": "AWS::Serverless::Function", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations", + "SyncedEvents": [ + "Sqs" + ], + "SyncedResources": [ + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueWithoutAQueueNameGeneratedQueue" + ] + }, + "Properties": { + "Runtime": "dotnet6", + "CodeUri": ".", + "MemorySize": 256, + "Timeout": 30, + "Policies": [ + "AWSLambdaBasicExecutionRole" + ], + "PackageType": "Zip", + "Handler": "TestServerlessApp::TestServerlessApp.Messaging_MessageHandlerForNewFifoQueueWithoutAQueueName_Generated::MessageHandlerForNewFifoQueueWithoutAQueueName", + "Events": { + "Sqs": { + "Type": "SQS", + "Properties": { + "Queue": { + "Fn::GetAtt": [ + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueWithoutAQueueNameGeneratedQueue", + "Arn" + ] + } + } + } + } + } + }, + "TestServerlessAppMessagingMessageHandlerForNewFifoQueueWithoutAQueueNameGeneratedQueue": { + "Type": "AWS::SQS::Queue", + "Metadata": { + "Tool": "Amazon.Lambda.Annotations" + }, + "Properties": { + "FifoQueue": true, + "QueueName": "TestServerlessAppMessagingMessageHandlerForNewFifoQueueWithoutAQueueNameGeneratedQueue.fifo" + } } }, "Outputs": {