Skip to content

Commit 9f9a0a7

Browse files
committed
Add [SNSEvent] annotation core implementation
- SNSEventAttribute with Topic (required), ResourceName, FilterPolicy, Enabled - SNSEventAttributeBuilder for Roslyn AttributeData parsing - TypeFullNames constants and Events hashset - EventType.SNS enum value and EventTypeBuilder - SyntaxReceiver secondary attribute registration - AttributeModelBuilder else-if branch - CloudFormationWriter ProcessSnsAttribute (SAM SNS event type) - LambdaFunctionValidator: ValidateSnsEvents + dependency check - DiagnosticDescriptors: InvalidSnsEventAttribute (AWSLambda0134)
1 parent be86153 commit 9f9a0a7

10 files changed

Lines changed: 264 additions & 2 deletions

File tree

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,5 +242,12 @@ public static class DiagnosticDescriptors
242242
category: "AWSLambdaCSharpGenerator",
243243
DiagnosticSeverity.Error,
244244
isEnabledByDefault: true);
245+
246+
public static readonly DiagnosticDescriptor InvalidSnsEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0134",
247+
title: "Invalid SNSEventAttribute",
248+
messageFormat: "Invalid SNSEventAttribute encountered: {0}",
249+
category: "AWSLambdaCSharpGenerator",
250+
DiagnosticSeverity.Error,
251+
isEnabledByDefault: true);
245252
}
246253
}

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using Amazon.Lambda.Annotations.APIGateway;
3+
using Amazon.Lambda.Annotations.SNS;
34
using Amazon.Lambda.Annotations.SQS;
45
using Microsoft.CodeAnalysis;
56

@@ -90,6 +91,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
9091
Type = TypeModelBuilder.Build(att.AttributeClass, context)
9192
};
9293
}
94+
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.SNSEventAttribute), SymbolEqualityComparer.Default))
95+
{
96+
var data = SNSEventAttributeBuilder.Build(att);
97+
model = new AttributeModel<Amazon.Lambda.Annotations.SNS.SNSEventAttribute>
98+
{
99+
Data = data,
100+
Type = TypeModelBuilder.Build(att.AttributeClass, context)
101+
};
102+
}
93103
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.HttpApiAuthorizerAttribute), SymbolEqualityComparer.Default))
94104
{
95105
var data = HttpApiAuthorizerAttributeBuilder.Build(att);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Amazon.Lambda.Annotations.SNS;
2+
using Microsoft.CodeAnalysis;
3+
using System;
4+
5+
namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
6+
{
7+
/// <summary>
8+
/// Builder for <see cref="SNSEventAttribute"/>.
9+
/// </summary>
10+
public class SNSEventAttributeBuilder
11+
{
12+
public static SNSEventAttribute Build(AttributeData att)
13+
{
14+
if (att.ConstructorArguments.Length != 1)
15+
{
16+
throw new NotSupportedException($"{TypeFullNames.SNSEventAttribute} must have constructor with 1 argument.");
17+
}
18+
var topic = att.ConstructorArguments[0].Value as string;
19+
var data = new SNSEventAttribute(topic);
20+
21+
foreach (var pair in att.NamedArguments)
22+
{
23+
if (pair.Key == nameof(data.ResourceName) && pair.Value.Value is string resourceName)
24+
{
25+
data.ResourceName = resourceName;
26+
}
27+
else if (pair.Key == nameof(data.FilterPolicy) && pair.Value.Value is string filterPolicy)
28+
{
29+
data.FilterPolicy = filterPolicy;
30+
}
31+
else if (pair.Key == nameof(data.Enabled) && pair.Value.Value is bool enabled)
32+
{
33+
data.Enabled = enabled;
34+
}
35+
}
36+
37+
return data;
38+
}
39+
}
40+
}

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventType.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public enum EventType
99
API,
1010
S3,
1111
SQS,
12+
SNS,
1213
DynamoDB,
1314
Schedule,
1415
Authorizer

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
2626
{
2727
events.Add(EventType.SQS);
2828
}
29+
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.SNSEventAttribute)
30+
{
31+
events.Add(EventType.SNS);
32+
}
2933
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAuthorizerAttribute
3034
|| attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAuthorizerAttribute)
3135
{

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ internal class SyntaxReceiver : ISyntaxContextReceiver
2121
{ "RestApiAuthorizerAttribute", "RestApiAuthorizer" },
2222
{ "HttpApiAttribute", "HttpApi" },
2323
{ "RestApiAttribute", "RestApi" },
24-
{ "SQSEventAttribute", "SQSEvent" }
24+
{ "SQSEventAttribute", "SQSEvent" },
25+
{ "SNSEventAttribute", "SNSEvent" }
2526
};
2627

2728
public List<MethodDeclarationSyntax> LambdaMethods { get; } = new List<MethodDeclarationSyntax>();

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public static class TypeFullNames
4646
public const string SQSBatchResponse = "Amazon.Lambda.SQSEvents.SQSBatchResponse";
4747
public const string SQSEventAttribute = "Amazon.Lambda.Annotations.SQS.SQSEventAttribute";
4848

49+
public const string SNSEvent = "Amazon.Lambda.SNSEvents.SNSEvent";
50+
public const string SNSEventAttribute = "Amazon.Lambda.Annotations.SNS.SNSEventAttribute";
51+
4952
public const string LambdaSerializerAttribute = "Amazon.Lambda.Core.LambdaSerializerAttribute";
5053
public const string DefaultLambdaSerializer = "Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer";
5154

@@ -67,7 +70,8 @@ public static class TypeFullNames
6770
{
6871
RestApiAttribute,
6972
HttpApiAttribute,
70-
SQSEventAttribute
73+
SQSEventAttribute,
74+
SNSEventAttribute
7175
};
7276
}
7377
}

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Amazon.Lambda.Annotations.SourceGenerator.Extensions;
33
using Amazon.Lambda.Annotations.SourceGenerator.Models;
44
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
5+
using Amazon.Lambda.Annotations.SNS;
56
using Amazon.Lambda.Annotations.SQS;
67
using Microsoft.CodeAnalysis;
78
using System.Collections.Generic;
@@ -59,6 +60,7 @@ internal static bool ValidateFunction(GeneratorExecutionContext context, IMethod
5960
// Validate Events
6061
ValidateApiGatewayEvents(lambdaFunctionModel, methodLocation, diagnostics);
6162
ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics);
63+
ValidateSnsEvents(lambdaFunctionModel, methodLocation, diagnostics);
6264

6365
return ReportDiagnostics(diagnosticReporter, diagnostics);
6466
}
@@ -86,6 +88,16 @@ internal static bool ValidateDependencies(GeneratorExecutionContext context, IMe
8688
}
8789
}
8890

91+
// Check for references to "Amazon.Lambda.SNSEvents" if the Lambda method is annotated with SNSEvent attribute.
92+
if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.SNSEventAttribute))
93+
{
94+
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.SNSEvents") == null)
95+
{
96+
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.SNSEvents"));
97+
return false;
98+
}
99+
}
100+
89101
return true;
90102
}
91103

@@ -268,6 +280,45 @@ private static void ValidateSqsEvents(LambdaFunctionModel lambdaFunctionModel, L
268280
}
269281
}
270282

283+
private static void ValidateSnsEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List<Diagnostic> diagnostics)
284+
{
285+
if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.SNS))
286+
{
287+
return;
288+
}
289+
290+
// Validate SNSEventAttributes
291+
foreach (var att in lambdaFunctionModel.Attributes)
292+
{
293+
if (att.Type.FullName != TypeFullNames.SNSEventAttribute)
294+
continue;
295+
296+
var snsEventAttribute = ((AttributeModel<SNSEventAttribute>)att).Data;
297+
var validationErrors = snsEventAttribute.Validate();
298+
validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidSnsEventAttribute, methodLocation, errorMessage)));
299+
}
300+
301+
// Validate method parameters - When using SNSEventAttribute, the method signature must be (SNSEvent snsEvent) or (SNSEvent snsEvent, ILambdaContext context)
302+
var parameters = lambdaFunctionModel.LambdaMethod.Parameters;
303+
if (parameters.Count == 0 ||
304+
parameters.Count > 2 ||
305+
(parameters.Count == 1 && parameters[0].Type.FullName != TypeFullNames.SNSEvent) ||
306+
(parameters.Count == 2 && (parameters[0].Type.FullName != TypeFullNames.SNSEvent || parameters[1].Type.FullName != TypeFullNames.ILambdaContext)))
307+
{
308+
var errorMessage = $"When using the {nameof(SNSEventAttribute)}, the Lambda method can accept at most 2 parameters. " +
309+
$"The first parameter is required and must be of type {TypeFullNames.SNSEvent}. " +
310+
$"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}.";
311+
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
312+
}
313+
314+
// Validate method return type - When using SNSEventAttribute, the return type must be either void or Task
315+
if (!lambdaFunctionModel.LambdaMethod.ReturnsVoid && !lambdaFunctionModel.LambdaMethod.ReturnsVoidTask)
316+
{
317+
var errorMessage = $"When using the {nameof(SNSEventAttribute)}, the Lambda method can return either void or {TypeFullNames.Task}";
318+
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
319+
}
320+
}
321+
271322
private static bool ReportDiagnostics(DiagnosticReporter diagnosticReporter, List<Diagnostic> diagnostics)
272323
{
273324
var isValid = true;

Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Amazon.Lambda.Annotations.SourceGenerator.FileIO;
44
using Amazon.Lambda.Annotations.SourceGenerator.Models;
55
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
6+
using Amazon.Lambda.Annotations.SNS;
67
using Amazon.Lambda.Annotations.SQS;
78
using Microsoft.CodeAnalysis;
89
using System;
@@ -221,6 +222,10 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
221222
eventName = ProcessSqsAttribute(lambdaFunction, sqsAttributeModel.Data, currentSyncedEventProperties);
222223
currentSyncedEvents.Add(eventName);
223224
break;
225+
case AttributeModel<SNSEventAttribute> snsAttributeModel:
226+
eventName = ProcessSnsAttribute(lambdaFunction, snsAttributeModel.Data, currentSyncedEventProperties);
227+
currentSyncedEvents.Add(eventName);
228+
break;
224229
}
225230
}
226231

@@ -596,6 +601,46 @@ private string ProcessSqsAttribute(ILambdaFunctionSerializable lambdaFunction, S
596601
return att.ResourceName;
597602
}
598603

604+
/// <summary>
605+
/// Writes all properties associated with <see cref="SNSEventAttribute"/> to the serverless template.
606+
/// </summary>
607+
private string ProcessSnsAttribute(ILambdaFunctionSerializable lambdaFunction, SNSEventAttribute att, Dictionary<string, List<string>> syncedEventProperties)
608+
{
609+
var eventName = att.ResourceName;
610+
var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}";
611+
612+
_templateWriter.SetToken($"{eventPath}.Type", "SNS");
613+
614+
// Topic - SNS topics use Ref to get the ARN
615+
_templateWriter.RemoveToken($"{eventPath}.Properties.Topic");
616+
if (!att.Topic.StartsWith("@"))
617+
{
618+
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Topic", att.Topic);
619+
}
620+
else
621+
{
622+
var topic = att.Topic.Substring(1);
623+
if (_templateWriter.Exists($"{PARAMETERS}.{topic}"))
624+
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Topic.{REF}", topic);
625+
else
626+
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, $"Topic.{REF}", topic);
627+
}
628+
629+
// FilterPolicy
630+
if (att.IsFilterPolicySet)
631+
{
632+
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "FilterPolicy", att.FilterPolicy);
633+
}
634+
635+
// Enabled
636+
if (att.IsEnabledSet)
637+
{
638+
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Enabled", att.Enabled);
639+
}
640+
641+
return att.ResourceName;
642+
}
643+
599644
/// <summary>
600645
/// Writes all properties associated with <see cref="LambdaFunctionRoleAttribute"/> to the serverless template.
601646
/// </summary>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text.RegularExpressions;
5+
6+
namespace Amazon.Lambda.Annotations.SNS
7+
{
8+
/// <summary>
9+
/// This attribute defines the SNS event source configuration for a Lambda function.
10+
/// </summary>
11+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
12+
public class SNSEventAttribute : Attribute
13+
{
14+
private static readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$");
15+
16+
/// <summary>
17+
/// The SNS topic that will act as the event trigger for the Lambda function.
18+
/// This can either be the topic ARN or reference to the SNS topic resource that is already defined in the serverless template.
19+
/// To reference an SNS topic resource in the serverless template, prefix the resource name with "@" symbol.
20+
/// </summary>
21+
public string Topic { get; set; }
22+
23+
/// <summary>
24+
/// 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.
25+
/// If <see cref="Topic"/> is set to an existing CloudFormation resource, than that is used as the default value without the "@" prefix.
26+
/// </summary>
27+
public string ResourceName
28+
{
29+
get
30+
{
31+
if (IsResourceNameSet)
32+
{
33+
return resourceName;
34+
}
35+
if (Topic.StartsWith("@"))
36+
{
37+
return Topic.Substring(1);
38+
}
39+
40+
var arnTokens = Topic.Split(new char[] { ':' }, 6);
41+
var topicName = arnTokens[5];
42+
var sanitizedTopicName = string.Join(string.Empty, topicName.Where(char.IsLetterOrDigit));
43+
return sanitizedTopicName;
44+
}
45+
set => resourceName = value;
46+
}
47+
48+
private string resourceName { get; set; } = null;
49+
internal bool IsResourceNameSet => resourceName != null;
50+
51+
/// <summary>
52+
/// A JSON filter policy that is applied to the SNS subscription.
53+
/// Only messages matching the filter policy will be delivered to the Lambda function.
54+
/// </summary>
55+
public string FilterPolicy { get; set; } = null;
56+
internal bool IsFilterPolicySet => FilterPolicy != null;
57+
58+
/// <summary>
59+
/// If set to false, the event source will be disabled.
60+
/// Default value is true.
61+
/// </summary>
62+
public bool Enabled
63+
{
64+
get => enabled.GetValueOrDefault();
65+
set => enabled = value;
66+
}
67+
private bool? enabled { get; set; }
68+
internal bool IsEnabledSet => enabled.HasValue;
69+
70+
/// <summary>
71+
/// Creates an instance of the <see cref="SNSEventAttribute"/> class.
72+
/// </summary>
73+
/// <param name="topic"><see cref="Topic"/> property</param>
74+
public SNSEventAttribute(string topic)
75+
{
76+
Topic = topic;
77+
}
78+
79+
internal List<string> Validate()
80+
{
81+
var validationErrors = new List<string>();
82+
83+
if (!Topic.StartsWith("@"))
84+
{
85+
var arnTokens = Topic.Split(new char[] { ':' }, 6);
86+
if (arnTokens.Length != 6)
87+
{
88+
validationErrors.Add($"{nameof(SNSEventAttribute.Topic)} = {Topic}. The SNS topic ARN is invalid. The ARN format is 'arn:<partition>:sns:<region>:<account-id>:<topic-name>'");
89+
}
90+
}
91+
if (IsResourceNameSet && !_resourceNameRegex.IsMatch(ResourceName))
92+
{
93+
validationErrors.Add($"{nameof(SNSEventAttribute.ResourceName)} = {ResourceName}. It must only contain alphanumeric characters and must not be an empty string");
94+
}
95+
96+
return validationErrors;
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)