Skip to content

Commit b1c1d26

Browse files
committed
Add [ScheduleEvent] annotation attribute and source generator support
- ScheduleEventAttribute with Schedule (rate/cron), ResourceName, Description, Input, Enabled - ScheduleEventAttributeBuilder for Roslyn AttributeData parsing - Source generator wiring (TypeFullNames, SyntaxReceiver, EventTypeBuilder, AttributeModelBuilder) - CloudFormationWriter ProcessScheduleAttribute (SAM Schedule event rule) - LambdaFunctionValidator ValidateScheduleEvents - DiagnosticDescriptors InvalidScheduleEventAttribute - ScheduleEventAttributeTests (attribute unit tests) - ScheduleEventsTests (CloudFormation writer tests) - E2E source generator snapshot tests - Integration test (ScheduleEventRule) - Sample function (ScheduledProcessing) - .autover change file - README documentation
1 parent e06cfdd commit b1c1d26

25 files changed

+1002
-3
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Projects": [
3+
{
4+
"Name": "Amazon.Lambda.Annotations",
5+
"Type": "Minor",
6+
"ChangelogMessages": [
7+
"Added [ScheduleEvent] annotation attribute for declaratively configuring schedule-triggered Lambda functions with support for rate and cron expressions, description, input, and enabled state."
8+
]
9+
}
10+
]
11+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,5 +281,12 @@ public static class DiagnosticDescriptors
281281
category: "AWSLambdaCSharpGenerator",
282282
DiagnosticSeverity.Error,
283283
isEnabledByDefault: true);
284+
285+
public static readonly DiagnosticDescriptor InvalidScheduleEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0132",
286+
title: "Invalid ScheduleEventAttribute",
287+
messageFormat: "Invalid ScheduleEventAttribute encountered: {0}",
288+
category: "AWSLambdaCSharpGenerator",
289+
DiagnosticSeverity.Error,
290+
isEnabledByDefault: true);
284291
}
285292
}

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.ALB;
3+
using Amazon.Lambda.Annotations.Schedule;
34
using Amazon.Lambda.Annotations.APIGateway;
45
using Amazon.Lambda.Annotations.S3;
56
using Amazon.Lambda.Annotations.SQS;
@@ -101,6 +102,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
101102
Type = TypeModelBuilder.Build(att.AttributeClass, context)
102103
};
103104
}
105+
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.ScheduleEventAttribute), SymbolEqualityComparer.Default))
106+
{
107+
var data = ScheduleEventAttributeBuilder.Build(att);
108+
model = new AttributeModel<ScheduleEventAttribute>
109+
{
110+
Data = data,
111+
Type = TypeModelBuilder.Build(att.AttributeClass, context)
112+
};
113+
}
104114
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.HttpApiAuthorizerAttribute), SymbolEqualityComparer.Default))
105115
{
106116
var data = HttpApiAuthorizerAttributeBuilder.Build(att);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using Amazon.Lambda.Annotations.Schedule;
2+
using Microsoft.CodeAnalysis;
3+
using System;
4+
5+
namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
6+
{
7+
/// <summary>
8+
/// Builder for <see cref="ScheduleEventAttribute"/>.
9+
/// </summary>
10+
public class ScheduleEventAttributeBuilder
11+
{
12+
public static ScheduleEventAttribute Build(AttributeData att)
13+
{
14+
if (att.ConstructorArguments.Length != 1)
15+
{
16+
throw new NotSupportedException($"{TypeFullNames.ScheduleEventAttribute} must have constructor with 1 argument.");
17+
}
18+
var schedule = att.ConstructorArguments[0].Value as string;
19+
var data = new ScheduleEventAttribute(schedule);
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.Description) && pair.Value.Value is string description)
28+
{
29+
data.Description = description;
30+
}
31+
else if (pair.Key == nameof(data.Input) && pair.Value.Value is string input)
32+
{
33+
data.Input = input;
34+
}
35+
else if (pair.Key == nameof(data.Enabled) && pair.Value.Value is bool enabled)
36+
{
37+
data.Enabled = enabled;
38+
}
39+
}
40+
41+
return data;
42+
}
43+
}
44+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
3030
{
3131
events.Add(EventType.S3);
3232
}
33+
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.ScheduleEventAttribute)
34+
{
35+
events.Add(EventType.Schedule);
36+
}
3337
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAuthorizerAttribute
3438
|| attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAuthorizerAttribute)
3539
{

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ internal class SyntaxReceiver : ISyntaxContextReceiver
2323
{ "RestApiAttribute", "RestApi" },
2424
{ "SQSEventAttribute", "SQSEvent" },
2525
{ "ALBApiAttribute", "ALBApi" },
26-
{ "S3EventAttribute", "S3Event" }
26+
{ "S3EventAttribute", "S3Event" },
27+
{ "ScheduleEventAttribute", "ScheduleEvent" }
2728
};
2829

2930
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
@@ -56,6 +56,9 @@ public static class TypeFullNames
5656
public const string S3Event = "Amazon.Lambda.S3Events.S3Event";
5757
public const string S3EventAttribute = "Amazon.Lambda.Annotations.S3.S3EventAttribute";
5858

59+
public const string ScheduledEvent = "Amazon.Lambda.CloudWatchEvents.ScheduledEvents.ScheduledEvent";
60+
public const string ScheduleEventAttribute = "Amazon.Lambda.Annotations.Schedule.ScheduleEventAttribute";
61+
5962
public const string LambdaSerializerAttribute = "Amazon.Lambda.Core.LambdaSerializerAttribute";
6063
public const string DefaultLambdaSerializer = "Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer";
6164

@@ -84,7 +87,8 @@ public static class TypeFullNames
8487
HttpApiAttribute,
8588
SQSEventAttribute,
8689
ALBApiAttribute,
87-
S3EventAttribute
90+
S3EventAttribute,
91+
ScheduleEventAttribute
8892
};
8993
}
9094
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Amazon.Lambda.Annotations.SourceGenerator.Extensions;
55
using Amazon.Lambda.Annotations.SourceGenerator.Models;
66
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
7+
using Amazon.Lambda.Annotations.Schedule;
78
using Amazon.Lambda.Annotations.SQS;
89
using Microsoft.CodeAnalysis;
910
using System.Collections.Generic;
@@ -61,6 +62,7 @@ internal static bool ValidateFunction(GeneratorExecutionContext context, IMethod
6162
// Validate Events
6263
ValidateApiGatewayEvents(lambdaFunctionModel, methodLocation, diagnostics);
6364
ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics);
65+
ValidateScheduleEvents(lambdaFunctionModel, methodLocation, diagnostics);
6466
ValidateAlbEvents(lambdaFunctionModel, methodLocation, diagnostics);
6567
ValidateS3Events(lambdaFunctionModel, methodLocation, diagnostics);
6668

@@ -110,6 +112,16 @@ internal static bool ValidateDependencies(GeneratorExecutionContext context, IMe
110112
}
111113
}
112114

115+
// Check for references to "Amazon.Lambda.CloudWatchEvents" if the Lambda method is annotated with ScheduleEvent attribute.
116+
if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.ScheduleEventAttribute))
117+
{
118+
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.CloudWatchEvents") == null)
119+
{
120+
diagnosticReporter.Report(Diagnostic.Create(DiagnosticDescriptors.MissingDependencies, methodLocation, "Amazon.Lambda.CloudWatchEvents"));
121+
return false;
122+
}
123+
}
124+
113125
return true;
114126
}
115127

@@ -420,6 +432,43 @@ private static void ValidateS3Events(LambdaFunctionModel lambdaFunctionModel, Lo
420432
}
421433
}
422434

435+
private static void ValidateScheduleEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List<Diagnostic> diagnostics)
436+
{
437+
if (!lambdaFunctionModel.LambdaMethod.Events.Contains(EventType.Schedule))
438+
{
439+
return;
440+
}
441+
442+
foreach (var att in lambdaFunctionModel.Attributes)
443+
{
444+
if (att.Type.FullName != TypeFullNames.ScheduleEventAttribute)
445+
continue;
446+
447+
var scheduleEventAttribute = ((AttributeModel<ScheduleEventAttribute>)att).Data;
448+
var validationErrors = scheduleEventAttribute.Validate();
449+
validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidScheduleEventAttribute, methodLocation, errorMessage)));
450+
}
451+
452+
// Validate method parameters
453+
var parameters = lambdaFunctionModel.LambdaMethod.Parameters;
454+
if (parameters.Count > 2 ||
455+
(parameters.Count >= 1 && parameters[0].Type.FullName != TypeFullNames.ScheduledEvent) ||
456+
(parameters.Count == 2 && parameters[1].Type.FullName != TypeFullNames.ILambdaContext))
457+
{
458+
var errorMessage = $"When using the {nameof(ScheduleEventAttribute)}, the Lambda method can accept at most 2 parameters. " +
459+
$"The first parameter must be of type {TypeFullNames.ScheduledEvent}. " +
460+
$"The second parameter is optional and must be of type {TypeFullNames.ILambdaContext}.";
461+
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
462+
}
463+
464+
// Validate return type - must be void or Task
465+
if (!lambdaFunctionModel.LambdaMethod.ReturnsVoid && !lambdaFunctionModel.LambdaMethod.ReturnsVoidTask)
466+
{
467+
var errorMessage = $"When using the {nameof(ScheduleEventAttribute)}, the Lambda method can return either void or {TypeFullNames.Task}";
468+
diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidLambdaMethodSignature, methodLocation, errorMessage));
469+
}
470+
}
471+
423472
private static bool ReportDiagnostics(DiagnosticReporter diagnosticReporter, List<Diagnostic> diagnostics)
424473
{
425474
var isValid = true;

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics;
44
using Amazon.Lambda.Annotations.SourceGenerator.FileIO;
55
using Amazon.Lambda.Annotations.SourceGenerator.Models;
6+
using Amazon.Lambda.Annotations.Schedule;
67
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
78
using Amazon.Lambda.Annotations.S3;
89
using Amazon.Lambda.Annotations.SQS;
@@ -232,6 +233,10 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
232233
eventName = ProcessS3Attribute(lambdaFunction, s3AttributeModel.Data, currentSyncedEventProperties);
233234
currentSyncedEvents.Add(eventName);
234235
break;
236+
case AttributeModel<ScheduleEventAttribute> scheduleAttributeModel:
237+
eventName = ProcessScheduleAttribute(lambdaFunction, scheduleAttributeModel.Data, currentSyncedEventProperties);
238+
currentSyncedEvents.Add(eventName);
239+
break;
235240
}
236241
}
237242

@@ -608,6 +613,40 @@ private string ProcessSqsAttribute(ILambdaFunctionSerializable lambdaFunction, S
608613
return att.ResourceName;
609614
}
610615

616+
/// <summary>
617+
/// Writes all properties associated with <see cref="ScheduleEventAttribute"/> to the serverless template.
618+
/// </summary>
619+
private string ProcessScheduleAttribute(ILambdaFunctionSerializable lambdaFunction, ScheduleEventAttribute att, Dictionary<string, List<string>> syncedEventProperties)
620+
{
621+
var eventName = att.ResourceName;
622+
var eventPath = $"Resources.{lambdaFunction.ResourceName}.Properties.Events.{eventName}";
623+
624+
_templateWriter.SetToken($"{eventPath}.Type", "Schedule");
625+
626+
// Schedule expression
627+
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Schedule", att.Schedule);
628+
629+
// Description
630+
if (att.IsDescriptionSet)
631+
{
632+
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Description", att.Description);
633+
}
634+
635+
// Input
636+
if (att.IsInputSet)
637+
{
638+
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Input", att.Input);
639+
}
640+
641+
// Enabled
642+
if (att.IsEnabledSet)
643+
{
644+
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Enabled", att.Enabled);
645+
}
646+
647+
return att.ResourceName;
648+
}
649+
611650
/// <summary>
612651
/// Writes all properties associated with <see cref="S3EventAttribute"/> to the serverless template.
613652
/// </summary>

Libraries/src/Amazon.Lambda.Annotations/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Topics:
1919
- [Amazon API Gateway example](#amazon-api-gateway-example)
2020
- [Amazon S3 example](#amazon-s3-example)
2121
- [SQS Event Example](#sqs-event-example)
22+
- [Schedule Event Example](#schedule-event-example)
2223
- [Application Load Balancer (ALB) Example](#application-load-balancer-alb-example)
2324
- [Custom Lambda Authorizer Example](#custom-lambda-authorizer-example)
2425
- [HTTP API Authorizer](#http-api-authorizer)
@@ -1073,6 +1074,25 @@ The `ALBApi` attribute requires an existing ALB listener. Here is a minimal exam
10731074

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

1077+
## Schedule Event Example
1078+
This example shows how to use the `ScheduleEvent` attribute to trigger a Lambda function on a schedule using EventBridge.
1079+
1080+
The `ScheduleEvent` attribute contains the following properties:
1081+
* **Schedule** (Required) - The schedule expression. Supports `rate()` and `cron()` expressions (e.g. `rate(5 minutes)`, `cron(0 12 * * ? *)`).
1082+
* **ResourceName** (Optional) - The CloudFormation resource name for the schedule event.
1083+
* **Description** (Optional) - A description for the schedule rule.
1084+
* **Input** (Optional) - A JSON string to pass as input to the Lambda function.
1085+
* **Enabled** (Optional) - If false, the schedule rule is disabled. Default is true.
1086+
1087+
```csharp
1088+
[LambdaFunction(ResourceName = "ScheduledHandler", Policies = "AWSLambdaBasicExecutionRole")]
1089+
[ScheduleEvent("rate(5 minutes)", ResourceName = "FiveMinuteSchedule", Description = "Runs every 5 minutes")]
1090+
public void HandleSchedule(ScheduledEvent evnt, ILambdaContext lambdaContext)
1091+
{
1092+
lambdaContext.Logger.Log($"Scheduled event received at {evnt.Time}");
1093+
}
1094+
```
1095+
10761096
## Custom Lambda Authorizer Example
10771097

10781098
Lambda Annotations supports defining custom Lambda authorizers using attributes. Custom authorizers let you control access to your API Gateway endpoints by running a Lambda function that validates tokens or request parameters before the target function is invoked. The source generator automatically wires up the authorizer resources and references in the CloudFormation template.
@@ -1420,6 +1440,8 @@ parameter to the `LambdaFunction` must be the event object and the event source
14201440
* 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.
14211441
* SQSEvent
14221442
* 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.
1443+
* ScheduleEvent
1444+
* Triggers the Lambda function on a schedule using EventBridge. Supports rate and cron expressions.
14231445
* ALBApi
14241446
* 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.
14251447

0 commit comments

Comments
 (0)