Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ AWSLambda0128 | AWSLambdaCSharpGenerator | Error | Authorizer Payload Version Mi
AWSLambda0129 | AWSLambdaCSharpGenerator | Error | Missing LambdaFunction Attribute
AWSLambda0130 | AWSLambdaCSharpGenerator | Error | Invalid return type IAuthorizerResult
AWSLambda0131 | AWSLambdaCSharpGenerator | Error | FromBody not supported on Authorizer functions
AWSLambda0134 | AWSLambdaCSharpGenerator | Error | Invalid SNSEventAttribute
Original file line number Diff line number Diff line change
Expand Up @@ -242,5 +242,12 @@ public static class DiagnosticDescriptors
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor InvalidSnsEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0134",
title: "Invalid SNSEventAttribute",
messageFormat: "Invalid SNSEventAttribute encountered: {0}",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.Annotations.SNS;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;

Expand Down Expand Up @@ -90,6 +91,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<Amazon.Lambda.Annotations.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,40 @@
using Amazon.Lambda.Annotations.SNS;
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 @@ -26,6 +26,10 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
{
events.Add(EventType.SQS);
}
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 @@ -21,7 +21,8 @@ internal class SyntaxReceiver : ISyntaxContextReceiver
{ "RestApiAuthorizerAttribute", "RestApiAuthorizer" },
{ "HttpApiAttribute", "HttpApi" },
{ "RestApiAttribute", "RestApi" },
{ "SQSEventAttribute", "SQSEvent" }
{ "SQSEventAttribute", "SQSEvent" },
{ "SNSEventAttribute", "SNSEvent" }
};

public List<MethodDeclarationSyntax> LambdaMethods { get; } = new List<MethodDeclarationSyntax>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public static class TypeFullNames
public const string SQSBatchResponse = "Amazon.Lambda.SQSEvents.SQSBatchResponse";
public const string SQSEventAttribute = "Amazon.Lambda.Annotations.SQS.SQSEventAttribute";

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 All @@ -67,7 +70,8 @@ public static class TypeFullNames
{
RestApiAttribute,
HttpApiAttribute,
SQSEventAttribute
SQSEventAttribute,
SNSEventAttribute
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,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 @@ -59,6 +60,7 @@ internal static bool ValidateFunction(GeneratorExecutionContext context, IMethod
// Validate Events
ValidateApiGatewayEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateSnsEvents(lambdaFunctionModel, methodLocation, diagnostics);

return ReportDiagnostics(diagnosticReporter, diagnostics);
}
Expand Down Expand Up @@ -86,6 +88,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 @@ -268,6 +280,45 @@ private static void ValidateSqsEvents(LambdaFunctionModel lambdaFunctionModel, L
}
}

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 @@ -3,6 +3,7 @@
using Amazon.Lambda.Annotations.SourceGenerator.FileIO;
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;
Expand Down Expand Up @@ -221,6 +222,10 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
eventName = ProcessSqsAttribute(lambdaFunction, sqsAttributeModel.Data, currentSyncedEventProperties);
currentSyncedEvents.Add(eventName);
break;
case AttributeModel<SNSEventAttribute> snsAttributeModel:
eventName = ProcessSnsAttribute(lambdaFunction, snsAttributeModel.Data, currentSyncedEventProperties);
currentSyncedEvents.Add(eventName);
break;
}
}

Expand Down Expand Up @@ -596,6 +601,46 @@ 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);
if (_templateWriter.Exists($"{PARAMETERS}.{topic}"))
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Topic.{REF}", topic);
else
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="LambdaFunctionRoleAttribute"/> to the serverless template.
/// </summary>
Expand Down
99 changes: 99 additions & 0 deletions Libraries/src/Amazon.Lambda.Annotations/SNS/SNSEventAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Amazon.Lambda.Annotations.SNS
{
/// <summary>
/// This attribute defines the SNS event source configuration for a Lambda function.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class SNSEventAttribute : Attribute
{
private static readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$");

/// <summary>
/// The SNS topic that will act as the event trigger for the Lambda function.
/// This can either be the topic ARN or reference to the SNS topic resource that is already defined in the serverless template.
/// To reference an SNS topic resource in the serverless template, prefix the resource name with "@" symbol.
/// </summary>
public string Topic { get; set; }

/// <summary>
/// The CloudFormation resource name for the SNS event. By default this is set to the SNS topic name if the <see cref="Topic"/> is set to an SNS topic ARN.
/// If <see cref="Topic"/> is set to an existing CloudFormation resource, than that is used as the default value without the "@" prefix.
/// </summary>
public string ResourceName
{
get
{
if (IsResourceNameSet)
{
return resourceName;
}
if (Topic.StartsWith("@"))
{
return Topic.Substring(1);
}

var arnTokens = Topic.Split(new char[] { ':' }, 6);
var topicName = arnTokens[5];
var sanitizedTopicName = string.Join(string.Empty, topicName.Where(char.IsLetterOrDigit));
return sanitizedTopicName;
}
set => resourceName = value;
}

private string resourceName { get; set; } = null;
internal bool IsResourceNameSet => resourceName != null;

/// <summary>
/// A JSON filter policy that is applied to the SNS subscription.
/// Only messages matching the filter policy will be delivered to the Lambda function.
/// </summary>
public string FilterPolicy { get; set; } = null;
internal bool IsFilterPolicySet => FilterPolicy != null;

/// <summary>
/// If set to false, the event source will be disabled.
/// Default value is true.
/// </summary>
public bool Enabled
{
get => enabled.GetValueOrDefault();
set => enabled = value;
}
private bool? enabled { get; set; }
internal bool IsEnabledSet => enabled.HasValue;

/// <summary>
/// Creates an instance of the <see cref="SNSEventAttribute"/> class.
/// </summary>
/// <param name="topic"><see cref="Topic"/> property</param>
public SNSEventAttribute(string topic)
{
Topic = topic;
}

internal List<string> Validate()
{
var validationErrors = new List<string>();

if (!Topic.StartsWith("@"))
{
var arnTokens = Topic.Split(new char[] { ':' }, 6);
if (arnTokens.Length != 6)
{
validationErrors.Add($"{nameof(SNSEventAttribute.Topic)} = {Topic}. The SNS topic ARN is invalid. The ARN format is 'arn:<partition>:sns:<region>:<account-id>:<topic-name>'");
}
}
if (IsResourceNameSet && !_resourceNameRegex.IsMatch(ResourceName))
{
validationErrors.Add($"{nameof(SNSEventAttribute.ResourceName)} = {ResourceName}. It must only contain alphanumeric characters and must not be an empty string");
}

return validationErrors;
}
}
}
Loading
Loading