Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .autover/changes/add-snsevent-annotation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "Amazon.Lambda.Annotations",
"Type": "Minor",
"ChangelogMessages": [
"Added [SNSEvent] annotation attribute for declaratively configuring SNS topic-triggered Lambda functions with support for topic reference, filter policy, and enabled state."
]
}
]
}
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @aws/aws-sdk-dotnet-team
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ AWSLambda0133 | AWSLambdaCSharpGenerator | Error | ALB Listener Reference Not Fo
AWSLambda0134 | AWSLambdaCSharpGenerator | Error | FromRoute not supported on ALB functions
AWSLambda0135 | AWSLambdaCSharpGenerator | Error | Unmapped parameter on ALB function
AWSLambda0136 | AWSLambdaCSharpGenerator | Error | Invalid S3EventAttribute
AWSLambda0138 | AWSLambdaCSharpGenerator | Error | Invalid SNSEventAttribute
Original file line number Diff line number Diff line change
Expand Up @@ -281,5 +281,12 @@ public static class DiagnosticDescriptors
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor InvalidSnsEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0138",
title: "Invalid SNSEventAttribute",
messageFormat: "Invalid SNSEventAttribute encountered: {0}",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
Comment thread
GarrettBeatty marked this conversation as resolved.
isEnabledByDefault: true);
Comment thread
GarrettBeatty marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.SNSEventAttribute), SymbolEqualityComparer.Default))
{
var data = SNSEventAttributeBuilder.Build(att);
model = new AttributeModel<SNS.SNSEventAttribute>
{
Data = data,
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.HttpApiAuthorizerAttribute), SymbolEqualityComparer.Default))
{
var data = HttpApiAuthorizerAttributeBuilder.Build(att);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using Amazon.Lambda.Annotations.SNS;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add license header

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added everywhere

using Microsoft.CodeAnalysis;
using System;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
{
/// <summary>
/// Builder for <see cref="SNSEventAttribute"/>.
/// </summary>
public class SNSEventAttributeBuilder
{
public static SNSEventAttribute Build(AttributeData att)
{
if (att.ConstructorArguments.Length != 1)
{
throw new NotSupportedException($"{TypeFullNames.SNSEventAttribute} must have constructor with 1 argument.");
}
var topic = att.ConstructorArguments[0].Value as string;
var data = new SNSEventAttribute(topic);

foreach (var pair in att.NamedArguments)
{
if (pair.Key == nameof(data.ResourceName) && pair.Value.Value is string resourceName)
{
data.ResourceName = resourceName;
}
else if (pair.Key == nameof(data.FilterPolicy) && pair.Value.Value is string filterPolicy)
{
data.FilterPolicy = filterPolicy;
}
else if (pair.Key == nameof(data.Enabled) && pair.Value.Value is bool enabled)
{
data.Enabled = enabled;
}
}

return data;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum EventType
API,
S3,
SQS,
SNS,
DynamoDB,
Schedule,
Authorizer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
{
events.Add(EventType.S3);
}
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.SNSEventAttribute)
{
events.Add(EventType.SNS);
}
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAuthorizerAttribute
|| attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAuthorizerAttribute)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ internal class SyntaxReceiver : ISyntaxContextReceiver
{ "FunctionUrlAttribute", "FunctionUrl" },
{ "SQSEventAttribute", "SQSEvent" },
{ "ALBApiAttribute", "ALBApi" },
{ "S3EventAttribute", "S3Event" }
{ "S3EventAttribute", "S3Event" },
{ "SNSEventAttribute", "SNSEvent" }
};

public List<MethodDeclarationSyntax> LambdaMethods { get; } = new List<MethodDeclarationSyntax>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ public static class TypeFullNames
public const string S3Event = "Amazon.Lambda.S3Events.S3Event";
public const string S3EventAttribute = "Amazon.Lambda.Annotations.S3.S3EventAttribute";

public const string SNSEvent = "Amazon.Lambda.SNSEvents.SNSEvent";
public const string SNSEventAttribute = "Amazon.Lambda.Annotations.SNS.SNSEventAttribute";

public const string LambdaSerializerAttribute = "Amazon.Lambda.Core.LambdaSerializerAttribute";
public const string DefaultLambdaSerializer = "Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer";

Expand Down Expand Up @@ -91,7 +94,8 @@ public static class TypeFullNames
FunctionUrlAttribute,
SQSEventAttribute,
ALBApiAttribute,
S3EventAttribute
S3EventAttribute,
SNSEventAttribute
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Amazon.Lambda.Annotations.SourceGenerator.Extensions;
using Amazon.Lambda.Annotations.SourceGenerator.Models;
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
using Amazon.Lambda.Annotations.SNS;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;
using System.Collections.Generic;
Expand Down Expand Up @@ -64,6 +65,7 @@ internal static bool ValidateFunction(GeneratorExecutionContext context, IMethod
// Validate Events
ValidateApiGatewayEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateSnsEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateAlbEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateS3Events(lambdaFunctionModel, methodLocation, diagnostics);

Expand Down Expand Up @@ -114,6 +116,16 @@ internal static bool ValidateDependencies(GeneratorExecutionContext context, IMe
}
}

// Check for references to "Amazon.Lambda.SNSEvents" if the Lambda method is annotated with SNSEvent attribute.
if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.SNSEventAttribute))
{
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.SNSEvents") == null)
{
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.SNSEvents"));
return false;
}
}

return true;
}

Expand Down Expand Up @@ -424,6 +436,45 @@ private static void ValidateS3Events(LambdaFunctionModel lambdaFunctionModel, Lo
}
}

private static void ValidateSnsEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List<Diagnostic> diagnostics)
{
if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.SNS))
{
return;
}

// Validate SNSEventAttributes
foreach (var att in lambdaFunctionModel.Attributes)
{
if (att.Type.FullName != TypeFullNames.SNSEventAttribute)
continue;

var snsEventAttribute = ((AttributeModel<SNSEventAttribute>)att).Data;
var validationErrors = snsEventAttribute.Validate();
validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidSnsEventAttribute, methodLocation, errorMessage)));
}

// Validate method parameters - When using SNSEventAttribute, the method signature must be (SNSEvent snsEvent) or (SNSEvent snsEvent, ILambdaContext context)
var parameters = lambdaFunctionModel.LambdaMethod.Parameters;
if (parameters.Count == 0 ||
parameters.Count > 2 ||
(parameters.Count == 1 && parameters[0].Type.FullName != TypeFullNames.SNSEvent) ||
(parameters.Count == 2 && (parameters[0].Type.FullName != TypeFullNames.SNSEvent || parameters[1].Type.FullName != TypeFullNames.ILambdaContext)))
{
var errorMessage = $"When using the {nameof(SNSEventAttribute)}, the Lambda method can accept at most 2 parameters. " +
$"The first parameter is required and must be of type {TypeFullNames.SNSEvent}. " +
$"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}.";
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
}

// Validate method return type - When using SNSEventAttribute, the return type must be either void or Task
if (!lambdaFunctionModel.LambdaMethod.ReturnsVoid && !lambdaFunctionModel.LambdaMethod.ReturnsVoidTask)
{
var errorMessage = $"When using the {nameof(SNSEventAttribute)}, the Lambda method can return either void or {TypeFullNames.Task}";
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
}
}

private static bool ReportDiagnostics(DiagnosticReporter diagnosticReporter, List<Diagnostic> diagnostics)
{
var isValid = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics;
using Amazon.Lambda.Annotations.SourceGenerator.FileIO;
using Amazon.Lambda.Annotations.SourceGenerator.Models;
using Amazon.Lambda.Annotations.SNS;
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
using Amazon.Lambda.Annotations.S3;
using Amazon.Lambda.Annotations.SQS;
Expand Down Expand Up @@ -241,6 +242,10 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
_templateWriter.SetToken($"Resources.{lambdaFunction.ResourceName}.Metadata.SyncedFunctionUrlConfig", true);
hasFunctionUrl = true;
break;
case AttributeModel<SNSEventAttribute> snsAttributeModel:
eventName = ProcessSnsAttribute(lambdaFunction, snsAttributeModel.Data, currentSyncedEventProperties);
currentSyncedEvents.Add(eventName);
break;
}
}

Expand Down Expand Up @@ -673,6 +678,43 @@ private string ProcessSqsAttribute(ILambdaFunctionSerializable lambdaFunction, S
return att.ResourceName;
}

/// <summary>
/// Writes all properties associated with <see cref="SNSEventAttribute"/> to the serverless template.
/// </summary>
private string ProcessSnsAttribute(ILambdaFunctionSerializable lambdaFunction, SNSEventAttribute att, Dictionary<string, List<string>> syncedEventProperties)
{
var eventName = att.ResourceName;
var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}";

_templateWriter.SetToken($"{eventPath}.Type", "SNS");

// Topic - SNS topics use Ref to get the ARN
_templateWriter.RemoveToken($"{eventPath}.Properties.Topic");
if (!att.Topic.StartsWith("@"))
{
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Topic", att.Topic);
}
else
{
var topic = att.Topic.Substring(1);
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Topic.{REF}", topic);
}

// FilterPolicy
if (att.IsFilterPolicySet)
{
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "FilterPolicy", att.FilterPolicy);
}

// Enabled
if (att.IsEnabledSet)
{
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Enabled", att.Enabled);
}

return att.ResourceName;
}

/// <summary>
/// Writes all properties associated with <see cref="S3EventAttribute"/> to the serverless template.
/// </summary>
Expand Down
20 changes: 20 additions & 0 deletions Libraries/src/Amazon.Lambda.Annotations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,24 @@ The `ALBApi` attribute requires an existing ALB listener. Here is a minimal exam

Then your Lambda function references `@MyListener` in the `ALBApi` attribute.

## SNS Event Example
This example shows how to use the `SNSEvent` attribute to subscribe a Lambda function to an SNS topic.

The `SNSEvent` attribute contains the following properties:
* **Topic** (Required) - The SNS topic ARN or a reference to an SNS topic resource prefixed with "@".
* **ResourceName** (Optional) - The CloudFormation resource name for the SNS event.
* **FilterPolicy** (Optional) - A JSON filter policy applied to the subscription.
* **Enabled** (Optional) - If false, the event source is disabled. Default is true.

```csharp
[LambdaFunction(ResourceName = "SNSMessageHandler", Policies = "AWSLambdaSNSTopicExecutionRole")]
[SNSEvent("@TestTopic", ResourceName = "TestTopicEvent", FilterPolicy = "{ \"store\": [\"example_corp\"] }")]
public void HandleMessage(SNSEvent evnt, ILambdaContext lambdaContext)
{
lambdaContext.Logger.Log($"Received {evnt.Records.Count} messages");
}
```

## Lambda Function URL Example

[Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) provide a dedicated HTTPS endpoint for your Lambda function without needing API Gateway or an Application Load Balancer. The `FunctionUrl` attribute configures the function to be invoked via a Function URL. Function URLs use the same payload format as HTTP API v2 (`APIGatewayHttpApiV2ProxyRequest`/`APIGatewayHttpApiV2ProxyResponse`).
Expand Down Expand Up @@ -1525,6 +1543,8 @@ parameter to the `LambdaFunction` must be the event object and the event source
* Marks a Lambda function as a REST API (API Gateway V1) custom authorizer. The authorizer name is automatically derived from the method name. Other functions reference it via `RestApi.Authorizer` using `nameof()`. Use the `Type` property to choose between `Token` and `Request` authorizer types.
* SQSEvent
* Sets up event source mapping between the Lambda function and SQS queues. The SQS queue ARN is required to be set on the attribute. If users want to pass a reference to an existing SQS queue resource defined in their CloudFormation template, they can pass the SQS queue resource name prefixed with the '@' symbol.
* SNSEvent
* Subscribes the Lambda function to an SNS topic. The topic ARN or resource reference (prefixed with '@') is required.
* ALBApi
* Configures the Lambda function to be called from an Application Load Balancer. The listener ARN (or `@ResourceName` template reference), path pattern, and priority are required. The source generator creates standalone CloudFormation resources (TargetGroup, ListenerRule, Lambda Permission) rather than SAM event types.
* FunctionUrl
Expand Down
Loading
Loading